Output Switcher

Output Switcher is a feature of the Cast SDK that enables seamless transferring between local and remote playback of content starting with Android 13. The goal is to help sender apps easily and quickly control where the content is playing. Output Switcher uses the MediaRouter library to switch the content playback among the phone speaker, paired Bluetooth devices, and remote Cast-enabled devices. Use cases can be broken down into the following scenarios:

Download and use the CastVideos-android sample app for reference on how to implement Output Switcher in your app.

Output Switcher should be enabled to support local-to-remote, remote-to-local and remote-to-remote using the steps covered in this guide. There are no additional steps needed to support the transfer between the local device speakers and paired Bluetooth devices.

Output Switcher UI

The Output Switcher displays the local and remote devices that are available as well as the current device states, including if the device is selected, is connecting, the current volume level. If there are other devices in addition to the current device, clicking other device lets you transfer the media playback to the selected device.

Known issues

  • Media Sessions created for local playback will be dismissed and recreated when switching to the Cast SDK notification.

Entry points

Media notification

If an app posts a media notification with MediaSession for local playback (playing locally), the top-right corner of the media notification displays a notification chip with the device name (such as phone speaker) that the content is currently being played on. Tapping on the notification chip opens the Output Switcher dialog system UI.

Volume settings

The Output Switcher dialog system UI can also be triggered by clicking the physical volume buttons on the device, tapping the settings icon at the bottom, and tapping the "Play <App Name> on <Cast Device>" text.

Summary of steps

Prerequisites

  1. Migrate your existing Android app to AndroidX.
  2. Update your app's build.gradle to use the minimum required version of the Android Sender SDK for the Output Switcher:
     dependencies 
      
     { 
      
     ... 
      
     implementation 
      
     ' 
     com 
     . 
     google 
     . 
     android 
     . 
     gms 
     : 
     play 
     - 
     services 
     - 
     cast 
     - 
     framework 
     : 
     21.2.0 
     ' 
      
     ... 
     } 
    
  3. App supports media notifications.
  4. Device running Android 13.

Set up Media Notifications

To use the Output Switcher, audio and video apps are required to create a media notification to display the playback status and controls for their media for local playback. This requires creating a MediaSession , setting the MediaStyle with the MediaSession 's token, and setting the media controls on the notification.

If you are not currently using a MediaStyle and MediaSession , the snippet below shows how to set them up and guides are available for setting up the media session callbacks for audio and video apps:

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 
 (); 
 } 

Additionally, to populate the notification with the information for your media, you will need to add your media's metadata and playback state to the MediaSession .

To add metadata to the MediaSession , use setMetaData() and provide all of the relevant MediaMetadata constants for your media in the 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 
 () 
  
 ); 
 } 

To add the playback state to the MediaSession , use setPlaybackState() and provide all of the relevant PlaybackStateCompat constants for your media in the 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 
 () 
  
 ); 
 } 

Video app notification behavior

Video apps or audio apps that don't support local playback in the background should have specific behavior for media notifications to avoid issues with sending media commands in situations that playback is not supported:

  • Post the media notification when playing media locally and the app is in the foreground.
  • Pause local playback and dismiss the notification when the app is in the background.
  • When the app moves back to the foreground, local playback should resume and the notification should be reposted.

Enable Output Switcher in AndroidManifest.xml

To enable the Output Switcher, the MediaTransferReceiver needs to be added to the app's AndroidManifest.xml . If it isn't, the feature won't be enabled and the remote-to-local feature flag will also be invalid.

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

The MediaTransferReceiver is a broadcast receiver that enables media transfer among devices with system UI. See the MediaTransferReceiver reference for more information.

Local-to-remote

When the user switches playback from local to remote, the Cast SDK will start the Cast session automatically. However, apps need to handle switching from local to remote, for example stop the local playback and load the media on the Cast device. Apps should listen to the Cast SessionManagerListener , using the onSessionStarted() and onSessionEnded() callbacks, and handle the action when receiving the Cast SessionManager callbacks. Apps should ensure that these callbacks are still alive when the Output Switcher dialog is opened and the app is not in the foreground.

Update SessionManagerListener for background casting

The legacy Cast experience already supports local-to-remote when the app is in foreground. A typical Cast experience starts when users click the Cast icon in the app and pick a device to stream media. In this case, the app needs to register to the SessionManagerListener , in onCreate() or onStart() and unregister the listener in onStop() or onDestroy() of the app's activity.

With the new experience of casting using the Output Switcher, apps can start casting when they are in the background. This is particularly useful for audio apps that post notifications when playing in the background. Apps can register the SessionManager listeners in the onCreate() of the service and unregister in the onDestroy() of the service. Apps should always receive the local-to-remote callbacks (such as onSessionStarted ) when the app is in the background.

If the app uses the MediaBrowserService , it is recommended to register the SessionManagerListener there.

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 
 ); 
  
 } 
  
 } 
 } 

With this update, local-to-remote acts the same as conventional casting when the app is in the background and extra work is not required for switching from Bluetooth devices to Cast devices.

Remote-to-local

The Output Switcher provides the ability to transfer from remote playback to the phone speaker or local Bluetooth device. This can be enabled by setting the setRemoteToLocalEnabled flag to true on the CastOptions .

For cases where the current sender device joins an existing session with multiple senders and the app needs to check if the current media is allowed to be transferred locally, apps should use the onTransferred callback of the SessionTransferCallback to check the SessionState .

Set the setRemoteToLocalEnabled flag

The CastOptions.Builder provides a setRemoteToLocalEnabled to show or hide the phone speaker and local Bluetooth devices as transfer-to targets in the Output Switcher dialog when there is an active Cast session.

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 
 () 
  
 } 
 } 

Continue playback locally

Apps that support remote-to-local should register the SessionTransferCallback to get notified when the event occurs so they can check if media should be allowed to transfer and continue playback locally.

CastContext#addSessionTransferCallback(SessionTransferCallback) allows an app to register its SessionTransferCallback and listen for onTransferred and onTransferFailed callbacks when a sender is transferred to local playback.

After the app unregisters its SessionTransferCallback , the app will no longer receive SessionTransferCallback s.

The SessionTransferCallback is an extension of the existing SessionManagerListener callbacks and is triggered after onSessionEnded is triggered. The order of remote-to-local callbacks is:

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

Since the Output Switcher can be opened by the media notification chip when the app is in the background and casting, apps need to handle the transfer to local differently depending on if they support background playback or not. In the case of a failed transfer, onTransferFailed will fire at any time the error occurs.

Apps that support background playback

For apps that support playback in the background (typically audio apps), it is recommended to use a Service (for example MediaBrowserService ). Services should listen to the onTransferred callback and resume playback locally both when the app is in the foreground or background.

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. 
  
 } 
  
 } 
 } 

Apps that don't support background playback

For apps that don't support background playback (typically video apps), it is recommended to listen to the onTransferred callback and resume playback locally if the app is in the foreground.

If the app is in the background, it should pause playback and should store the necessary information from SessionState (for example, media metadata and playback position). When the app is foregrounded from the background, the local playback should continue with the stored information.

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. 
  
 } 
  
 } 
 } 

Remote-to-remote

The Output Switcher supports the ability to expand to multiple Cast-enabled speaker devices for audio apps using Stream Expansion.

Audio apps are apps that support Google Cast for Audio in the Receiver App settings in the Google Cast SDK Developer Console

Stream Expansion with speakers

Audio apps that use the Output Switcher have the ability to expand the audio to multiple Cast-enabled speaker devices during a Cast session using Stream Expansion.

This feature is supported by the Cast platform and doesn't require any further changes if the app is using the default UI. If a custom UI is used, the app should update the UI to reflect that the app is casting to a group.

To get the new expanded group name during a stream expansion, register a Cast.Listener using the CastSession#addCastListener . Then call CastSession#getCastDevice() during the onDeviceNameChanged callback.

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 
 ); 
  
 } 
 } 

Testing remote-to-remote

To test the feature:

  1. Cast your content to a Cast-enabled device using conventional casting or with local-to-remote .
  2. Open the Output Switcher using one of the entry points .
  3. Tap on another Cast-enabled device, audio apps will expand the content to the additional device, creating a dynamic group.
  4. Tap on the Cast-enabled device again, it will be removed from the dynamic group.
Design a Mobile Site
View Site in Mobile | Classic
Share by: