media3-cast-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Prerequisites

前提条件

  • Jetpack Media3 version must be
    >= 1.9.0
    . Cast isn't available in lower versions.
  • Jetpack Media3版本必须
    >= 1.9.0
    。低于此版本不支持Cast功能。

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
    .
  • CastPlayer
    :Media3的
    Player
    组件,可控制本地和远程Cast设备上的播放。
  • RemoteCastPlayer
    :Media3的
    Player
    组件,仅用于远程播放,与Cast接收器通信。
  • Google Cast SDK:已进入维护模式的旧版投屏SDK,已被Jetpack Media3取代。
  • OptionsProvider
    :为初始化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.
  • 旧版Google Cast SDK已进入维护模式。
  • 对于新的Cast集成:
    • 必须使用Jetpack Media3 Cast
    • 除非有明确要求,否则不得使用旧版Cast SDK。

Step 1: Set up dependencies

步骤1:配置依赖项

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.10.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.
完成此步骤必须确保以下内容:
  • 在应用级构建文件中,声明版本1.9.0或更高的
    media3-cast
    依赖。
    implementation("androidx.media3:media3-cast:1.10.1")
  • 确保存在必要的Media3依赖:
    • androidx.media3:media3-exoplayer
    • androidx.media3:media3-session
    • androidx.media3:media3-ui-compose
  • 如果应用使用旧版Views,需添加
    media3-ui
  • 确保所有Media3依赖使用相同版本。
  • CastPlayer入门指南中“添加构建依赖项”部分的配置为标准。
  • 对于无现有Cast集成的应用:
    • 确认不存在旧版Cast SDK(
      libs.play.services.cast.framework
      )。
  • 从旧版Cast SDK迁移:
    • 先添加Media3 Cast依赖。
    • 此阶段保留现有旧版依赖,避免编译错误。

Step 2: Update the manifest

步骤2:更新清单文件

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.
完成此步骤必须确保以下内容:
  • 在清单的
    <application>
    标签内,声明Cast选项提供器。
  • 默认使用
    DefaultCastOptionsProvider
    。详情见CastPlayer入门指南中的“OptionsProvider”部分。
  • 仅在有明确要求时才声明自定义
    OptionsProvider
    。详情见自定义CastOptions
  • 确保存在
    INTERNET
    权限,不要添加不必要的权限。
  • 从旧版Cast SDK迁移:
    • 不要删除现有自定义选项提供器文件或清单条目。

Step 3: Implement the player and service

步骤3:实现播放器与服务

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.
集成Media3 Cast之前,现有应用通常采用以下两种架构之一:
  • 仅本地播放:仅使用Media3
    ExoPlayer
    支持本地播放。
  • 旧版Cast架构:使用
    ExoPlayer
    进行本地播放,同时使用基于旧版
    RemoteMediaClient
    Player
    包装器处理远程播放。UI通过
    MediaSession
    ForwardingPlayer
    交互,最终将控制指令路由到本地或远程播放。
完成此步骤必须确保以下内容:
  • 在应用的
    MediaSessionService
    (或
    MediaLibraryService
    )的
    onCreate()
    方法中,初始化
    ExoPlayer
    CastPlayer
  • 默认使用
    CastPlayer
    ,除非明确要求使用
    RemoteCastPlayer
    。详情见CastPlayer入门指南中的“构建CastPlayer”部分。
  • 对于
    CastPlayer
    ,直接将实例传递给
    MediaSession.Builder
  • 替换所有旧版转发播放器包装器。
  • 暂时不要删除旧版类文件,避免迁移期间出现编译错误。

Advanced:
RemoteCastPlayer

进阶:
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)
    }
    }
  • 仅在用户明确要求时使用
    RemoteCastPlayer
  • 使用
    localPlayer
    初始化
    MediaSession
    ,并在
    RemoteCastPlayer
    上设置
    SessionAvailabilityListener
    ,以便在Cast会话可用性变化时转移播放状态:
    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

步骤4:配置UI

Compose-based UI

基于Compose的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
    }
完成此步骤必须确保以下内容:
  • 关于Compose集成指南,参见CastPlayer入门指南中的“向播放器添加MediaRouteButton组合项”部分。
  • 使用
    androidx.media3.cast
    包中的
    MediaRouteButton
    组合项
  • 不要在Compose UI层次结构中使用
    AndroidView
  • MediaRouteButton
    放置在播放控件附近,不要隐藏在系统UI后方。
  • 不要使用
    PlayerSurface
    自定义播放器UI,使用Material3的
    Player
    组合项
  • 播放位置切换时强制重组,确保UI同步。在
    DeviceInfo
    变化时使用键约束:
    @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? {
       // 将MediaController连接到MediaSession并释放的逻辑
    }
    
    @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

基于View的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.
完成此步骤必须确保以下内容:
  • 基于View的UI配置,参见CastPlayer入门指南中的“添加UI元素”部分。
  • 投屏相关Activity必须继承
    AppCompatActivity
    FragmentActivity
    ,并使用
    Theme.AppCompat
    的派生主题。
  • 如果要将
    MediaRouteButton
    添加到选项菜单,确保
    AppCompat
    主题有可见的
    ActionBar
  • 将所有
    CastButtonFactory
    的实例和导入替换为
    MediaRouteButtonFactory
  • onDeviceInfoChanged
    事件时重新绑定
    PlayerView.player
    引用,防止黑屏或UI冻结:
    private val playerListener: Player.Listener =
      object : Player.Listener {
        override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {
          // 重置为null可绕过PlayerView.setPlayer()的实例相等性检查
          // (this.player == player),强制将视频表面重新绑定到控制器。
          playerView.player = null
          playerView.player = controller
        }
      }
  • 迁移到Compose:
    • 不要使用
      AndroidView
      包装旧版
      PlayerView
    • 按照CastPlayer入门指南实现Material3的
      Player
      组合项
      MediaRouteButton
      组合项
    • 删除旧版XML布局声明、菜单文件和View组件引用。

Step 5: Clean up legacy Cast SDK code

步骤5:清理旧版Cast SDK代码

[!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.
[!WARNING] **警告:**不要直接执行清理操作。仅在用户明确要求时,再删除旧版文件和依赖。
完成此步骤必须确保以下内容:
  • 删除旧版GMS Cast SDK(
    libs.play.services.cast.framework
    )和MediaRouter(
    libs.androidx.mediarouter
    )依赖。
  • 如果采用
    DefaultCastOptionsProvider
    ,删除自定义
    OptionsProvider
    类和清单条目。
  • 如果存在旧版
    MediaTransferReceiver
    清单声明,将其删除。
  • 删除所有对旧版Cast SDK组件的引用,比如旧版助手包装器、转发播放器和
    RemoteMediaClient
    接口。
  • 如果已完成向Compose的迁移,删除旧版View XML布局、菜单文件和
    PlayerView
    引用。