输出切换器

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

应启用输出切换器,以支持“本地到远程”和“远程到本地”功能 和远程转远程服务。没有任何 需要执行额外的步骤才能支持在本地设备之间转移数据 音箱和已配对的蓝牙设备。

输出切换器界面

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

已知问题

  • 为本地播放创建的媒体会话将被关闭并重新创建 。

入口点

媒体通知

如果应用发布了带有 MediaSession: 本地播放(在本地播放),媒体通知的右上角 会显示带有设备名称(例如手机扬声器)的通知条状标签, 当前用于播放相应内容的位置。点按通知条状标签即可打开 输出切换器对话框系统界面。

音量设置

您也可以通过点击 点按设备上的实体音量按钮,点按底部的设置图标, 点按“Play <App Name>”在 <Cast Device> 上文本。

步骤摘要

前提条件

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

设置媒体通知

如需使用输出切换器,请执行以下操作: audio视频应用 才能创建媒体通知,以显示播放状态和 媒体控件,以进行本地播放。这需要创建一个 MediaSession, 设置 MediaStyle 替换为MediaSession的令牌,并在 通知。

如果您当前未使用 MediaStyleMediaSession,则代码段 下面介绍了如何设置媒体规则,并提供了媒体设置指南 会话回调 audio视频 应用:

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() 并提供所有相关的 MediaMetadata 常量: 媒体资源中 MediaMetadataCompat.Builder()

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 常量 PlaybackStateCompat.Builder()

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 设备上加载媒体。应用应监听 Cast SessionManagerListener, 使用 onSessionStarted()onSessionEnded() 回调,并在接收 Cast 时处理操作 SessionManager 回调。应用应确保在 “Output Switcher”对话框已打开,但应用未在前台运行。

更新 SessionManagerListener 以实现后台投放

旧版 Cast 体验已支持在应用运行 在前台运行当用户点击“投放”图标时,开始典型的投放体验 并选择设备来流式传输媒体内容。在这种情况下,应用需要 注册到 SessionManagerListener, 在onCreate()onStart() 并在 onStop()onDestroy() 与应用活动相关的信息

得益于使用输出切换器的全新投放体验,应用可启动 在后台运行时进行投射。这一功能对于 可在后台播放时发布通知的应用程序。应用可以注册 SessionManager 在服务的 onCreate() 中使用监听器并在 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);
    }
  }
}

经过此次更新,当 应用在后台运行,因此无需执行额外的操作即可从 从蓝牙设备到投放设备。

远程到本地

输出切换器提供了从远程播放 手机扬声器或本地蓝牙设备。这可以通过设置 setRemoteToLocalEnabled 标志设置为 CastOptions 上的 true

当前发送方设备通过以下设备加入现有会话: 多个发送者,并且应用需要检查是否允许当前媒体 则应用应使用 onTransferred SessionTransferCallback 的回调 以查看 SessionState

设置 setRemoteToLocalEnabled 标志

CastOptions.Builder 提供 setRemoteToLocalEnabled,用于显示或隐藏手机扬声器和本地蓝牙设备 显示“输出切换器”对话框中的目标。

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 的应用 Google Cast SDK Developer 中的“设置”菜单 控制台

使用音响设备扩展视频流功能

使用输出切换器的音频应用能够展开音频 在投放会话期间,通过流式传输将内容投放到多台支持 Cast 的音响设备上 展开。

Cast 平台支持此功能,无需执行任何其他操作 更改。如果使用自定义界面,则应用 应更新界面,以反映应用正在投射到某个组。

如需在数据流展开期间获取新的展开后群组名称,请执行以下操作: 注册 Cast.Listener 使用 CastSession#addCastListener。 然后调用 CastSession#getCastDevice()onDeviceNameChanged 回调期间触发。

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. 使用传统投射功能或借助 local-to-remote
  2. 使用其中一个入口点打开输出切换器。
  3. 点按另一台支持 Cast 的设备,音频应用就会将内容扩展到 创建动态群组
  4. 再次点按支持 Cast 的设备,系统就会将其从动态模式中移除 。