Prerequisites
- Jetpack Media3 version must be
>= 1.9.0. Cast isn't available in lower versions.
Glossary
CastPlayer : Media3 Player that controls playback on both local and remote Cast devices.
RemoteCastPlayer : Media3 Player that communicates with a Cast receiver, only used for remote playback.
- Google Cast SDK: Legacy casting SDK in maintenance mode, superseded by Jetpack Media3.
OptionsProvider : Interface providing configuration options to initialize GMS CastContext.
Common guidelines
- Legacy Google Cast SDK is in maintenance mode.
- For new Cast setups:
- You must use Jetpack Media3 Cast.
- You mustn't use legacy Cast SDK unless explicitly requested.
Step 1: Set up dependencies
To complete this step, you MUST ensure the following:
In the app-level build file, declare the media3-cast dependency version
1.9.0 or higher.
implementation("androidx.media3:media3-cast:1.11.0")
Ensure required Media3 dependencies are present:
androidx.media3:media3-exoplayer
androidx.media3:media3-session
androidx.media3:media3-ui-compose
If the application uses legacy Views, add media3-ui.
Enforce the same versions across all Media3 dependencies.
Use configurations in "Add build dependencies" section of Getting started
with CastPlayer as the source of
truth.
For apps without an existing Cast integration:
- Verify legacy Cast SDK (
libs.play.services.cast.framework) is absent.
If Migrating from Legacy Cast SDK:
- Add Media3 Cast dependencies first.
- Keep existing legacy dependencies untouched at this stage to prevent compilation errors.
Step 2: Update the manifest
To complete this step, you MUST ensure the following:
- Inside the manifest's
<application> tag, declare the Cast options provider.
- Use
DefaultCastOptionsProvider by default. See the "OptionsProvider" section in Getting started with
CastPlayer.
- Declare a custom
OptionsProvider only if explicitly requested. See Customize CastOptions.
- Ensure
INTERNET permission is present. Don't add any unnecessary permissions.
- If Migrating from Legacy Cast SDK:
- Don't delete existing custom options provider files or manifest entries.
Step 3: Implement the player and service
Architecture baseline
Before integrating Media3 Cast, an existing app follows one of two setups:
- Local-only playback: Uses Media3
ExoPlayer only to support local playback.
- Legacy Cast setup: Uses
ExoPlayer for local playback, alongside a Player wrapper over the legacy RemoteMediaClient for remote playback. The UI interfaces with a MediaSession interacting with a ForwardingPlayer, which finally routes controls to either local or remote playback.
To complete this step, you MUST ensure the following:
- Inside the application's
MediaSessionService (or MediaLibraryService) onCreate() method, initialize ExoPlayer and CastPlayer.
- Use
CastPlayer by default unless RemoteCastPlayer is explicitly requested. See the "Build a CastPlayer" section in Getting started with
CastPlayer.
- For
CastPlayer, pass the instance directly to MediaSession.Builder.
- Replace all legacy forwarding player wrappers.
- Don't delete legacy class files yet to prevent compilation errors during migration.
Advanced: RemoteCastPlayer
Use RemoteCastPlayer only if explicitly requested by user.
Initialize MediaSession with localPlayer and set a SessionAvailabilityListener on RemoteCastPlayer to transfer playback state on Cast session availability changes:
class PlaybackService : MediaSessionService() {
private var mediaSession: MediaSession? = null
private lateinit var localPlayer: ExoPlayer
private lateinit var remotePlayer: RemoteCastPlayer
override fun onCreate() {
super.onCreate()
localPlayer = ExoPlayer.Builder(this).build()
remotePlayer = RemoteCastPlayer.Builder(this).build()
mediaSession = MediaSession.Builder(this, localPlayer).build()
remotePlayer.setSessionAvailabilityListener(
object : SessionAvailabilityListener {
override fun onCastSessionAvailable() {
transferPlaybackState(localPlayer, remotePlayer)
}
override fun onCastSessionUnavailable() {
transferPlaybackState(remotePlayer, localPlayer)
}
}
)
}
private fun transferPlaybackState(previousPlayer: Player, newPlayer: Player) {
if (previousPlayer.mediaItemCount > 0) {
val transferStateBuilder = PlayerTransferState.builderFromPlayer(previousPlayer)
if (previousPlayer.playbackState == Player.STATE_ENDED ||
previousPlayer.currentPosition == C.TIME_END_OF_SOURCE) {
transferStateBuilder.setCurrentMediaItemIndex(0)
transferStateBuilder.setCurrentPosition(0)
}
transferStateBuilder.build().setToPlayer(newPlayer)
}
previousPlayer.stop()
previousPlayer.clearMediaItems()
newPlayer.prepare()
mediaSession?.setPlayer(newPlayer)
}
}
Step 4: Set up the UI
Compose-based UI
To complete this step, you MUST ensure the following:
See the "Add a MediaRouteButton Composable to the Player" section in Getting started with CastPlayer for Compose integration guidelines.
Use the MediaRouteButton composable from androidx.media3.cast package.
Don't use AndroidView in the Compose UI hierarchy.
Place MediaRouteButton in an area next to playback controls. Don't hide it behind system UI.
Don't use PlayerSurface for custom player UI. Use the Material3 Player
composable.
Force recomposition on playback location shifts to ensure UI sync. Use key
constraints on DeviceInfo changes:
@OptIn(UnstableApi::class)
@Composable
fun MainScreen() {
val player = rememberMediaController()
val deviceInfo = rememberDeviceInfo(player)
player?.let { activePlayer -> key(deviceInfo) { PlayerScreen(player = activePlayer) } }
}
@Composable
private fun rememberMediaController(): Player? {
// Logic to connect MediaController to MediaSession and release it
}
@Composable
private fun rememberDeviceInfo(player: Player?): DeviceInfo? {
var deviceInfo by remember(player) { mutableStateOf(player?.deviceInfo) }
DisposableEffect(player) {
val activePlayer = player ?: return@DisposableEffect onDispose {}
deviceInfo = activePlayer.deviceInfo
val listener = object : Player.Listener {
override fun onDeviceInfoChanged(info: DeviceInfo) {
deviceInfo = info
}
}
activePlayer.addListener(listener)
onDispose { activePlayer.removeListener(listener) }
}
return deviceInfo
}
View-based UI
To complete this step, you MUST ensure the following:
For View-based UI setups, see the "Add UI elements" section in Getting
started with CastPlayer.
Casting Activities must extend AppCompatActivity or FragmentActivity and use a Theme.AppCompat descendant.
Ensure the AppCompat theme has a visible ActionBar if adding MediaRouteButton to the options menu.
Replace all instances and imports of CastButtonFactory with MediaRouteButtonFactory.
Rebind PlayerView.player references upon onDeviceInfoChanged events to
prevent black screens or UI freezes:
private val playerListener: Player.Listener =
object : Player.Listener {
override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {
// Resetting to null bypasses PlayerView.setPlayer()'s instance equality check
// (this.player == player), forcing it to re-bind the video surface to the controller.
playerView.player = null
playerView.player = controller
}
}
Migration to Compose:
- Don't use
AndroidView to wrap the legacy PlayerView.
- Implement Material3
Player composable and MediaRouteButton composable as per Getting started with CastPlayer.
- Remove legacy XML layout declarations, menu files, and View component references.
Step 5: Clean up legacy Cast SDK code
[!WARNING]
Warning: Don't perform cleanup directly. Remove legacy files and dependencies only when explicitly requested by the user.
To complete this step, you MUST ensure the following:
- Remove legacy GMS Cast SDK (
libs.play.services.cast.framework) and MediaRouter (libs.androidx.mediarouter) dependencies.
- Delete custom
OptionsProvider classes and manifest entries if DefaultCastOptionsProvider is adopted.
- Remove legacy
MediaTransferReceiver manifest declarations if present.
- Remove all references to legacy Cast SDK components such as legacy helper wrappers, forwarding players, and
RemoteMediaClient interfaces.
- Delete legacy View XML layouts, menu files, and references to
PlayerView if the migration to Compose is complete.
1---2name: media3-cast-integration3description: Implements Google Cast support in Android apps using Jetpack Media3. Handles adding build dependencies, updating manifest, configuring OptionsProvider, and managing CastPlayer or RemoteCastPlayer for playback in both Compose and View-based UIs. Use when adding Cast functionality or migrating from legacy Cast SDK to Media3 Cast.4license: Complete terms in LICENSE.txt5---67## Prerequisites89- Jetpack Media3 version must be `>= 1.9.0`. Cast isn't available in lower versions.1011## Glossary1213- **`CastPlayer`** : Media3 `Player` that controls playback on both local and remote Cast devices.14- **`RemoteCastPlayer`** : Media3 `Player` that communicates with a Cast receiver, only used for remote playback.15- **Google Cast SDK**: Legacy casting SDK in maintenance mode, superseded by Jetpack Media3.16- **`OptionsProvider`** : Interface providing configuration options to initialize GMS `CastContext`.1718## Common guidelines1920- Legacy [Google Cast SDK](https://developers.google.com/cast) is in maintenance mode.21- For new Cast setups:22 - You must use [Jetpack Media3 Cast](references/android/media/media3/cast/index.md).23 - You mustn't use legacy Cast SDK unless explicitly requested.2425## Step 1: Set up dependencies2627To complete this step, you **MUST** ensure the following:2829- In the app-level build file, declare the `media3-cast` dependency version30 1.9.0 or higher.3132 implementation("androidx.media3:media3-cast:1.11.0")3334- Ensure required Media3 dependencies are present:3536 - `androidx.media3:media3-exoplayer`37 - `androidx.media3:media3-session`38 - `androidx.media3:media3-ui-compose`39- If the application uses legacy Views, add `media3-ui`.4041- Enforce the same versions across all Media3 dependencies.4243- Use configurations in "Add build dependencies" section of [Getting started44 with CastPlayer](references/android/media/media3/cast/create-castplayer.md) as the source of45 truth.4647- **For apps without an existing Cast integration:**4849 - Verify legacy Cast SDK (`libs.play.services.cast.framework`) is absent.50- **If Migrating from Legacy Cast SDK:**5152 - Add Media3 Cast dependencies first.53 - Keep existing legacy dependencies untouched at this stage to prevent compilation errors.5455## Step 2: Update the manifest5657To complete this step, you **MUST** ensure the following:5859- Inside the manifest's `<application>` tag, declare the Cast options provider.60- Use `DefaultCastOptionsProvider` by default. See the "OptionsProvider" section in [Getting started with61 CastPlayer](references/android/media/media3/cast/create-castplayer.md).62- Declare a custom `OptionsProvider` only if explicitly requested. See [Customize CastOptions](references/android/media/media3/cast/customize-castoptions.md).63- Ensure `INTERNET` permission is present. Don't add any unnecessary permissions.64- **If Migrating from Legacy Cast SDK:**65 - Don't delete existing custom options provider files or manifest entries.6667## Step 3: Implement the player and service6869### Architecture baseline7071Before integrating Media3 Cast, an existing app follows one of two setups:7273- **Local-only playback:** Uses Media3 `ExoPlayer` only to support local playback.74- **Legacy Cast setup:** Uses `ExoPlayer` for local playback, alongside a `Player` wrapper over the legacy `RemoteMediaClient` for remote playback. The UI interfaces with a `MediaSession` interacting with a `ForwardingPlayer`, which finally routes controls to either local or remote playback.7576To complete this step, you **MUST** ensure the following:7778- Inside the application's `MediaSessionService` (or `MediaLibraryService`) `onCreate()` method, initialize `ExoPlayer` and `CastPlayer`.79- Use `CastPlayer` by default unless `RemoteCastPlayer` is explicitly requested. See the "Build a CastPlayer" section in [Getting started with80 CastPlayer](references/android/media/media3/cast/create-castplayer.md).81- For `CastPlayer`, pass the instance directly to `MediaSession.Builder`.82- Replace all legacy forwarding player wrappers.83- Don't delete legacy class files yet to prevent compilation errors during migration.8485### Advanced: `RemoteCastPlayer`8687- Use `RemoteCastPlayer` only if explicitly requested by user.88- Initialize `MediaSession` with `localPlayer` and set a `SessionAvailabilityListener` on `RemoteCastPlayer` to transfer playback state on Cast session availability changes:8990 class PlaybackService : MediaSessionService() {91 private var mediaSession: MediaSession? = null92 private lateinit var localPlayer: ExoPlayer93 private lateinit var remotePlayer: RemoteCastPlayer9495 override fun onCreate() {96 super.onCreate()9798 localPlayer = ExoPlayer.Builder(this).build()99 remotePlayer = RemoteCastPlayer.Builder(this).build()100 mediaSession = MediaSession.Builder(this, localPlayer).build()101102 remotePlayer.setSessionAvailabilityListener(103 object : SessionAvailabilityListener {104 override fun onCastSessionAvailable() {105 transferPlaybackState(localPlayer, remotePlayer)106 }107108 override fun onCastSessionUnavailable() {109 transferPlaybackState(remotePlayer, localPlayer)110 }111 }112 )113 }114115 private fun transferPlaybackState(previousPlayer: Player, newPlayer: Player) {116 if (previousPlayer.mediaItemCount > 0) {117 val transferStateBuilder = PlayerTransferState.builderFromPlayer(previousPlayer)118 if (previousPlayer.playbackState == Player.STATE_ENDED ||119 previousPlayer.currentPosition == C.TIME_END_OF_SOURCE) {120 transferStateBuilder.setCurrentMediaItemIndex(0)121 transferStateBuilder.setCurrentPosition(0)122 }123 transferStateBuilder.build().setToPlayer(newPlayer)124 }125126 previousPlayer.stop()127 previousPlayer.clearMediaItems()128 newPlayer.prepare()129 mediaSession?.setPlayer(newPlayer)130 }131 }132133## Step 4: Set up the UI134135### Compose-based UI136137To complete this step, you **MUST** ensure the following:138139- See the "Add a MediaRouteButton Composable to the Player" section in [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md) for Compose integration guidelines.140- Use the [`MediaRouteButton` composable](https://developer.android.com/reference/kotlin/androidx/media3/cast/MediaRouteButton.composable) from `androidx.media3.cast` package.141- Don't use `AndroidView` in the Compose UI hierarchy.142- Place `MediaRouteButton` in an area next to playback controls. Don't hide it behind system UI.143- Don't use `PlayerSurface` for custom player UI. Use the Material3 [`Player`144 composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable).145- Force recomposition on playback location shifts to ensure UI sync. Use key146 constraints on `DeviceInfo` changes:147148 @OptIn(UnstableApi::class)149 @Composable150 fun MainScreen() {151 val player = rememberMediaController()152 val deviceInfo = rememberDeviceInfo(player)153 player?.let { activePlayer -> key(deviceInfo) { PlayerScreen(player = activePlayer) } }154 }155156 @Composable157 private fun rememberMediaController(): Player? {158 // Logic to connect MediaController to MediaSession and release it159 }160161 @Composable162 private fun rememberDeviceInfo(player: Player?): DeviceInfo? {163 var deviceInfo by remember(player) { mutableStateOf(player?.deviceInfo) }164 DisposableEffect(player) {165 val activePlayer = player ?: return@DisposableEffect onDispose {}166 deviceInfo = activePlayer.deviceInfo167 val listener = object : Player.Listener {168 override fun onDeviceInfoChanged(info: DeviceInfo) {169 deviceInfo = info170 }171 }172 activePlayer.addListener(listener)173 onDispose { activePlayer.removeListener(listener) }174 }175 return deviceInfo176 }177178### View-based UI179180To complete this step, you **MUST** ensure the following:181182- For View-based UI setups, see the "Add UI elements" section in [Getting183 started with CastPlayer](references/android/media/media3/cast/create-castplayer.md).184- Casting Activities must extend `AppCompatActivity` or `FragmentActivity` and use a `Theme.AppCompat` descendant.185- Ensure the `AppCompat` theme has a visible `ActionBar` if adding `MediaRouteButton` to the options menu.186- Replace all instances and imports of `CastButtonFactory` with `MediaRouteButtonFactory`.187- Rebind `PlayerView.player` references upon `onDeviceInfoChanged` events to188 prevent black screens or UI freezes:189190 private val playerListener: Player.Listener =191 object : Player.Listener {192 override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {193 // Resetting to null bypasses PlayerView.setPlayer()'s instance equality check194 // (this.player == player), forcing it to re-bind the video surface to the controller.195 playerView.player = null196 playerView.player = controller197 }198 }199200- **Migration to Compose:**201202 - Don't use `AndroidView` to wrap the legacy `PlayerView`.203 - Implement Material3 [`Player` composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable) and [`MediaRouteButton` composable](https://developer.android.com/reference/kotlin/androidx/media3/cast/MediaRouteButton.composable) as per [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md).204 - Remove legacy XML layout declarations, menu files, and View component references.205206## Step 5: Clean up legacy Cast SDK code207208> [!WARNING]209> **Warning:** Don't perform cleanup directly. Remove legacy files and dependencies only when explicitly requested by the user.210211To complete this step, you **MUST** ensure the following:212213- Remove legacy GMS Cast SDK (`libs.play.services.cast.framework`) and MediaRouter (`libs.androidx.mediarouter`) dependencies.214- Delete custom `OptionsProvider` classes and manifest entries if `DefaultCastOptionsProvider` is adopted.215- Remove legacy `MediaTransferReceiver` manifest declarations if present.216- Remove all references to legacy Cast SDK components such as legacy helper wrappers, forwarding players, and `RemoteMediaClient` interfaces.217- Delete legacy View XML layouts, menu files, and references to `PlayerView` if the migration to Compose is complete.