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`](https://developer.android.com/jetpack/androidx/releases/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:

- [Local-to-remote](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#local-to-remote)
- [Remote-to-local](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#remote-to-local)
- [Remote-to-remote](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#remote-to-remote)

Download and use the [**CastVideos-android sample app**](https://developers.google.com/cast/docs/sample_apps.md.txt#castvideos-android)
for reference on how to implement Output Switcher in your app.

> [!IMPORTANT]
> **Key Term:** In this guide, the term *local* refers to any paired Bluetooth device or mobile device (such as phones and tablets) the app is being launched on, and the term *remote* refers to any Cast-enabled device.

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.

![](https://developers.google.com/static/cast/images/output_switcher/output-switcher-device-picker.png)
![](https://developers.google.com/static/cast/images/output_switcher/output-switcher-device-picker-bluetooth.png)

## 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`](https://developer.android.com/reference/android/media/session/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.

![](https://developers.google.com/static/cast/images/output_switcher/output-switcher-entry-point-notification.png)

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

![](https://developers.google.com/static/cast/images/output_switcher/output-switcher-entry-point-volume.png)

## Summary of steps

- [Ensure prerequisites are met](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#prerequisites)
- [Enable Output Switcher in AndroidManifest.xml](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#enable_output_switcher_in_androidmanifestxml)
- [Update SessionManagerListener for background casting](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#update_sessionmanagerlistener_for_background_casting)
- [Add support for Remote-to-Remote](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#remote-to-remote)
- [Set the setRemoteToLocalEnabled flag](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#set_the_setremotetolocalenabled_flag)
- [Continue playback locally](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#continue_playback_locally)

## 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](https://developer.android.com/guide/topics/media-apps/audio-app/building-an-audio-app) and
[video](https://developer.android.com/guide/topics/media-apps/video-app/building-a-video-app) 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`](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session),
setting the
[`MediaStyle`](https://developer.android.com/reference/androidx/media/app/NotificationCompat.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](https://developer.android.com/guide/topics/media-apps/audio-app/mediasession-callbacks) and
[video](https://developer.android.com/guide/topics/media-apps/video-app/mediasession-callbacks)
apps:
Kotlin

```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

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

![](https://developers.google.com/static/cast/images/output_switcher/media-notification-media-controls.png)

Additionally, to populate the notification with the information for your media,
you will need to add your media's
[metadata and playback state](https://developer.android.com/guide/topics/media-apps/working-with-a-media-session#maintain-state)
to the `MediaSession`.

To add metadata to the `MediaSession`, use
[`setMetaData()`](https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat#setMetadata(android.support.v4.media.MediaMetadataCompat))
and provide all of the relevant
[`MediaMetadata`](https://developer.android.com/reference/android/media/MediaMetadata) constants for
your media in the
[`MediaMetadataCompat.Builder()`](https://developer.android.com/reference/android/support/v4/media/MediaMetadataCompat.Builder).
Kotlin

```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

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

![](https://developers.google.com/static/cast/images/output_switcher/media-notification-metadata.png)

To add the playback state to the `MediaSession`, use
[`setPlaybackState()`](https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat#setPlaybackState(android.support.v4.media.session.PlaybackStateCompat))
and provide all of the relevant
[`PlaybackStateCompat`](https://developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat)
constants for your media in the
[`PlaybackStateCompat.Builder()`](https://developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat.Builder).
Kotlin

```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

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

![](https://developers.google.com/static/cast/images/output_switcher/media-notification-playback-state.png)

#### 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`](https://developer.android.com/reference/androidx/mediarouter/media/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`](https://developer.android.com/reference/androidx/mediarouter/media/MediaTransferReceiver)
is a broadcast receiver that enables media transfer among devices with system
UI. See the [MediaTransferReceiver
reference](https://developer.android.com/reference/androidx/mediarouter/media/MediaTransferReceiver)
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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManagerListener.md.txt),
using the
[`onSessionStarted()`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/media/uicontroller/UIMediaController.md.txt#onSessionStarted(com.google.android.gms.cast.framework.CastSession,%20java.lang.String))
and
[`onSessionEnded()`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/media/uicontroller/UIMediaController.md.txt#onSessionEnded(com.google.android.gms.cast.framework.CastSession,%20int))
callbacks, and handle the action when receiving the Cast
[`SessionManager`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManager.md.txt)
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

> [!CAUTION]
> **Caution:** Due to the [lifecycle of activities](https://developer.android.com/guide/components/fundamentals#Components), apps that support background playback (audio apps) are recommended to use a service rather than an activity for [`SessionManagerListener`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManagerListener.md.txt) callbacks to prevent necessary callbacks from getting unregistered when an activity is stopped.

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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManagerListener.md.txt),
in `onCreate()` or
[`onStart()`](https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity#onStart())
and unregister the listener in
[`onStop()`](https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity#onStop())
or
[`onDestroy()`](https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity#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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManager.md.txt)
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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/media/uicontroller/UIMediaController.md.txt#onSessionStarted(com.google.android.gms.cast.framework.CastSession,%20java.lang.String)))
when the app is in the background.

If the app uses the
[`MediaBrowserService`](https://developer.android.com/reference/android/service/media/MediaBrowserService),
it is recommended to register the [`SessionManagerListener`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManagerListener.md.txt)
there.
Kotlin

```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

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

> [!NOTE]
> **Note:** To ensure smooth transition from local to remote, it is recommended to stop playback on the local device during the [`onSessionStarted`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/media/uicontroller/UIMediaController.md.txt#onSessionStarted(com.google.android.gms.cast.framework.CastSession,%20java.lang.String)) callback, after the Cast SDK has successfully connected to the receiver. This ensures that the local playback is stopped only after playback has resumed on the remote device and have any timing issues due to latency.

## 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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.Builder.md.txt#setRemoteToLocalEnabled(boolean))
flag to `true` on the [`CastOptions`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.md.txt).

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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt#onTransferred(int,%20com.google.android.gms.cast.SessionState))
callback of the [`SessionTransferCallback`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt)
to check the [`SessionState`](https://developers.google.com/android/reference/com/google/android/gms/cast/SessionState.md.txt).

### Set the setRemoteToLocalEnabled flag

The [`CastOptions.Builder`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.Builder.md.txt)
provides a [`setRemoteToLocalEnabled`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.Builder.md.txt#setRemoteToLocalEnabled(boolean)) 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

```kotlin
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
```
Java

```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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt)
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)`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastContext.md.txt#addSessionTransferCallback(com.google.android.gms.cast.framework.SessionTransferCallback))
allows an app to register its [`SessionTransferCallback`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt)
and listen for [`onTransferred`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt#onTransferred(int,%20com.google.android.gms.cast.SessionState)) and [`onTransferFailed`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt#onTransferFailed(int,%20int)) callbacks when a sender is
transferred to local playback.

After the app unregisters its [`SessionTransferCallback`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt),
the app will no longer receive [`SessionTransferCallback`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt)
s.

The [`SessionTransferCallback`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt)
is an extension of the existing [`SessionManagerListener`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionManagerListener.md.txt)
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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt#onTransferFailed(int,%20int))
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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt#onTransferred(int,%20com.google.android.gms.cast.SessionState))
callback and resume playback locally both when the app is in the foreground or
background.
Kotlin

```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

```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`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/SessionTransferCallback.md.txt#onTransferred(int,%20com.google.android.gms.cast.SessionState))
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`](https://developers.google.com/android/reference/com/google/android/gms/cast/SessionState.md.txt)
(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

```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

```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](https://cast.google.com/publish/#/overview)

![](https://developers.google.com/static/cast/images/output_switcher/google-cast-for-audio.png)

### Stream Expansion with speakers

> [!IMPORTANT]
> **Important:** Although remote-to-remote expansion with speakers is supported automatically by the Cast platform, apps might need to update their UIs with the speaker group names. Apps should follow the [Testing remote-to-remote](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#testing_remote-to-remote) steps to ensure their app will work properly.

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.

![](https://developers.google.com/static/cast/images/output_switcher/output-switcher-device-picker-expandable.png)
![](https://developers.google.com/static/cast/images/output_switcher/output-switcher-device-picker-expanded.png)

To get the new expanded group name during a stream expansion,
register a
[`Cast.Listener`](https://developers.google.com/android/reference/com/google/android/gms/cast/Cast.Listener.md.txt)
using the
[`CastSession#addCastListener`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastSession.md.txt#public-void-addcastlistener-cast.listener-listener).
Then call
[`CastSession#getCastDevice()`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastSession.md.txt#getCastDevice())
during the [`onDeviceNameChanged`](https://developers.google.com/android/reference/com/google/android/gms/cast/Cast.Listener.md.txt#onDeviceNameChanged()) callback.
Kotlin

```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

```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](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#local-to-remote).
2. Open the Output Switcher using one of the [entry points](https://developers.google.com/cast/docs/android_sender/output_switcher.md.txt#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.