基础播放器构建:SimpleExoPlayer 创建、MediaItem 封装、PlayerView 在 Compose 中嵌入、SurfaceProvider 绑定
本章将完整搭建一个基于 ExoPlayer 的最小播放器内核,涵盖从 player 实例创建、媒体条目封装、到 Compose 视图集成与 Surface 绑定四个核心步骤。这是后续所有高级功能(缓存、DRM、字幕、自定义控制器)的基石,必须确保每一步的生命周期与渲染链路正确闭合。
2.1 核心职责与数据流
一个完整的基础播放器由四个角色协作完成:
- SimpleExoPlayer:播放引擎核心,负责解码、渲染、状态管理。
- MediaItem:媒体资源描述对象,封装 URI、DRM 方案、字幕轨道等元数据。
- PlayerView(Compose 侧):通过 AndroidView 封装原生 SurfaceView/TextureView,为 player 提供显示画布。
- SurfaceProvider:连接 player 输出端与视图 Surface 的生命周期安全桥接。
下图展示了四者之间的依赖与数据流向:
2.2 SimpleExoPlayer 创建
ExoPlayer 推荐通过 ExoPlayer.Builder 链式构建,并绑定应用生命周期。核心点:
- 使用
android.content.Context创建 builder。 - 可通过
.setRenderersFactory()自定义解码器工厂。 - 必须通过
.build()获取实例,并在 Activity/Fragment 的onStop中release()。
// 创建包含默认渲染器的播放器
SimpleExoPlayer player = new ExoPlayer.Builder(context)
.setRenderersFactory(new DefaultRenderersFactory(context))
.build();
// 生命周期管理示例(在 ViewModel 或 CompositionLocal 中维护)
override fun onStop() {
super.onStop()
player.release() // 释放解码器与 Surface
}
最佳实践:将 Player 实例托管在
ViewModel 中,避免配置变更时重建。使用 rememberPlayer(context) Compose 方法时确保跟随保留实例。
2.3 MediaItem 封装
MediaItem 是 ExoPlayer 3.x 引入的统一资源描述对象,替代旧版 MediaSource 链式构建。封装时需明确:
| 参数 | 说明 | 示例值 |
|---|---|---|
| Uri | 媒体源地址(本地文件 / 网络流 / 资产) | Uri.parse("https://...mp4") |
| DrmConfiguration | DRM 方案与许可证 URL(可选) | MediaItem.DrmConfiguration.Builder(Uri.parse(...)).build() |
| SubtitleConfiguration | 外挂字幕轨道(可选) | SubtitleConfiguration.Builder(uri, MimeTypes.TEXT_VTT) |
| MediaMetadata | 标题、封面等元数据(可选) | MediaMetadata.Builder().setTitle("Demo").build() |
MediaItem mediaItem = new MediaItem.Builder()
.setUri(Uri.parse("https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv"))
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle("ExoPlayer 测试视频")
.build()
)
.build();
注意:MediaItem 构建器方法是 Java 风格的,Kotlin 中需注意使用
apply { } 或直接构造函数。URI 必须为路径或 content:// scheme,不支持相对路径。
2.4 PlayerView 在 Compose 中嵌入
Compose 中没有对应的 ExoPlayer 原生视图,必须通过 AndroidView 桥接 StyledPlayerView 或 PlayerView(来自 com.google.android.exoplayer2.ui 包)。核心步骤:
- 在
AndroidView的 factory 中创建StyledPlayerView(context)。 - 在 update 或 LaunchedEffect 中绑定 player 实例。
- 设置
useController控制是否显示内置控制条。
@Composable
fun ExoPlayerView(
player: SimpleExoPlayer,
modifier: Modifier = Modifier
) {
// 使用 StyledPlayerView 以获得默认控制条
AndroidView(
modifier = modifier,
factory = { context ->
StyledPlayerView(context).apply {
this.player = player
useController = true // 显示播放/暂停/进度条
keepScreenOn = true
}
},
update = { view ->
view.player = player // 确保重组时重新绑定
}
)
}
2.5 SurfaceProvider 绑定
当需要自定义渲染 Surface(例如使用 TextureView 配合 Camera/GL 叠加)时,必须手动提供 SurfaceProvider。ExoPlayer 从 2.13 开始使用 setVideoSurfaceProvider() 替代旧的 setVideoSurface(),以支持异步 Surface 生命周期。
// 自定义 TextureView 作为 SurfaceProvider
val textureView = TextureView(context)
val surfaceProvider = object : SurfaceProvider {
override fun onSurfaceReady(surface: Surface) {
player.setVideoSurface(surface, true) // true 表示释放旧 surface
}
override fun onSurfaceDestroyed() {
player.setVideoSurface(null, false)
}
}
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
surfaceProvider.onSurfaceReady(Surface(surface))
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
surfaceProvider.onSurfaceDestroyed()
return true
}
// 其他回调...
}
在 Compose 中如果直接使用 StyledPlayerView,内部已自动管理 SurfaceProvider,无需手动处理。只有在自定义渲染视图时才需要介入。
2.6 调试要点与常见错误
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
| 黑屏无画面 | Surface 未绑定 / 生命周期中 player 被释放 | 检查 PlayerView.player 赋值时机,确保在 onStart 之后调用 player.prepare() |
| 播放器崩溃:MediaCodec 错误 | URI 不可访问 / 编解码器不支持 | 使用 DefaultRenderersFactory 或添加 MediaSource.Factory 降级 |
| Compose 重组闪烁 | AndroidView 的 update 未正确使用 key | 将 player 实例作为 remember 的 key,或用 LaunchedEffect(player) 绑定 |
| 音频正常但视频卡顿 | Surface 尺寸与视频分辨率不匹配 | 在 player.addListener 的 onVideoSizeChanged 中动态调整视图尺寸 |
2.7 完整集成流程总结
- 创建
SimpleExoPlayer实例(建议使用ExoPlayer.Builder并注入ApplicationContext)。 - 构造
MediaItem,设置 URI 及可选元数据。 - 调用
player.setMediaItem(mediaItem)并player.prepare()。 - 在 Compose 中通过
AndroidView放置StyledPlayerView,绑定 player。 - 如需自定义 Surface,手动实现
SurfaceProvider并调用player.setVideoSurfaceProvider()。 - 在
DisposableEffect或onStop中player.release()。
核心检查清单:
- ❒ Player 实例在 ViewModel 层保持单例。
- ❒ MediaItem 使用
setUri()而非字符串拼接。 - ❒ Compose 中使用
remember保持 PlayerView 不因重组重复创建。 - ❒ 生命周期回调
onStop释放 player,onStart恢复播放。