首先,这一系列文章均基于本身的理解和实践,可能有不对的地方,欢迎你们指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深刻的知识网上也有许许多多的博文供你们学习了。
最后,写文章过程当中,会借鉴参考其余人分享的文章,会在文章最后列出,感谢这些做者的分享。android
码字不易,转载请注明出处!git
教程代码:【Github传送门】 |
---|
本文主要简介Android使用硬解码API实现硬解码的流程,包含MediaCodec输入输出缓冲、MediaCodec解码流程、解码代码封装和讲解。github
MediaCodec 是Android 4.1(api 16)版本引入的编解码接口,同时支持音视频的编码和解码。api
必定要好好理解接下来这两幅图,由于后续的代码就是基于这两幅图来编写的。缓存
首先,来看看MediaCodec的数据流,也是官方Api文档中的,不少文章都会引用。框架
仔细看一下,MediaCodec将数据分为两部分,分别为input(左边)和output(右边),即输入和输出两个数据缓冲区。异步
input:是给客户端输入须要解码的数据(解码时)或者须要编码的数据(编码时)。ide
output:是输出解码好(解码时)或者编码好(编码时)的数据给客户端。函数
MediaCodec内部使用异步的方式对input和output数据进行处理。MediaCodec将处理好input的数据,填充到output缓冲区,交给客户端渲染或处理post
注:客户端处理完数据后,必须手动释放output缓冲区,不然将会致使MediaCodec输出缓冲被占用,没法继续解码。
依然是一副来自官方的状态图
再仔细看看这幅图,总体上分为三个大的状态:Sotpped、Executing、Released。
首先,新建MediaCodec后,会进入Uninitialized状态;
其次,调用configure方法配置参数后,会进入Configured;
再次,调用start方法后,MediaCodec进入Flushed状态;
接着,调用dequeueInputBuffer方法后,进入Running状态;
最后,当解码/编码结束时,进入End of Stream(EOF)状态。
这时,一个视频就处理完成了。
那么,Flushed是什么状态呢?
从图中咱们能够看到,在Running或者End of Stream状态时,均可以调用flush方法,从新进入Flushed状态。
当咱们在解码过程当中,进入了End of Stream后,解码器就再也不接收输入了,这时候,须要调用flush方法,从新进入接收数据状态。
或者,咱们在播放视频过程当中,想进行跳播,这时候,咱们须要Seek到指定的时间点,这时候,也须要调用flush方法,清除缓冲,不然解码时间戳会混乱。
再次强调一下,必定要好好理解这两幅图,由于后续的代码就是基于这两幅图来编写的。
MediaCodec有两种工做模式,分别为异步模式和同步模式,这里咱们使用同步模式,异步模式能够参考官网例子。
根据官方的数据流图和状态图,画出一个最基础的解码流程以下:
通过初始化和配置之后,进入循环解码流程,不断的输入数据,而后获取解码完数据,最后渲染出来,直到全部数据解码完成(End of Stream)。
根据上面的流程图,能够发现,不管音频仍是视频,解码流程基本是一致的,不一样的地方只在于【配置】、【渲染】两个部分。
所以,咱们将整个解码流程抽象为一个解码基类:BaseDecoder,为了规范代码和更好的拓展性,咱们先定义一个解码器:IDecoder,继承Runnable。
interface IDecoder: Runnable {
/** * 暂停解码 */
fun pause()
/** * 继续解码 */
fun goOn()
/** * 中止解码 */
fun stop()
/** * 是否正在解码 */
fun isDecoding(): Boolean
/** * 是否正在快进 */
fun isSeeking(): Boolean
/** * 是否中止解码 */
fun isStop(): Boolean
/** * 设置状态监听器 */
fun setStateListener(l: IDecoderStateListener?)
/** * 获取视频宽 */
fun getWidth(): Int
/** * 获取视频高 */
fun getHeight(): Int
/** * 获取视频长度 */
fun getDuration(): Long
/** * 获取视频旋转角度 */
fun getRotationAngle(): Int
/** * 获取音视频对应的格式参数 */
fun getMediaFormat(): MediaFormat?
/** * 获取音视频对应的媒体轨道 */
fun getTrack(): Int
/** * 获取解码的文件路径 */
fun getFilePath(): String
}
复制代码
定义了解码器的一些基础操做,如暂停/继续/中止解码,获取视频的时长,视频的宽高,解码状态等等
为何继承Runnable?
这里使用的是同步模式解码,须要不断循环压入和拉取数据,是一个耗时操做,所以,咱们将解码器定义为一个Runnable,最后放到线程池中执行。
接着,继承IDecoder,定义基础解码器BaseDecoder。
首先来看下基础参数:
abstract class BaseDecoder: IDecoder {
//-------------线程相关------------------------
/** * 解码器是否在运行 */
private var mIsRunning = true
/** * 线程等待锁 */
private val mLock = Object()
/** * 是否能够进入解码 */
private var mReadyForDecode = false
//---------------解码相关-----------------------
/** * 音视频解码器 */
protected var mCodec: MediaCodec? = null
/** * 音视频数据读取器 */
protected var mExtractor: IExtractor? = null
/** * 解码输入缓存区 */
protected var mInputBuffers: Array<ByteBuffer>? = null
/** * 解码输出缓存区 */
protected var mOutputBuffers: Array<ByteBuffer>? = null
/** * 解码数据信息 */
private var mBufferInfo = MediaCodec.BufferInfo()
private var mState = DecodeState.STOP
private var mStateListener: IDecoderStateListener? = null
/** * 流数据是否结束 */
private var mIsEOS = false
protected var mVideoWidth = 0
protected var mVideoHeight = 0
//省略后面的方法
....
}
复制代码
首先,咱们定义了线程相关的资源,用于判断是否持续解码的mIsRunning,挂起线程的mLock等。
而后,就是解码相关的资源了,好比MdeiaCodec自己,输入输出缓冲,解码状态等等。
其中,有一个解码状态DecodeState和音视频数据读取器IExtractor。
为了方便记录解码状态,这里使用一个枚举类表示
enum class DecodeState {
/**开始状态*/
START,
/**解码中*/
DECODING,
/**解码暂停*/
PAUSE,
/**正在快进*/
SEEKING,
/**解码完成*/
FINISH,
/**解码器释放*/
STOP
}
复制代码
前面说过,MediaCodec须要咱们不断地喂数据给输入缓冲,那么数据从哪里来呢?确定是音视频文件了,这里的IExtractor就是用来提取音视频文件中数据流。
Android自带有一个音视频数据读取器MediaExtractor,一样为了方便维护和拓展性,咱们依然先定一个读取器IExtractor。
interface IExtractor {
/** * 获取音视频格式参数 */
fun getFormat(): MediaFormat?
/** * 读取音视频数据 */
fun readBuffer(byteBuffer: ByteBuffer): Int
/** * 获取当前帧时间 */
fun getCurrentTimestamp(): Long
/** * Seek到指定位置,并返回实际帧的时间戳 */
fun seek(pos: Long): Long
fun setStartPos(pos: Long)
/** * 中止读取数据 */
fun stop()
}
复制代码
最重要的一个方法就是readBuffer,用于读取音视频数据流
前面咱们只贴出了解码器的参数部分,接下来,贴出最重要的部分,也就是解码流程部分。
abstract class BaseDecoder: IDecoder {
//省略参数定义部分,见上
.......
final override fun run() {
mState = DecodeState.START
mStateListener?.decoderPrepare(this)
//【解码步骤:1. 初始化,并启动解码器】
if (!init()) return
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
waitDecode()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
//若是数据没有解码完毕,将数据推入解码器解码
if (!mIsEOS) {
//【解码步骤:2. 将数据压入解码器输入缓冲】
mIsEOS = pushBufferToDecoder()
}
//【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
val index = pullBufferFromDecoder()
if (index >= 0) {
//【解码步骤:4. 渲染】
render(mOutputBuffers!![index], mBufferInfo)
//【解码步骤:5. 释放输出缓冲】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//【解码步骤:6. 判断解码是否完成】
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
doneDecode()
//【解码步骤:7. 释放解码器】
release()
}
/** * 解码线程进入等待 */
private fun waitDecode() {
try {
if (mState == DecodeState.PAUSE) {
mStateListener?.decoderPause(this)
}
synchronized(mLock) {
mLock.wait()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/** * 通知解码线程继续运行 */
protected fun notifyDecode() {
synchronized(mLock) {
mLock.notifyAll()
}
if (mState == DecodeState.DECODING) {
mStateListener?.decoderRunning(this)
}
}
/** * 渲染 */
abstract fun render(outputBuffers: ByteBuffer, bufferInfo: MediaCodec.BufferInfo)
/** * 结束解码 */
abstract fun doneDecode()
}
复制代码
在Runnable的run回调方法中,集成了整个解码流程:
abstract class BaseDecoder: IDecoder {
//省略上面已有代码
......
private fun init(): Boolean {
//1.检查参数是否完整
if (mFilePath.isEmpty() || File(mFilePath).exists()) {
Log.w(TAG, "文件路径为空")
mStateListener?.decoderError(this, "文件路径为空")
return false
}
//调用虚函数,检查子类参数是否完整
if (!check()) return false
//2.初始化数据提取器
mExtractor = initExtractor(mFilePath)
if (mExtractor == null ||
mExtractor!!.getFormat() == null) return false
//3.初始化参数
if (!initParams()) return false
//4.初始化渲染器
if (!initRender()) return false
//5.初始化解码器
if (!initCodec()) return false
return true
}
private fun initParams(): Boolean {
try {
val format = mExtractor!!.getFormat()!!
mDuration = format.getLong(MediaFormat.KEY_DURATION) / 1000
if (mEndPos == 0L) mEndPos = mDuration
initSpecParams(mExtractor!!.getFormat()!!)
} catch (e: Exception) {
return false
}
return true
}
private fun initCodec(): Boolean {
try {
//1.根据音视频编码格式初始化解码器
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
//2.配置解码器
if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
waitDecode()
}
//3.启动解码器
mCodec!!.start()
//4.获取解码器缓冲区
mInputBuffers = mCodec?.inputBuffers
mOutputBuffers = mCodec?.outputBuffers
} catch (e: Exception) {
return false
}
return true
}
/** * 检查子类参数 */
abstract fun check(): Boolean
/** * 初始化数据提取器 */
abstract fun initExtractor(path: String): IExtractor
/** * 初始化子类本身特有的参数 */
abstract fun initSpecParams(format: MediaFormat)
/** * 初始化渲染器 */
abstract fun initRender(): Boolean
/** * 配置解码器 */
abstract fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean
}
复制代码
初始化方法中,分为5个步骤,看起很复杂,实际很简单。
检查参数是否完整:路径是否有效等
初始化数据提取器:初始化Extractor
初始化参数:提取一些必须的参数:duration,width,height等
初始化渲染器:视频不须要,音频为AudioTracker
初始化解码器:初始化MediaCodec
在initCodec()中,
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
复制代码
初始化MediaCodec的时候:
须要说明的是:因为音频和视频的初始化稍有不一样,因此定义了几个虚函数,将不一样的东西交给子类去实现。具体将在下一篇文章[音视频播放:音视频同步]说明。
直接进入pushBufferToDecoder方法中
abstract class BaseDecoder: IDecoder {
//省略上面已有代码
......
private fun pushBufferToDecoder(): Boolean {
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
var isEndOfStream = false
if (inputBufferIndex >= 0) {
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
if (sampleSize < 0) {
//若是数据已经取完,压入数据结束标志:BUFFER_FLAG_END_OF_STREAM
mCodec!!.queueInputBuffer(inputBufferIndex, 0, 0,
0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEndOfStream = true
} else {
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
}
}
return isEndOfStream
}
}
复制代码
调用了如下方法:
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
复制代码
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
复制代码
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
复制代码
注意:若是SampleSize返回-1,说明没有更多的数据了。
这个时候,queueInputBuffer的最后一个参数要传入结束标记MediaCodec.BUFFER_FLAG_END_OF_STREAM。
直接进入pullBufferFromDecoder()
abstract class BaseDecoder: IDecoder {
//省略上面已有代码
......
private fun pullBufferFromDecoder(): Int {
// 查询是否有解码完成的数据,index >=0 时,表示数据有效,而且index为缓冲区索引
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
when (index) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {}
MediaCodec.INFO_TRY_AGAIN_LATER -> {}
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
mOutputBuffers = mCodec!!.outputBuffers
}
else -> {
return index
}
}
return -1
}
}
复制代码
第1、调用dequeueOutputBuffer方法查询是否有解码完成的可用数据,其中mBufferInfo用于获取数据帧信息,第二参数是等待时间,这里等待1000ms,填入-1是无限等待。
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
复制代码
第2、判断index类型:
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:输出格式改变了
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:输入缓冲改变了
MediaCodec.INFO_TRY_AGAIN_LATER:没有可用数据,等会再来
大于等于0:有可用数据,index就是输出缓冲索引
这里调用了一个虚函数render,也就是将渲染交给子类
调用releaseOutputBuffer方法, 释放输出缓冲区。
注:第二个参数,是个boolean,命名为render,这个参数在视频解码时,用于决定是否要将这一帧数据显示出来。
mCodec!!.releaseOutputBuffer(index, true)
复制代码
还记得咱们在把数据压入解码器时,当sampleSize < 0 时,压入了一个结束标记吗?
当接收到这个标志后,解码器就知道全部数据已经接收完毕,在全部数据解码完成之后,会在最后一帧数据加上结束标记信息,即
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
复制代码
在while循环结束后,释放掉全部的资源。至此,一次解码结束。
abstract class BaseDecoder: IDecoder {
//省略上面已有代码
......
private fun release() {
try {
mState = DecodeState.STOP
mIsEOS = false
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
mStateListener?.decoderDestroy(this)
} catch (e: Exception) {
}
}
}
复制代码
最后,解码器定义的其余方法(如pause、goOn、stop等)再也不细说,可查看工程源码。
原本打算把音频和视频播放部分也放到本篇来说,最后发现篇幅太长,不利于阅读,看了会累。因此把真正实现播放部分和下一篇【音视频播放:音视频同步】作一个整合,内容和长度都会更合理。
so,下一篇见!