Android 视频播放器封装(一):先抽象播放能力,再接入 Media3
视频播放器的封装,最容易踩的坑是把 ExoPlayer、XML 控件、Compose 状态和页面业务全部写进一个类。短期可以播放,后续一旦需要 RecyclerView、全屏、播放进度恢复,代码就会迅速失控。
这次实现先做了一件事:把“播放能力”和“展示方式”拆开。
项目分成三个模块:
• video-player:核心播放接口和 Media3 实现。
•video-player-xml:传统 View/XML 播放器 UI。
•video-player-compose:Compose 播放器 UI。
核心模块不关心按钮放在哪里,也不关心是 RecyclerView 还是 Compose。它只负责设置地址、播放、暂停、跳转、静音、状态和资源释放。
定义稳定的播放器协议
核心接口如下:
interface VideoPlayer {
val state: StateFlow<VideoPlayerState>
val currentPositionMs: Long
val durationMs: Long
val bufferedPositionMs: Long
val isPlaying: Boolean
val isLooping: Boolean
fun setMediaUrl(url: String)
fun setErrorListener(listener: ((message: String, cause: Throwable?) -> Unit)?)
fun play()
fun pause()
fun stop()
fun seekTo(positionMs: Long)
fun setMuted(muted: Boolean)
fun setLooping(looping: Boolean)
fun release()
}
这里最关键的是 StateFlow。UI 不需要监听 ExoPlayer 的各种回调,而是订阅统一状态:Idle、Loading、Ready、Playing、Paused、Ended 和 Error。
sealed interface VideoPlayerState {
data object Idle : VideoPlayerState
data object Loading : VideoPlayerState
data class Ready(val durationMs: Long) : VideoPlayerState
data class Playing(val positionMs: Long, val durationMs: Long) : VideoPlayerState
data class Paused(val positionMs: Long, val durationMs: Long) : VideoPlayerState
data class Ended(val durationMs: Long) : VideoPlayerState
data class Error(val message: String, val cause: Throwable? = null) : VideoPlayerState
}
这样做的好处是,XML 和 Compose 不必直接依赖 Media3 的 Player.STATE_READY、STATE_BUFFERING 等常量。未来如果替换成 IJKPlayer 或其他播放器,也只需要实现同一份接口。
只让实现层知道 ExoPlayer
Media3VideoPlayer 内部创建 ExoPlayer 与 PlayerView,并将 Media3 回调映射成业务状态:
override fun setMediaUrl(url: String) {
mediaUrl = url
prepared = false
exoPlayer.setMediaItem(MediaItem.fromUri(url))
mutableState.value = VideoPlayerState.Idle
if (config.autoPlay) play()
}
override fun play() {
if (!prepared) {
prepared = true
mutableState.value = VideoPlayerState.Loading
exoPlayer.prepare()
}
exoPlayer.play()
}
这样列表初始只显示封面图,用户点播放才发生视频请求。
给 UI 提供渲染能力,但不污染核心协议
有 UI 的播放器需要拿到渲染 View,因此额外定义一个可选接口:
interface RenderableVideoPlayer : VideoPlayer {
fun renderView(): Any
}
XML 可以直接把它添加进布局;Compose 则用 AndroidView 承载。核心播放协议仍然没有 Android View 依赖。
小结
第一篇完成了播放器最重要的边界划分:
•核心模块只负责播放。
•UI 模块只负责展示和交互。
•页面只负责业务编排。
•列表默认不加载视频,点击后再 prepare。
下一篇开始做 XML 播放器 UI,并处理一个很常见的体验问题:视频真正出首帧之前,不让黑色背景露出来
gitee 地址:https://gitee.com/nextT/playvideo