Skip to content

2 播放内核

WenchangMai edited this page Jun 7, 2020 · 3 revisions

播放内核的封装和使用

如果要设计一个Android的多媒体播放器,其核心内容就是播放内核和视图容器。目前常见的播放内核有系统自带的MediaPlayer,谷歌的ExoPlayer和BiliBili的IjkPlayer等等。要使得我们的播放器可以自由切换内核,我们可以通过策略模式,对播放内核进行一个统一的封装。这一次,我们选择使用Kotlin语言实现一个功能完备的多媒体播放器。

播放状态和流程

MediaPlayer State diagram

这是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的封装,分别是 SystemMediaPlayerIjkPlayerExoMediaPlayer,大家可以点击链接查看他们的实现代码。可以看到,我们实现除了实现基本的播放功能,还实现了各个播放内核的设置播放资源的方法,并在方法最后把播放器状态设置为INITIALIZED。在每个方法的背后,一般都对播放状态进行了一个更新,方便开发者对状态进行监听。

IjkPlayer比其他的播放器多了设置倍速播放和硬件加速的方法,ExoMediaPlayerprepaer()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) {
        //缓冲状态发生改变
        
    }
})
Clone this wiki locally