输出切换器

输出切换器是 Cast SDK 的一项功能,可实现从 Android 13 开始在本地和远程播放内容之间无缝切换。目标是帮助发送方应用轻松快速地控制内容播放位置。 输出切换器使用 MediaRouter 库在手机扬声器、已配对的蓝牙设备和支持 Cast 的远程设备之间切换内容播放。使用情形可分为以下几种:

下载并使用 CastVideos-android 示例应用,了解如何在应用中实现输出切换器。

应启用输出切换器,以支持本地到远程、远程到本地和远程到远程,具体步骤请参阅本指南。无需执行任何其他步骤即可支持在本地设备扬声器和已配对的蓝牙设备之间进行转移。

输出切换器界面

输出切换器会显示可用的本地设备和远程设备,以及当前的设备状态,包括设备是否已选择、是否正在连接、当前音量。如果除了当前设备之外还有其他设备,点击其他设备可将媒体播放转移到所选设备。

已知问题

  • 切换到 Cast SDK 通知时,为本地播放创建的媒体会话将被关闭并重新创建。

入口点

媒体通知

如果应用发布了带有 MediaSession 的媒体通知以进行本地播放(在本地播放),则媒体通知的右上角会显示一个通知条状标签,其中包含当前正在播放内容的设备名称(例如手机扬声器)。点按通知芯片会打开“输出切换器”对话框系统界面。

音量设置

您还可以通过以下方式触发输出切换器对话框系统界面:点击设备上的实体音量按钮,点按底部的“设置”图标,然后点按“在 <投放设备> 上播放 <应用名称>”文本。

步骤摘要

前提条件

  1. 将现有的 Android 应用迁移到 AndroidX。
  2. 更新应用的 build.gradle,以使用 Output Switcher 所需的最低 Android Sender SDK 版本:
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. 应用支持媒体通知。
  4. 设备搭载 Android 13。

设置媒体通知

如需使用输出切换器,音频视频应用必须创建媒体通知,以显示其媒体的播放状态和本地播放控件。这需要创建 MediaSession、使用 MediaSession 的令牌设置 MediaStyle,以及在通知中设置媒体控件。

如果您目前未使用 MediaStyleMediaSession,以下代码段展示了如何设置它们,并且提供了有关为音频视频应用设置媒体会话回调的指南:

Kotlin
// Create a media session. NotificationCompat.MediaStyle
// PlayerService is your own Service or Activity responsible for media playback.
val mediaSession = MediaSessionCompat(this, "PlayerService")

// Create a MediaStyle object and supply your media session token to it.
val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken)

// Create a Notification which is styled by your MediaStyle object.
// This connects your media session to the media controls.
// Don't forget to include a small icon.
val notification = Notification.Builder(this@PlayerService, CHANNEL_ID)
    .setStyle(mediaStyle)
    .setSmallIcon(R.drawable.ic_app_logo)
    .build()

// Specify any actions which your users can perform, such as pausing and skipping to the next track.
val pauseAction: Notification.Action = Notification.Action.Builder(
        pauseIcon, "Pause", pauseIntent
    ).build()
notification.addAction(pauseAction)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    // Create a media session. NotificationCompat.MediaStyle
    // PlayerService is your own Service or Activity responsible for media playback.
    MediaSession mediaSession = new MediaSession(this, "PlayerService");

    // Create a MediaStyle object and supply your media session token to it.
    Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken());

    // Specify any actions which your users can perform, such as pausing and skipping to the next track.
    Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build();

    // Create a Notification which is styled by your MediaStyle object.
    // This connects your media session to the media controls.
    // Don't forget to include a small icon.
    String CHANNEL_ID = "CHANNEL_ID";
    Notification notification = new Notification.Builder(this, CHANNEL_ID)
        .setStyle(mediaStyle)
        .setSmallIcon(R.drawable.ic_app_logo)
        .addAction(pauseAction)
        .build();
}

此外,为了在通知中填充媒体信息,您需要将媒体的元数据和播放状态添加到 MediaSession

如需向 MediaSession 添加元数据,请使用 setMetaData(),并在 MediaMetadataCompat.Builder() 中为媒体提供所有相关的 MediaMetadata 常量。

Kotlin
mediaSession.setMetadata(MediaMetadataCompat.Builder()
    // Title
    .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

    // Artist
    // Could also be the channel name or TV series.
    .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

    // Album art
    // Could also be a screenshot or hero image for video content
    // The URI scheme needs to be "content", "file", or "android.resource".
    .putString(
        MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)
    )

    // Duration
    // If duration isn't set, such as for live broadcasts, then the progress
    // indicator won't be shown on the seekbar.
    .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

    .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setMetadata(
        new MediaMetadataCompat.Builder()
        // Title
        .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

        // Artist
        // Could also be the channel name or TV series.
        .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

        // Album art
        // Could also be a screenshot or hero image for video content
        // The URI scheme needs to be "content", "file", or "android.resource".
        .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)

        // Duration
        // If duration isn't set, such as for live broadcasts, then the progress
        // indicator won't be shown on the seekbar.
        .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

        .build()
    );
}

如需将播放状态添加到 MediaSession,请使用 setPlaybackState(),并在 PlaybackStateCompat.Builder() 中为媒体提供所有相关的 PlaybackStateCompat 常量。

Kotlin
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setState(
            PlaybackStateCompat.STATE_PLAYING,

            // Playback position
            // Used to update the elapsed time and the progress bar.
            mediaPlayer.currentPosition.toLong(),

            // Playback speed
            // Determines the rate at which the elapsed time changes.
            playbackSpeed
        )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setPlaybackState(
        new PlaybackStateCompat.Builder()
            .setState(
                 PlaybackStateCompat.STATE_PLAYING,

                // Playback position
                // Used to update the elapsed time and the progress bar.
                mediaPlayer.currentPosition.toLong(),

                // Playback speed
                // Determines the rate at which the elapsed time changes.
                playbackSpeed
            )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
    );
}

视频应用通知行为

不支持在后台进行本地播放的视频应用或音频应用应针对媒体通知具有特定行为,以避免在不支持播放的情况下发送媒体命令时出现问题:

  • 在本地播放媒体且应用位于前台时,发布媒体通知。
  • 当应用在后台运行时,暂停本地播放并关闭通知。
  • 当应用移回前台时,本地播放应恢复,并且通知应重新发布。

在 AndroidManifest.xml 中启用输出切换器

如需启用输出切换器,需要将 MediaTransferReceiver 添加到应用的 AndroidManifest.xml 中。如果不是,则不会启用该功能,并且远程到本地功能标志也会失效。

<application>
    ...
    <receiver
         android:name="androidx.mediarouter.media.MediaTransferReceiver"
         android:exported="true">
    </receiver>
    ...
</application>

MediaTransferReceiver 是一个广播接收器,可实现设备之间通过系统界面进行媒体传输。如需了解详情,请参阅 MediaTransferReceiver 参考文档

本地到远程

当用户将播放从本地切换到远程时,Cast SDK 会自动启动 Cast 会话。不过,应用需要处理从本地切换到远程的情况,例如停止本地播放并在投放设备上加载媒体。应用应使用 onSessionStarted()onSessionEnded() 回调监听 Cast SessionManagerListener,并在收到 Cast SessionManager 回调时处理相应操作。应用应确保在打开输出源切换器对话框且应用不在前台时,这些回调仍处于有效状态。

更新了 SessionManagerListener 以支持后台投屏

当应用位于前台时,旧版 Cast 体验已支持本地到远程的转换。典型的 Cast 体验从用户点击应用中的“投射”图标并选择用于流式传输媒体的设备开始。在这种情况下,应用需要在 onCreate()onStart() 中注册 SessionManagerListener,并在应用的 Activity 的 onStop()onDestroy() 中取消注册监听器。

借助使用输出源切换器的新投屏体验,应用可以在后台开始投屏。这对于在后台播放时发布通知的音频应用尤其有用。应用可以在服务的 onCreate() 中注册 SessionManager 监听器,并在服务的 onDestroy() 中取消注册。当应用在后台运行时,应始终接收本地到远程的回调(例如 onSessionStarted)。

如果应用使用 MediaBrowserService,建议在此处注册 SessionManagerListener

Kotlin
class MyService : Service() {
    private var castContext: CastContext? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext
            .getSessionManager()
            .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext
                .getSessionManager()
                .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
        }
    }
}
Java
public class MyService extends Service {
  private CastContext castContext;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
    }
  }
}

在此更新中,当应用在后台运行时,本地到远程的投屏行为与常规投屏行为相同,并且无需额外操作即可从蓝牙设备切换到 Cast 设备。

远程到本地

借助输出切换器,您可以从远程播放切换到手机扬声器或本地蓝牙设备。为此,您可以在 CastOptions 上将 setRemoteToLocalEnabled 标志设置为 true

如果当前发送方设备加入具有多个发送方的现有会话,并且应用需要检查是否允许在本地转移当前媒体,则应用应使用 SessionTransferCallbackonTransferred 回调来检查 SessionState

设置 setRemoteToLocalEnabled 标志

CastOptions.Builder 提供了一个 setRemoteToLocalEnabled,用于在有活跃的 Cast 会话时,在“输出切换器”对话框中显示或隐藏手机扬声器和本地蓝牙设备作为转移目标。

Kotlin
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        ...
        return new CastOptions.Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
  }
}

继续在本地播放

支持从远程到本地的应用应注册 SessionTransferCallback,以便在发生相应事件时收到通知,从而检查是否应允许媒体转移并继续在本地播放。

CastContext#addSessionTransferCallback(SessionTransferCallback) 允许应用注册其 SessionTransferCallback,并在发送方转移到本地播放时监听 onTransferredonTransferFailed 回调。

应用取消注册其 SessionTransferCallback 后,将不再接收 SessionTransferCallback

SessionTransferCallback 是现有 SessionManagerListener 回调的扩展,会在 onSessionEnded 触发后触发。从远程到本地的回调顺序为:

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. onTransferred

由于当应用在后台投屏时,媒体通知芯片可以打开输出切换器,因此应用需要根据是否支持后台播放来以不同的方式处理向本地的转移。如果转移失败,onTransferFailed 会在发生错误时随时触发。

支持后台播放的应用

对于支持在后台播放的应用(通常是音频应用),建议使用 Service(例如 MediaBrowserService)。服务应监听 onTransferred 回调,并在应用位于前台或后台时恢复本地播放。

Kotlin
class MyService : Service() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyService extends Service {
    private CastContext castContext;
    private SessionTransferCallback sessionTransferCallback;

    @Override
    protected void onCreate() {
        castContext = CastContext.getSharedInstance(this);
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession.class);
        sessionTransferCallback = new MySessionTransferCallback();
        castContext.addSessionTransferCallback(sessionTransferCallback);
    }

    @Override
    protected void onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession.class);
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback);
            }
        }
    }

    public static class MySessionTransferCallback extends SessionTransferCallback {
        public MySessionTransferCallback() {}

        @Override
        public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
            // Perform necessary steps prior to onTransferred
        }

        @Override
        public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                                  SessionState sessionState) {
            if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        @Override
        public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                     @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
            // Handle transfer failure.
        }
    }
}

不支持后台播放的应用

对于不支持后台播放的应用(通常是视频应用),建议监听 onTransferred 回调,并在应用位于前台时在本地恢复播放。

如果应用在后台运行,则应暂停播放,并存储 SessionState 中的必要信息(例如媒体元数据和播放位置)。当应用从后台转到前台时,本地播放应继续使用存储的信息。

Kotlin
class MyActivity : AppCompatActivity() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.

                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyActivity extends AppCompatActivity {
  private CastContext castContext;
  private SessionTransferCallback sessionTransferCallback;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
     sessionTransferCallback = new MySessionTransferCallback();
     castContext.addSessionTransferCallback(sessionTransferCallback);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
      if (sessionTransferCallback != null) {
         castContext.removeSessionTransferCallback(sessionTransferCallback);
      }
    }
  }

  public static class MySessionTransferCallback extends SessionTransferCallback {
    public MySessionTransferCallback() {}

    @Override
    public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
        // Perform necessary steps prior to onTransferred
    }

    @Override
    public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                               SessionState sessionState) {
      if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
        // Remote stream is transferred to the local device.

        // Retrieve information from the SessionState to continue playback on the local player.
      }
    }

    @Override
    public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                 @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
      // Handle transfer failure.
    }
  }
}

遥控器到遥控器

输出切换器支持使用流扩展功能扩展到多个支持 Cast 的音箱设备,以用于音频应用。

音频应用是指在 Google Cast SDK 开发者控制台的接收器应用设置中支持 Google Cast for Audio 的应用

使用音箱进行流扩展

使用输出切换器的音频应用能够在 Cast 会话期间使用流扩展功能将音频扩展到多个支持 Cast 的音箱设备。

Cast 平台支持此功能,如果应用使用默认界面,则无需进行任何进一步的更改。如果使用自定义界面,应用应更新界面以反映应用正在向群组投屏。

如需在流扩展期间获取新的展开组名称,请使用 CastSession#addCastListener 注册 Cast.Listener。然后在 onDeviceNameChanged 回调期间调用 CastSession#getCastDevice()

Kotlin
class MyActivity : Activity() {
    private var mCastSession: CastSession? = null
    private lateinit var mCastContext: CastContext
    private lateinit var mSessionManager: SessionManager
    private val mSessionManagerListener: SessionManagerListener<CastSession> =
        SessionManagerListenerImpl()
    private val mCastListener = CastListener()

    private inner class SessionManagerListenerImpl : SessionManagerListener<CastSession?> {
        override fun onSessionStarting(session: CastSession?) {}

        override fun onSessionStarted(session: CastSession?, sessionId: String) {
            addCastListener(session)
        }

        override fun onSessionStartFailed(session: CastSession?, error: Int) {}

        override fun onSessionSuspended(session: CastSession?, reason Int) {
            removeCastListener()
        }

        override fun onSessionResuming(session: CastSession?, sessionId: String) {}

        override fun onSessionResumed(session: CastSession?, wasSuspended: Boolean) {
            addCastListener(session)
        }

        override fun onSessionResumeFailed(session: CastSession?, error: Int) {}

        override fun onSessionEnding(session: CastSession?) {}

        override fun onSessionEnded(session: CastSession?, error: Int) {
            removeCastListener()
        }
    }

    private inner class CastListener : Cast.Listener() {
        override fun onDeviceNameChanged() {
            mCastSession?.let {
                val castDevice = it.castDevice
                val deviceName = castDevice.friendlyName
                // Update UIs with the new cast device name.
            }
        }
    }

    private fun addCastListener(castSession: CastSession) {
        mCastSession = castSession
        mCastSession?.addCastListener(mCastListener)
    }

    private fun removeCastListener() {
        mCastSession?.removeCastListener(mCastListener)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mCastContext = CastContext.getSharedInstance(this)
        mSessionManager = mCastContext.sessionManager
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }

    override fun onDestroy() {
        super.onDestroy()
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession::class.java)
    }
}
Java
public class MyActivity extends Activity {
    private CastContext mCastContext;
    private CastSession mCastSession;
    private SessionManager mSessionManager;
    private SessionManagerListener<CastSession> mSessionManagerListener =
            new SessionManagerListenerImpl();
    private Cast.Listener mCastListener = new CastListener();

    private class SessionManagerListenerImpl implements SessionManagerListener<CastSession> {
        @Override
        public void onSessionStarting(CastSession session) {}
        @Override
        public void onSessionStarted(CastSession session, String sessionId) {
            addCastListener(session);
        }
        @Override
        public void onSessionStartFailed(CastSession session, int error) {}
        @Override
        public void onSessionSuspended(CastSession session, int reason) {
            removeCastListener();
        }
        @Override
        public void onSessionResuming(CastSession session, String sessionId) {}
        @Override
        public void onSessionResumed(CastSession session, boolean wasSuspended) {
            addCastListener(session);
        }
        @Override
        public void onSessionResumeFailed(CastSession session, int error) {}
        @Override
        public void onSessionEnding(CastSession session) {}
        @Override
        public void onSessionEnded(CastSession session, int error) {
            removeCastListener();
        }
    }

    private class CastListener extends Cast.Listener {
         @Override
         public void onDeviceNameChanged() {
             if (mCastSession == null) {
                 return;
             }
             CastDevice castDevice = mCastSession.getCastDevice();
             String deviceName = castDevice.getFriendlyName();
             // Update UIs with the new cast device name.
         }
    }

    private void addCastListener(CastSession castSession) {
        mCastSession = castSession;
        mCastSession.addCastListener(mCastListener);
    }

    private void removeCastListener() {
        if (mCastSession != null) {
            mCastSession.removeCastListener(mCastListener);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCastContext = CastContext.getSharedInstance(this);
        mSessionManager = mCastContext.getSessionManager();
        mSessionManager.addSessionManagerListener(mSessionManagerListener, CastSession.class);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mSessionManager.removeSessionManagerListener(mSessionManagerListener, CastSession.class);
    }
}

测试远程到远程

如需测试此功能,请执行以下操作:

  1. 使用常规投屏或本地到远程将内容投放到支持 Cast 的设备。
  2. 使用其中一个入口点打开输出切换器。
  3. 点按另一个支持 Cast 的设备,音频应用会将内容扩展到该附加设备,从而创建一个动态群组。
  4. 再次点按支持 Cast 的设备,该设备将从动态群组中移除。