輸出端切換器

輸出切換器是 Cast SDK 的一項功能,可讓使用者從 Android 13 開始,在內容的本機和遠端播放之間順暢轉移。目標是協助傳送端應用程式輕鬆快速地控制內容的播放位置。輸出切換器會使用 MediaRouter 程式庫,在手機喇叭、已配對的藍牙裝置和支援 Cast 的遠端裝置之間切換內容播放。用途可細分為下列情境:

下載並使用 CastVideos-android 範例應用程式,參考如何在應用程式中導入輸出切換器。

如要支援本機到遠端、遠端到本機,以及遠端到遠端,請按照本指南的步驟啟用輸出切換器。如要支援在本機裝置喇叭和已配對的藍牙裝置之間轉移音訊,不需要執行其他步驟。

輸出端切換器 UI

「輸出裝置切換器」會顯示可用的本機和遠端裝置,以及目前的裝置狀態,包括裝置是否已選取、是否正在連線,以及目前的音量。如果除了目前裝置外還有其他裝置,按一下其他裝置即可將媒體播放內容轉移至所選裝置。

已知問題

  • 切換至 Cast SDK 通知時,系統會關閉並重新建立為本機播放作業建立的媒體工作階段。

進入點

媒體通知

如果應用程式發布的媒體通知含有 MediaSession,表示內容正在本機播放,媒體通知右上角會顯示通知晶片,當中包含目前播放內容的裝置名稱 (例如手機喇叭)。輕觸通知晶片會開啟「輸出切換器」對話方塊系統 UI。

音量設定

你也可以點選裝置上的實體音量鍵、輕觸底部的設定圖示,然後輕觸「在 <投放裝置> 上播放『<應用程式名稱>』」文字,觸發輸出切換器對話方塊系統 UI。

步驟摘要

必要條件

  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 是廣播接收器,可透過系統 UI 在裝置間轉移媒體。詳情請參閱 MediaTransferReceiver 參考資料

本機到遠端

使用者將播放內容從本機切換至遠端時,Cast SDK 會自動啟動 Cast 工作階段。不過,應用程式需要處理從本機切換到遠端的情況,例如停止本機播放,並在 Google Cast 裝置上載入媒體。應用程式應使用 onSessionStarted()onSessionEnded() 回呼,監聽 Cast SessionManagerListener,並在收到 Cast SessionManager 回呼時處理動作。應用程式應確保在開啟輸出切換器對話方塊且應用程式不在前景時,這些回呼仍有效。

更新 SessionManagerListener,以便在背景投放內容

舊版 Google Cast 體驗已支援應用程式處於前景時的本機到遠端投放。一般來說,使用者點選應用程式中的 Cast 圖示,然後選擇要串流媒體的裝置,就會開始使用 Cast 服務。在這種情況下,應用程式需要在 onCreate()onStart() 中向 SessionManagerListener 註冊,並在應用程式活動的 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);
    }
  }
}

更新後,當應用程式在背景執行時,本機到遠端投放的運作方式與傳統投放相同,且從藍牙裝置切換至 Google Cast 裝置時,不需要額外作業。

遠端到本機

輸出切換器可讓你從遠端播放切換至手機揚聲器或本機藍牙裝置。如要啟用這項功能,請在 CastOptions 上將 setRemoteToLocalEnabled 旗標設為 true

如果目前的傳送端裝置加入有多個傳送端的現有工作階段,且應用程式需要檢查是否允許在本機轉移目前的媒體,應用程式應使用 SessionTransferCallbackonTransferred 回呼,檢查 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 SDK 開發人員控制台的接收器應用程式設定中,支援 Google Cast for Audio 的應用程式。

透過音箱增加串流裝置

使用輸出切換器的音訊應用程式,可在 Cast 工作階段期間使用串流擴展功能,將音訊擴展至多個支援 Cast 的音箱裝置。

這項功能由 Cast 平台支援,如果應用程式使用預設 UI,則不需要進行任何變更。如果使用自訂 UI,應用程式應更新 UI,反映應用程式正在投放至群組。

如要在串流擴展期間取得新的擴展群組名稱,請使用 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 的裝置,即可將其從動態群組中移除。