跳到主要内容

设计视频播放器 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

  1. 小窗模式:Player 的 Surface 绑定到列表 item 中的 PlayerView
  2. 切换全屏:将 PlayerView 从列表中移除,添加到全屏 Activity/Dialog 的容器中
  3. 退出全屏:反向操作

全程不中断播放,因为 Player 实例和 Surface 关系不变,只是 View 被 re-parent 了。

相关链接