-
Notifications
You must be signed in to change notification settings - Fork 101
2 播放内核
如果要设计一个Android的多媒体播放器,其核心内容就是播放内核和视图容器。目前常见的播放内核有系统自带的MediaPlayer,谷歌的ExoPlayer和BiliBili的IjkPlayer等等。要使得我们的播放器可以自由切换内核,我们可以通过策略模式,对播放内核进行一个统一的封装。这一次,我们选择使用Kotlin语言实现一个功能完备的多媒体播放器。
这是Android官网文档上的关于播放器的状态转移和播放流程图(地址),我们设计多媒体播放器的时候,也应遵循这个流程。这个图包含了很多有用的信息,比如,开始播放之前,要先进入准备状态;而准备之前,要先设置资源,进入INITIALIZED
状态;在什么情况可以调用seekTo()
方法;调用了release()
方法之后,就进入了END
状态,播放器必须重新初始化或者调用reset()
才能重新开始播放等等。
根据上一节,总结一下图上标注的状态,我们定义出播放状态的枚举类:
sealed class PlayerState(val code: Int) {
object IDLE :
PlayerState(1 shl 1)
object INITIALIZED :
PlayerState(1 shl 2)
object PREPARING :
PlayerState(1 shl 3)
object PREPARED :
PlayerState(1 shl 4)
object STARTED :
PlayerState(1 shl 5)
object PAUSED :
PlayerState(1 shl 6)
object STOPPED :
PlayerState(1 shl 7)
object COMPLETED :
PlayerState(1 shl 8)
object ERROR :
PlayerState(1 shl 9)
object END :
PlayerState(1 shl 10)
}
实际上,并没有真的使用Kotlin中的枚举类,而是通过sealed class
去实现了这样一个枚举功能。由于播放状态之间有一定的先后顺序,我们给这个状态加了一个整形的code
字段来标示这种关系,一般来讲code
越大表示播放状态越靠后。
根据流程图,我们定义出主要的播放行为:
interface IMediaPlayer<T> {
/**
* 播放器实例
*/
var impl: T
/**
* 准备后是否播放
*/
var playWhenReady: Boolean
/**
* 是否正在播放
*/
val isPlaying: Boolean
/**
* 当前位置
*/
val currentPosition: Long
/**
* 视频长度
*/
val duration: Long
/**
* 视频高度
*/
val videoHeight: Int
/**
* 视频宽度
*/
val videoWidth: Int
/**
* 当前播放器状态
*/
val playerState: PlayerState
/**
* 播放器状态监听
*/
val playerStateLD: LiveData<PlayerState>
/**
* 播放器尺寸
*/
val videoSizeLD: LiveData<VideoSize>
/**
* 加载进度
*/
val bufferingProgressLD: LiveData<Int>
/**
* 视频信息
*/
val videoInfoLD: LiveData<VideoInfo>
/**
* 视频报错
*/
val videoErrorLD: LiveData<VideoInfo>
/**
* 开始
*/
fun start()
/**
* 准备
*/
fun prepare()
/**
* 异步准备
*/
fun prepareAsync()
/**
* 暂停
*/
fun pause()
/**
* 停止
*/
fun stop()
/**
* 跳转到指定到位置
*/
fun seekTo(time: Long)
/**
* 重置
*/
fun reset()
/**
* 释放
*/
fun release()
/**
* 设置音量
*/
fun setVolume(volume: Float)
/**
* 设置循环播放
*/
fun setLooping(isLoop: Boolean)
/**
* 设置播放容器
*/
fun setSurface(surface: Surface?)
/**
* 设置播放容器
*/
fun setDisplay(surfaceHolder: SurfaceHolder)
}
我们通过interface来定义一个抽象的播放内核,考虑到除了以上规定的播放行为之外,播放内核本身会有一些特别的Api,为了方便调用,这里通过范型定义了一个impl
字段,存放播放器的真实对象的引用。
项目中,我们实现了对系统自带的MediaPlayer、BiliBili的IjkPlayer和ExoPlayer的封装,分别是 SystemMediaPlayer,IjkPlayer和ExoMediaPlayer,大家可以点击链接查看他们的实现代码。可以看到,我们实现除了实现基本的播放功能,还实现了各个播放内核的设置播放资源的方法,并在方法最后把播放器状态设置为INITIALIZED。在每个方法的背后,一般都对播放状态进行了一个更新,方便开发者对状态进行监听。
IjkPlayer
比其他的播放器多了设置倍速播放和硬件加速的方法,ExoMediaPlayer
的prepaer()
和prepareAsync()
的实现一样的,都是异步的方式去让播放内核进入准备状态。如果开发者有其他的需要,比如设置缓存策略等等,可以通过impl
这个字段获得播放器实例的引用去自由发挥。
在开始播放之前,需要先设置播放源,设置成功后,播放器进入INITIALIZED
状态,然后就可以预备播放了。下面介绍一下各个播放内核最基本的设置播放源的方法:
SystemMediaPlayer
val mediaPlayer = SystemMediaPlayer()
mediaPlayer.setDataSource(this, Uri.parse("https://test.mp4"))
IjkPlayer
val mediaPlayer = IjkPlayer()
mediaPlayer.setDataSource(this, Uri.parse("https://test.mp4"))
ExoMediaPlayer
val mediaPlayer = ExoMediaPlayer(context)
val mediaSource = ExoSourceBuilder(this, "https://test.mp4")
.apply {
this.isLooping = false
this.cacheEnable = true
}
.build()
mediaPlayer.mediaSource = mediaSource
ExoSourceBuilder
是我们封装的设置ExoPlayer播放源的构建帮助类,大家可以查看它的代码( 地址)。ExoPlayer对不同的编码格式可以设置对应的DataSource,ExoSourceBuilder
的作用主要是根据播放源的url去构建不同的DataSource并且可以设置不同的缓存策略等。当然,播放内核对于不同的播放源提供了其他的设置方法,比如本地资源、assets文件、raw文件等等。
我们决定采用LiveData去提供播放状态的监听,这样做可以很好地支持多个地方对播放器同时监听。下面给出例子:
mediaPlayer.playerStateLD.observe(lifecycleOwner, object : Observer<PlayerState> {
override fun onChanged(it: PlayerState) {
//播放状态发生改变
}
})
同样,对播放进度的监听也是很多开发者关心的地方,由于播放内核并没有直接提供回调接口,只提供了获取当前播放进度currentPosition
和视频时长duration
的方法,所以只能由开发者设置循环去获取这两个字段进行监听。这里给出例子:
Handler().let { handler ->
handler.post(object : Runnable {
override fun run() {
val progress = mediaPlayer.currentPosition * 1.000f / mediaPlayer.duration
Log.d("this@MainActivity", "progress: $progress")
if (mediaPlayer.isPlaying) {
handler.postDelayed(this, 1000)
}
}
})
}
mediaPlayer.bufferingProgressLD.observe(lifecycleOwner, object : Observer<Int> {
override fun onChanged(it: Int) {
//缓冲状态发生改变
}
})