輸出端切換器是 Cast SDK 的一項功能,可順暢傳輸
從 Android 13 開始,本機和遠端播放內容間取得的平衡目標
可以讓傳送者應用程式輕鬆快速地控製播放內容的位置
輸出端切換器會使用
將 MediaRouter
程式庫
切換手機喇叭、配對的藍牙裝置、
和支援 Cast 的遠端裝置用途可細分為
情境:
下載並使用 CastVideos-android 範例應用程式 ,用於瞭解如何在應用程式中實作輸出切換器。
請按照本指南中的步驟啟用輸出切換器,以支援本機至遠端、遠端至本機和遠端至遠端的連線。沒有任何 進行額外步驟以支援本機裝置之間的轉移作業 喇叭和配對的藍牙裝置。
輸出端切換器 UI
輸出切換器會顯示可用的本機和遠端裝置,以及目前的裝置狀態,包括裝置是否已選取、是否正在連線,以及目前的音量。如果還有其他裝置 到目前的裝置,點選其他裝置即可傳輸媒體 。
已知問題
- 系統會關閉並重新建立為本機播放建立的媒體工作階段 請在切換至 Cast SDK 通知後 看到投影片內容
進入點
媒體通知
如果應用程式會發布含有媒體通知的應用程式
MediaSession
:
本機播放 (在本機播放):媒體通知的右上角
會顯示通知方塊,內含裝置名稱 (例如手機喇叭)
以及目前播放的內容輕觸通知方塊即可開啟
輸出端切換器對話方塊系統 UI
音量設定
您也可透過按一下 實體音量按鈕、輕觸畫面底部的設定圖示、 並輕觸「播放 <應用程式名稱>」在 <Cast Device> 上文字。
步驟摘要
- 確認符合必要條件
- 在 AndroidManifest.xml 中啟用輸出切換器
- 更新背景投放功能的 SessionManagerListener
- 新增遠端對遠端支援功能
- 設定 setRemoteToLocalEnabled 標記
- 繼續在本機播放
必要條件
- 將現有的 Android 應用程式遷移至 AndroidX。
- 更新應用程式的
build.gradle
,使用最低版本需求 輸出端切換器的 Android Sender SDK:dependencies { ... implementation 'com.google.android.gms:play-services-cast-framework:21.2.0' ... }
- 應用程式支援媒體通知。
- 搭載 Android 13 的裝置。
設定媒體通知
使用輸出端切換器的方法如下:
audio 和
影片應用程式
,以便建立媒體通知來顯示播放狀態,
以便控製本機播放的媒體。因此您需要建立
MediaSession
、
設定
MediaStyle
與 MediaSession
權杖相符,並在
通知。
如果您目前沒有使用 MediaStyle
和 MediaSession
,請使用以下程式碼片段
將說明設定方式以及媒體設定指南
工作階段回呼
「audio」和
影片
應用程式:
// 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)
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()
。
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() )
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()
。
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() )
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
敬上
是一種廣播接收器,可讓搭載系統的裝置在不同裝置間傳輸媒體
第一種是使用無代碼解決方案 AutoML
透過使用者介面建立機器學習模型請參閱 MediaTransferReceiver
參考資料
瞭解詳情
本機對遠端
當使用者從本機切換至遠端播放時,Cast SDK 就會啟動
投放工作階段。不過,應用程式需要處理從本機切換至遠端的情況,例如停止本機播放並載入投放裝置上的媒體。應用程式應使用 onSessionStarted()
和 onSessionEnded()
回呼,監聽 Cast SessionManagerListener
,並在收到 Cast SessionManager
回呼時處理動作。應用程式應確保這些回呼在
輸出切換器對話方塊已開啟,且應用程式不在前景執行。
更新背景投放的 SessionManagerListener
舊版 Cast 服務已可在應用程式符合下列條件的情況下,支援本機對遠端裝置
在前景執行當使用者點選「投放」圖示時,一般的投放體驗隨即啟動
,並選擇要串流媒體的裝置。在此情況下,應用程式必須
註冊
SessionManagerListener
、
在onCreate()
或
onStart()
,然後從中取消註冊
onStop()
或
onDestroy()
應用程式活動。
透過使用輸出切換器投放內容,應用程式可啟動
並投放內容這在播放音訊時特別有用
在背景播放時張貼通知的應用程式。應用程式可註冊
SessionManager
並在 onCreate()
的接聽程式中將其取消註冊,並取消註冊 onDestroy()
此狀態。應用程式應一律接收本機對遠端回呼 (例如
以 onSessionStarted
的身分)
當應用程式在背景執行時。
如果應用程式使用 MediaBrowserService
,建議您在該處註冊 SessionManagerListener
。
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) } } }
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
,可在裝置轉移對像中顯示或隱藏手機喇叭和本機藍牙裝置
如果有執行中的投放工作階段,輸出端切換器對話方塊就會顯示目標。
class CastOptionsProvider : OptionsProvider { fun getCastOptions(context: Context?): CastOptions { ... return Builder() ... .setRemoteToLocalEnabled(true) .build() } }
public class CastOptionsProvider implements OptionsProvider { @Override public CastOptions getCastOptions(Context context) { ... return new CastOptions.Builder() ... .setRemoteToLocalEnabled(true) .build() } }
繼續在本機上播放
如果應用程式支援遠端傳輸,則應註冊 SessionTransferCallback
接收通知,以便他們判斷是否應該
可以傳輸並繼續在本機播放。
CastContext#addSessionTransferCallback(SessionTransferCallback)
敬上
允許應用程式註冊 SessionTransferCallback
並監聽傳送者的 onTransferred
和 onTransferFailed
回呼
轉換成本機播放
取消註冊 SessionTransferCallback
後,
應用程式不會再收到 SessionTransferCallback
。
SessionTransferCallback
是現有 SessionManagerListener
的擴充功能
回呼,且會在觸發 onSessionEnded
後觸發。請注意,
遠端對本機回呼:
onTransferring
onSessionEnding
onSessionEnded
onTransferred
由於媒體通知元件可在應用程式處於背景並投放時開啟輸出切換器,因此應用程式需要根據是否支援背景播放,以不同方式處理轉移至本機的作業。有這種情況
失敗之傳輸要求,onTransferFailed
隨時會在錯誤發生時觸發
支援背景播放的應用程式
對於支援背景播放的應用程式 (通常是音訊應用程式),建議使用 Service
(例如 MediaBrowserService
)。服務應監聽 onTransferred
回呼,並在應用程式處於前景或背景時,在本機繼續播放。
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. } } }
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
中的必要資訊 (例如媒體中繼資料和播放位置)。當應用程式處於以下狀態時
本機播放功能應在背景執行
儲存的資訊
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. } } }
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 平台支援,無須採取其他行動 來變更。如果使用自訂 UI,應用程式應更新 UI,以反映應用程式正在投放至群組。
如要在增加串流裝置期間使用新的展開群組名稱,請按照下列步驟操作:
註冊
Cast.Listener
敬上
方法是使用
CastSession#addCastListener
。
然後呼叫
CastSession#getCastDevice()
敬上
會在 onDeviceNameChanged
回呼期間播放。
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) } }
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); } }
測試遠端對遠端功能
如要測試這項功能,請按照下列步驟操作:
- 使用傳統投放功能或 local-to-remote。
- 使用其中一個進入點開啟輸出切換器。
- 輕觸其他支援 Cast 的裝置後,音訊應用程式就會展開內容, 或其他裝置建立動態群組
- 再次輕觸支援 Cast 的裝置,該裝置就會從動態畫面中移除 群組。