关于 media3-cast-integration
为 Android 应用集成或迁移到 Jetpack Media3 Cast 提供开发指导,面向维护本地与 Cast 播放的开发者。说明依赖、播放器和媒体会话配置,以及 Compose 和 View 界面的接入步骤。
主要功能
- 01Media3 Cast 依赖配置
- 02CastPlayer 与媒体会话设置
- 03从旧版 Cast SDK 迁移
- 04Compose 投放按钮接入
- 05View 界面播放器重新绑定
适用场景
- 01为 Media3 应用新增 Cast 支持
- 02将旧版 Google Cast 集成迁移到 Media3
- 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.mdcustomize-castoptions.mdindex.md详细介绍
前提条件
- Jetpack Media3 版本必须为
>= 1.9.0。较低版本不支持 Cast。
术语表
CastPlayer:Media3Player,用于控制本地和远程 Cast 设备上的播放。RemoteCastPlayer:与 Cast 接收器通信的 Media3Player,仅用于远程播放。- Google Cast SDK:处于维护模式的旧版投放 SDK,已由 Jetpack Media3 取代。
OptionsProvider:提供配置选项以初始化 GMSCastContext的接口。
通用指南
- 旧版 Google Cast SDK 处于维护模式。
- 对于新的 Cast 设置:
- 必须使用 Jetpack Media3 Cast。
- 除非明确要求,否则不得使用旧版 Cast SDK。
步骤 1:设置依赖项
要完成此步骤,必须确保以下事项:
-
在应用级构建文件中声明版本为 1.9.0 或更高的
media3-cast依赖项。implementation("androidx.media3:media3-cast:1.11.1") -
确保存在所需的 Media3 依赖项:
androidx.media3:media3-exoplayerandroidx.media3:media3-sessionandroidx.media3:media3-ui-compose
-
如果应用使用旧版 Views,请添加
media3-ui。 -
确保所有 Media3 依赖项使用相同的版本。
-
以开始使用 CastPlayer 中“添加构建依赖项”一节的配置为准。
-
对于尚未集成 Cast 的应用:
- 确认不存在旧版 Cast SDK(
libs.play.services.cast.framework)。
- 确认不存在旧版 Cast SDK(
-
从旧版 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。请使用 Material3Player可组合项。 -
在播放位置发生变化时强制重新组合,以确保 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:
- 不要使用
AndroidView包装旧版PlayerView。 - 按照CastPlayer 入门中的说明,实现 Material3
Playercomposable 和MediaRouteButtoncomposable。 - 移除旧版 XML 布局声明、菜单文件和 View 组件引用。
- 不要使用
第 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的引用。