mcpField
M
media3-cast-integration

media3-cast-integration

github.com/android开发10.0k839 次安装2 天前更新
代码仓库

关于 media3-cast-integration

为 Android 应用集成或迁移到 Jetpack Media3 Cast 提供开发指导,面向维护本地与 Cast 播放的开发者。说明依赖、播放器和媒体会话配置,以及 Compose 和 View 界面的接入步骤。

主要功能

  1. 01Media3 Cast 依赖配置
  2. 02CastPlayer 与媒体会话设置
  3. 03从旧版 Cast SDK 迁移
  4. 04Compose 投放按钮接入
  5. 05View 界面播放器重新绑定

适用场景

  1. 01为 Media3 应用新增 Cast 支持
  2. 02将旧版 Google Cast 集成迁移到 Media3
  3. 03在 Compose 播放界面添加投放控制

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/android/skills --skill media3-cast-integration

安装后在 Claude Code 中输入/media3-cast-integration确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/android/media/media3/cast/
create-castplayer.md
customize-castoptions.md
index.md

详细介绍

前提条件

  • Jetpack Media3 版本必须为 >= 1.9.0。较低版本不支持 Cast。

术语表

  • CastPlayer:Media3 Player,用于控制本地和远程 Cast 设备上的播放。
  • RemoteCastPlayer:与 Cast 接收器通信的 Media3 Player,仅用于远程播放。
  • Google Cast SDK:处于维护模式的旧版投放 SDK,已由 Jetpack Media3 取代。
  • OptionsProvider:提供配置选项以初始化 GMS CastContext 的接口。

通用指南

步骤 1:设置依赖项

要完成此步骤,必须确保以下事项:

  • 在应用级构建文件中声明版本为 1.9.0 或更高的 media3-cast 依赖项。

    implementation("androidx.media3:media3-cast:1.11.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 依赖项。
    • 此阶段保留现有旧版依赖项不变,以避免编译错误。

步骤 2:更新清单

要完成此步骤,必须确保以下事项:

  • 在清单的 <application> 标签内声明 Cast 选项提供程序。
  • 默认使用 DefaultCastOptionsProvider。请参阅开始使用 CastPlayer 中的“OptionsProvider”一节。
  • 仅在明确要求时声明自定义 OptionsProvider。请参阅自定义 CastOptions。
  • 确保存在 INTERNET 权限。不要添加不必要的权限。
  • 从旧版 Cast SDK 迁移时:
    • 不要删除现有的自定义选项提供程序文件或清单条目。

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

架构基线

集成 Media3 Cast 之前,现有应用通常采用以下两种设置之一:

  • **仅本地播放:**仅使用 Media3 ExoPlayer 支持本地播放。
  • **旧版 Cast 设置:**使用 ExoPlayer 进行本地播放,同时使用一个基于旧版 RemoteMediaClient 的 Player 包装器进行远程播放。UI 与一个交互的 MediaSession 通信,该 MediaSession 使用 ForwardingPlayer,最终将控制操作路由到本地或远程播放。

要完成此步骤,必须确保以下事项:

  • 在应用的 MediaSessionService(或 MediaLibraryService)的 onCreate() 方法中初始化 ExoPlayer 和 CastPlayer。
  • 除非明确要求使用 RemoteCastPlayer,否则默认使用 CastPlayer。请参阅开始使用 CastPlayer 中的“构建 CastPlayer”一节。
  • 对于 CastPlayer,将其实例直接传递给 MediaSession.Builder。
  • 替换所有旧版转发播放器包装器。
  • 迁移期间不要删除旧版类文件,以避免编译错误。
高级:RemoteCastPlayer
  • 仅在用户明确要求时使用 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)
    }
    

    }

第 4 步:设置 UI

基于 Compose 的 UI

要完成此步骤,必须确保以下各项:

  • 有关 Compose 集成指南,请参阅开始使用 CastPlayer 中的“向播放器添加 MediaRouteButton 可组合项”一节。

  • 使用 androidx.media3.cast 软件包中的 MediaRouteButton 可组合项。

  • 不要在 Compose UI 层次结构中使用 AndroidView。

  • 将 MediaRouteButton 放在播放控件旁边的区域,不要让系统 UI 遮挡它。

  • 不要使用 PlayerSurface 创建自定义播放器 UI。请使用 Material3 Player 可组合项。

  • 在播放位置发生变化时强制重新组合,以确保 UI 同步。对 DeviceInfo 变化使用 key 约束:

    @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 的 UI

要完成此步骤,必须确保以下事项:

  • 对于基于 View 的 UI 设置,请参阅CastPlayer 入门中的“添加 UI 元素”部分。

  • Casting 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:

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

警告

**警告:**不要直接执行清理。仅在用户明确要求时,才移除旧版文件和依赖项。

要完成此步骤,必须确保以下事项:

  • 移除旧版 GMS Cast SDK(libs.play.services.cast.framework)和 MediaRouter(libs.androidx.mediarouter)依赖项。
  • 如果采用了 DefaultCastOptionsProvider,则删除自定义 OptionsProvider 类和清单条目。
  • 如果存在旧版 MediaTransferReceiver 清单声明,则将其移除。
  • 移除所有对旧版 Cast SDK 组件的引用,例如旧版辅助包装器、转发播放器和 RemoteMediaClient 接口。
  • 如果已完成向 Compose 的迁移,则删除旧版 View XML 布局、菜单文件以及对 PlayerView 的引用。

评论

支持 Markdown