Generated banner for Media3 Cast Integration
Generated logo for Media3 Cast Integration

Media3 Cast Integration

Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)

  • Skill
  • Tool integration
  • Software development
  • Apache-2.0
  • No issues found

From the author

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.
— the description by Google LLC

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a comprehensive guide for implementing Google Cast support in Android apps using the Jetpack Media3 library. It covers the necessary build configuration, manifest updates, and player initialization required to enable seamless casting. The guide also details how to manage playback state between local and remote devices, supports both Compose and View-based UI architectures, and provides a structured approach for migrating from the legacy Google Cast SDK to the modern Media3 implementation.

What it does

  • Configures build dependencies for Jetpack Media3 Cast integration.
  • Updates Android manifest files with required Cast options providers.
  • Initializes CastPlayer and RemoteCastPlayer for unified playback control.
  • Provides patterns for managing playback state transitions between local and remote devices.
  • Offers guidance for implementing Cast UI components in both Jetpack Compose and legacy View systems.
  • Outlines a safe migration path from the legacy Google Cast SDK to Media3.

When to use it

  • When adding new Google Cast functionality to an Android application.
  • When migrating an existing application from the legacy Google Cast SDK to Jetpack Media3.
  • When building media playback services that require switching between local and remote Cast-enabled devices.

Advantages

  • Uses modern Jetpack Media3 APIs for consistent media playback management.
  • Simplifies the transition between local and remote playback states.
  • Provides clear architectural patterns for both Compose and View-based UIs.
  • Reduces complexity by replacing legacy forwarding player wrappers.

Usage examples

“How do I add Google Cast support to my Android app using Media3?”
The agent provides the necessary Gradle dependencies, manifest configuration steps, and instructions for initializing the CastPlayer within a MediaSessionService.
“How can I migrate my existing Cast implementation to Media3?”
The agent outlines a step-by-step migration process, including adding Media3 dependencies, updating the manifest, and eventually removing legacy Cast SDK components.
“How do I implement a MediaRouteButton in Jetpack Compose?”
The agent provides the implementation details for the MediaRouteButton composable and explains how to sync UI state with the active player's device information.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/android/media3-cast-integration/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip media3-cast-integration.zip -d .agents/skills

Documentation by Google LLC

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

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.1")
    
  • 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:

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.

Files

references/android/media/media3/cast/create-castplayer.md

The CastPlayer is a Jetpack Media3 Player implementation that supports both local playback and casting to a remote Cast-enabled device. CastPlayer simplifies adding cast functionality to your app and provides rich features to seamlessly switch between local and remote playback. This guide shows you how to integrate CastPlayer into your media app.

To integrate Cast with other platforms, see the Cast SDK.

Get a Cast-enabled device

To test CastPlayer, you need a Cast-enabled device. Choices include Android TV, Chromecast, smart speakers, and smart displays. Verify that your device is set up and connected to the same Wi-Fi network as your development mobile for discovery.

Add build dependencies

To start using CastPlayer, add the AndroidX Media3 and CastPlayer dependencies to the build.gradle file of your app module.

Kotlin
implementation("androidx.media3:media3-exoplayer:1.11.1")
implementation("androidx.media3:media3-ui:1.11.1")
implementation("androidx.media3:media3-session:1.11.1")
implementation("androidx.media3:media3-cast:1.11.1")
Groovy
implementation "androidx.media3:media3-exoplayer:1.11.1"
implementation "androidx.media3:media3-ui:1.11.1"
implementation "androidx.media3:media3-session:1.11.1"
implementation "androidx.media3:media3-cast:1.11.1"

Configure your CastPlayer

To configure the CastPlayer, update your AndroidManifest.xml file with an options provider.

Options provider

The CastPlayer requires an options provider to configure its behavior. For a basic setup, you can use the DefaultCastOptionsProvider by adding it to your AndroidManifest.xml file. This uses default settings, including the default receiver application.

<application>
  ...
  <meta-data
    android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
    android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
  ...
</application>

To customize the configuration, implement your own custom OptionsProvider. See the CastOptions guide to learn how.

Add a receiver for media transfers

Adding a MediaTransferReceiver to your manifest enables the System UI to discover Cast-enabled devices on the network and reroute media without opening the app activity. For example, a user can change the device playing your app's media from the media notification.

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

Build a CastPlayer

For remote playback with Cast, your app should be able to manage playback even when the user isn't interacting with an Activity from your app, such as through the system media notification. For this reason, you should create your ExoPlayer (for local playback) and CastPlayer (for remote playback) instances in a service, such as MediaSessionService or MediaLibraryService. First, create your ExoPlayer instance and then when building your CastPlayer instance, set ExoPlayer as the local player instance. You can then switch media playback between your mobile and the Cast-enabled device from the media notification or the lock screen notification. Media3 uses the Output Switcher feature to handle player transfers when the output route changes from local to remote or from remote to local. Image: Screenshot showing the Output Switcher UI in notifications. Figure 1: (a) Device chip on Media notification (b) Cast-enabled devices shown on tapping the device chip (c) Device chip on Lock screen notification

Kotlin
override fun onCreate() {
  super.onCreate()

  val exoPlayer = ExoPlayer.Builder(context).build()
  val castPlayer = CastPlayer.Builder(context).setLocalPlayer(exoPlayer).build()

  mediaSession = MediaSession.Builder(context, castPlayer).build()
}
Java
@Override
public void onCreate() {
  super.onCreate();

  ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
  CastPlayer castPlayer = new CastPlayer.Builder(context).setLocalPlayer(exoPlayer).build();

  mediaSession =
      new MediaSession.Builder(/* context= */ context, /* player= */ castPlayer).build();
}

<br />

[!IMPORTANT] Important: The preceding code snippet shows the onCreate method of a MediaSessionService. In an Activity, players should be created in onStart or onResume.

Add UI elements

Add a MediaRouteButton to your app's UI. Tapping the MediaRouteButton opens a dialog displaying a list of available Cast-enabled devices on the network. When the user selects a device, the media playback is transferred from the mobile to the selected receiver device. This section shows you how to add the button and listen for events to update your UI when playback switches between local and remote devices.

Set the MediaRouteButton

There are four ways to add the MediaRouteButton to your activity's UI. The best choice depends on your app's design and requirements.

  • Compose UI: Add a button composable.

  • Views UI :

    • Add the button to the app bar menu.

    • Add the button inside PlayerView.

    • Add the button as a standard View.

Image: Screenshot showing the MediaRouteButton in the UI. Figure 2: (a) MediaRouteButton in menu bar, (b) as a View, (c) in PlayerView, and (d) Dialog of Cast-enabled devices.

[!IMPORTANT] Important: To use the MediaRouteButton in Views UI, the containing activity must be a subclass of FragmentActivity.

[!TIP] Tip: These screenshots are from the demo-session apps in Media3. You can checkout the app to see an example of CastPlayer implementation.

Add a Composable MediaRouteButton to the Player

You can add the MediaRouteButton Composable to your player's UI. For more information, see the Compose guide.

@Composable
fun PlayerComposeView(player: Player, modifier: Modifier = Modifier) {
  var controlsVisible by remember { mutableStateOf(false) }

  Box(
    modifier = modifier.clickable { controlsVisible = true },
    contentAlignment = Alignment.Center,
  ) {
    PlayerSurface(player = player, modifier = modifier)
    AnimatedVisibility(visible = controlsVisible, enter = fadeIn(), exit = fadeOut()) {
      Box(modifier = Modifier.fillMaxSize()) {
        MediaRouteButton(modifier = Modifier.align(Alignment.TopEnd))
        PrimaryControls(player = player, modifier = Modifier.align(Alignment.Center))
      }
    }
  }
}

@Composable
fun PrimaryControls(player: Player, modifier: Modifier = Modifier) {
  // ...
}

<br />

Add the MediaRouteButton to the PlayerView

You can add the MediaRouteButton directly within the PlayerView's UI controls. After setting the MediaController as the player for your PlayerView, provide a MediaRouteButtonViewProvider to display the Cast button on the Player.

Kotlin
override fun onStart() {
  super.onStart()

  playerView.player = mediaController
  playerView.setMediaRouteButtonViewProvider(MediaRouteButtonViewProvider())
}
Java
@Override
public void onStart() {
  super.onStart();

  playerView.setPlayer(mediaController);
  playerView.setMediaRouteButtonViewProvider(new MediaRouteButtonViewProvider());
}

<br />

Add the MediaRouteButton to the app bar menu

To set up a MediaRouteButton in the app bar menu, create an XML menu and override onCreateOptionsMenu in your Activity.

<menu xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto">
  <item android:id="@+id/media_route_menu_item"
    android:title="@string/media_route_menu_title"
    app:showAsAction="always"
    app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"/>
</menu>
Kotlin
override fun onCreateOptionsMenu(menu: Menu): Boolean {
  // ...
  menuInflater.inflate(R.menu.sample_media_route_button_menu, menu)
  val menuItemFuture: ListenableFuture<MenuItem> =
    MediaRouteButtonFactory.setUpMediaRouteButton(context, menu, R.id.media_route_menu_item)
  Futures.addCallback(
    menuItemFuture,
    object : FutureCallback<MenuItem> {
      override fun onSuccess(menuItem: MenuItem) {
        // Do something with the menu item.
      }

      override fun onFailure(t: Throwable) {
        // Handle the failure.
      }
    },
    executor,
  )
  // ...
  return true
}
Java
@Override
public boolean onCreateOptionsMenu(Menu menu) {
  // ...
  getMenuInflater().inflate(R.menu.sample_media_route_button_menu, menu);
  ListenableFuture<MenuItem> menuItemFuture =
      MediaRouteButtonFactory.setUpMediaRouteButton(context, menu, R.id.media_route_menu_item);
  Futures.addCallback(
      menuItemFuture,
      new FutureCallback<MenuItem>() {
        @Override
        public void onSuccess(MenuItem menuItem) {
          // Do something with the menu item.
        }

        @Override
        public void onFailure(Throwable t) {
          // Handle the failure.
        }
      },
      executor);
  // ...
  return true;
}

<br />

Add the MediaRouteButton as a View

You can set up a MediaRouteButton in your activity layout.xml.

  <androidx.mediarouter.app.MediaRouteButton
      android:id="@+id/media_route_button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:mediaRouteButtonTint="@android:color/white" />

To complete the setup for the MediaRouteButton, use the Media3 Cast MediaRouteButtonFactory in your Activity code.

Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)

  findViewById<MediaRouteButton>(R.id.media_route_button)?.also {
    val unused = MediaRouteButtonFactory.setUpMediaRouteButton(context, it)
  }
}
Java
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  // ...
  MediaRouteButton button = findViewById(R.id.media_route_button);
  ListenableFuture<Void> setUpFuture =
      MediaRouteButtonFactory.setUpMediaRouteButton(context, button);
}

<br />

Activity Listener

Create a Player.Listener in your Activity to listen for changes to media playback location. When the playbackType changes between PLAYBACK_TYPE_LOCAL and PLAYBACK_TYPE_REMOTE, you can adjust your UI as needed. To prevent memory leaks and to confine listener activity to only when your app is visible, register the listener in onStart and unregister it in onStop:

Kotlin
private val playerListener: Player.Listener =
  object : Player.Listener {
    override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {
      if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) {
        // Add UI changes for local playback.
      } else if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) {
        // Add UI changes for remote playback.
      }
    }
  }

override fun onStart() {
  super.onStart()
  mediaController.addListener(playerListener)
}

override fun onStop() {
  super.onStop()
  mediaController.removeListener(playerListener)
}
Java
private final Player.Listener playerListener =
    new Player.Listener() {
      @Override
      public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
        if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_LOCAL) {
          // Add UI changes for local playback.
        } else if (deviceInfo.playbackType == DeviceInfo.PLAYBACK_TYPE_REMOTE) {
          // Add UI changes for remote playback.
        }
      }
    };

@Override
protected void onStart() {
  super.onStart();
  mediaController.addListener(playerListener);
}

@Override
protected void onStop() {
  super.onStop();
  mediaController.removeListener(playerListener);
}

<br />

For more information about listening and responding to playback events, see the player events guide.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Google LLC
Publisher
Android (Google) (Official)
Distributed by
Android (Google)
Source
android/skills · media/media3-cast-integration · commit bf68e2d
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE.txt)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download media3-cast-integration.zip