设计视频播放器 SDK
问题
如何设计一个可扩展的 Android 视频播放器 SDK?
答案
分层架构
播放引擎抽象
// 抽象播放引擎接口,支持替换底层实现
interface IPlayerEngine {
fun setDataSource(url: String, headers: Map<String, String> = emptyMap())
fun prepare()
fun start()
fun pause()
fun stop()
fun release()
fun seekTo(positionMs: Long)
fun getDuration(): Long
fun getCurrentPosition(): Long
fun isPlaying(): Boolean
fun setPlaybackSpeed(speed: Float)
fun setSurface(surface: Surface?)
fun setOnStateChangeListener(listener: OnPlayerStateListener?)
}
// ExoPlayer 实现
class ExoPlayerEngine(context: Context) : IPlayerEngine {
private val player = ExoPlayer.Builder(context).build()
override fun setDataSource(url: String, headers: Map<String, String>) {
val mediaItem = MediaItem.fromUri(url)
player.setMediaItem(mediaItem)
}
override fun prepare() { player.prepare() }
override fun start() { player.play() }
override fun pause() { player.pause() }
// ... 其他方法
}
手势控制
class PlayerGestureDetector(
private val controller: PlayerController
) : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(
e1: MotionEvent?, e2: MotionEvent,
distanceX: Float, distanceY: Float
): Boolean {
val startX = e1?.x ?: return false
val screenWidth = controller.getWidth()
if (abs(distanceX) > abs(distanceY)) {
// 水平滑动:快进/快退
controller.seekByDelta((-distanceX * 100).toLong())
} else {
if (startX < screenWidth / 2) {
// 左侧垂直滑动:调节亮度
controller.adjustBrightness(-distanceY)
} else {
// 右侧垂直滑动:调节音量
controller.adjustVolume(-distanceY)
}
}
return true
}
override fun onDoubleTap(e: MotionEvent): Boolean {
// 双击暂停/播放
controller.togglePlayPause()
return true
}
}
设计要点
| 要点 | 方案 |
|---|---|
| 引擎可替换 | 面向接口编程,IPlayerEngine |
| 全屏/小窗 | Window Flag + 旋转监听 |
| 画中画 | PictureInPictureParams |
| 弹幕 | 独立 SurfaceView 层叠加 |
| 预加载 | CacheDataSource + 提前 prepare |
| 无缝续播 | 保存 position 到 ViewModel |
常见面试问题
Q1: 播放器如何实现无缝全屏切换?
答案:
关键是复用同一个 Player 实例,只移动 Surface:
- 小窗模式:Player 的 Surface 绑定到列表 item 中的
PlayerView - 切换全屏:将
PlayerView从列表中移除,添加到全屏 Activity/Dialog 的容器中 - 退出全屏:反向操作
全程不中断播放,因为 Player 实例和 Surface 关系不变,只是 View 被 re-parent 了。