本篇文章的目的是为了跟随其他大佬的文章学习一遍,目的就是为了印象更加深刻
通过本文你可以学习到
以Android为基础,了解如何调用MediaCodec的API实现硬解码的过程,包括MediaCodec的输入输出缓冲,MediaCodec的解码流程,以及对该流程的代码编写。
一、MediaCodec介绍
MediaCodec是是 Android 平台提供的一个多媒体编解码器,从Android4.1(api 16)开始引入的,所以说,MediaCodec不支持跨平台,这也是跟ffmpeg最大的区别之一。MediaCodec封装的比较成熟,同时支持音视频的编码和解码,如果你的应用只发布在Android市场,其实MediaCodec够用了,而且也比较成熟。
下面关于数据流的两张图来自于MediaCodec官网,需要好好理解,后面的代码其实就是基于这两张图
数据流相关
官方文档首先介绍的就是数据流的内容,如下图所示
官网下面有对应的英文解释,大致翻译过来是这样:这个流程就是为了通过编码把输入数据转为输出数据。左边是输入数据input,右边是输出数据output。
input :首先使用一个空的缓冲区去input里面取数,数据填充满之后,进入codec环节,这个可以是解码的数据(解码时)或者需要编码的数据(编码时) output:解码好的的数据、或者编码好的数据通过缓冲区传递给请求者(或者订阅者),传递完成之后,请求者释放缓冲区给解码器,进行下一次的数据传递。
状态
再来看看编解码过程中的几个状态,来自于官网
该图主要表现为三个状态:Stopped、Exexuting、Released。更多的子环节,需要从左边图开始,跟随箭头,看向右边。
- Stopped: 包含Error、Uninitialized、Configured三个小状态
首先在新建MediaCodec之后,会先进入Uninitialized状态;
随后,调用configure方法配置参数后,进入Configured状态
- Exexuting: 包含Flushed、Running、End of Stream三个小状态
跟随上面,调用start方法后,MediaCodec进入Flushed状态;
接着,调用dequeueInputBuffer方法后,进入Running状态
最后,当编码/解码结束之后,进入End of Stream(EOF)状态
到这里,视频处理就算完成了
这里说明下Flushed状态
根据图中的箭头所示,在Running或者End of Stream状态时,都可以调用flush方法,重新进入Flushed状态。
在解码进入到End of Stream之后,解码器此时不再有任何输入,这时候需要调用flush方法,重新进入接收数据状态。
或者存在跳播的情况时,从一个视频到另一个视频,此时需要Seek到指定的时间点,这时候,需要调用flush方法,清除缓冲数据,否则解码时间戳会混乱。
- Released: 为了避免资源一直占据内存等,在数据处理完成之后,需要调用release方法,释放所有资源
上面两张图不要走马观花的看,需要多看几遍,知道每个状态的流转,这样代码写起来更加方便
二、解码流程
MediaCodec的解码流程分为同步和异步两种模式,这里先把同步的模式学习下。根据上面官方数据流处理流程图和状态图,画出一个基础的解码流程图如下:
经过Codec的初始化和配置参数以后,进入循环的解码流程,不断的输入数据流,然后得到解码完成的数据,最后在终端渲染出来,直到所有的数据都解码完成(End of Stream),数据流缓冲区释放(release),算是真正的结束。
三、开始解码
音频和视频的解码其实都在上面的循环解码这个流程里面,不同的地方也就只有【配置】、【渲染】两个部分。
创建解码器
首先要去定义一个接口IDecoder,定义基础的解码方法,如暂停/继续/停止解码、获取视频的时长、获取视频的宽高、解码状态等,该接口继承Runnable。
js
interface IDecoder: Runnable {
/**
* 暂停解码
*/
fun pause()
/**
* 继续解码
*/
fun goOn()
/**
* 跳转到指定位置
* 并返回实际帧的时间
*
* @param pos: 毫秒
* @return 实际时间戳,单位:毫秒
*/
fun seekTo(pos: Long): Long
/**
* 跳转到指定位置,并播放
* 并返回实际帧的时间
*
* @param pos: 毫秒
* @return 实际时间戳,单位:毫秒
*/
fun seekAndPlay(pos: Long): Long
/**
* 停止解码
*/
fun stop()
/**
* 是否正在解码
*/
fun isDecoding(): Boolean
/**
* 是否正在快进
*/
fun isSeeking(): Boolean
/**
* 是否停止解码
*/
fun isStop(): Boolean
/**
* 设置尺寸监听器
*/
fun setSizeListener(l: IDecoderProgress)
/**
* 设置状态监听器
*/
fun setStateListener(l: IDecoderStateListener?)
/**
* 获取视频宽
*/
fun getWidth(): Int
/**
* 获取视频高
*/
fun getHeight(): Int
/**
* 获取视频长度
*/
fun getDuration(): Long
/**
* 当前帧时间,单位:ms
*/
fun getCurTimeStamp(): Long
/**
* 获取视频旋转角度
*/
fun getRotationAngle(): Int
/**
* 获取音视频对应的格式参数
*/
fun getMediaFormat(): MediaFormat?
/**
* 获取音视频对应的媒体轨道
*/
fun getTrack(): Int
/**
* 获取解码的文件路径
*/
fun getFilePath(): String
/**
* 无需音视频同步
*/
fun withoutSync(): IDecoder
}
为什么继承Runnable?
这主要是因为这里使用的是同步方式解码,需要不断的循环压入和拉取数据,是一个耗时操作,因此这里定义一个Runnable,放到线程池中,处理一直压入的数据
再来看看如何定义一个基础的解码器BaseDecoder,去实现各个方法。
js
abstract class BaseDecoder(private val mFilePath: String): IDecoder {
private val TAG = "BaseDecoder"
//-------------线程相关------------------------
/**
* 解码器是否在运行
*/
private var mIsRunning = true
/**
* 线程等待锁
*/
private val mLock = Object()
/**
* 是否可以进入解码
*/
private var mReadyForDecode = false
//---------------状态相关-----------------------
/**
* 音视频解码器
*/
private var mCodec: MediaCodec? = null
/**
* 音视频数据读取器
*/
private var mExtractor: IExtractor? = null
/**
* 解码输入缓存区
*/
private var mInputBuffers: Array<ByteBuffer>? = null
/**
* 解码输出缓存区
*/
private var mOutputBuffers: Array<ByteBuffer>? = null
/**
* 解码数据信息
*/
private var mBufferInfo = MediaCodec.BufferInfo()
private var mState = DecodeState.STOP
protected var mStateListener: IDecoderStateListener? = null
/**
* 流数据是否结束
*/
private var mIsEOS = false
protected var mVideoWidth = 0
protected var mVideoHeight = 0
private var mDuration: Long = 0
private var mStartPos: Long = 0
private var mEndPos: Long = 0
/**
* 开始解码时间,用于音视频同步
*/
private var mStartTimeForSync = -1L
// 是否需要音视频渲染同步
private var mSyncRender = true
final override fun run() {
if (mState == DecodeState.STOP) {
mState = DecodeState.START
}
mStateListener?.decoderPrepare(this)
//【解码步骤:1. 初始化,并启动解码器】
if (!init()) return
Log.i(TAG, "开始解码")
try {
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
Log.i(TAG, "进入等待:$mState")
waitDecode()
// ---------【同步时间矫正】-------------
//恢复同步的起始时间,即去除等待流失的时间
mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
if (mStartTimeForSync == -1L) {
mStartTimeForSync = System.currentTimeMillis()
}
//如果数据没有解码完毕,将数据推入解码器解码
if (!mIsEOS) {
//【解码步骤:2. 见数据压入解码器输入缓冲】
mIsEOS = pushBufferToDecoder()
}
//【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
val index = pullBufferFromDecoder()
Log.i(TAG, "pullBufferFromDecoder index = $index")
if (index >= 0) {
// ---------【音视频同步】-------------
if (mSyncRender && mState == DecodeState.DECODING) {
sleepRender()
}
//【解码步骤:4. 渲染】
if (mSyncRender) {// 如果只是用于编码合成新视频,无需渲染
render(mOutputBuffers!![index], mBufferInfo)
}
//将解码数据传递出去
val frame = Frame()
frame.buffer = mOutputBuffers!![index]
frame.setBufferInfo(mBufferInfo)
mStateListener?.decodeOneFrame(this, frame)
//【解码步骤:5. 释放输出缓冲】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//【解码步骤:6. 判断解码是否完成】
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.i(TAG, "解码结束")
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
doneDecode()
release()
}
}
private fun init(): Boolean {
if (mFilePath.isEmpty() || !File(mFilePath).exists()) {
Log.w(TAG, "文件路径为空")
mStateListener?.decoderError(this, "文件路径为空")
return false
}
if (!check()) return false
//初始化数据提取器
mExtractor = initExtractor(mFilePath)
if (mExtractor == null ||
mExtractor!!.getFormat() == null) {
Log.w(TAG, "无法解析文件")
return false
}
//初始化参数
if (!initParams()) return false
//初始化渲染器
if (!initRender()) return false
//初始化解码器
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 {
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = type?.let { MediaCodec.createDecoderByType(it) }
if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
waitDecode()
}
mCodec!!.start()
mInputBuffers = mCodec?.inputBuffers
mOutputBuffers = mCodec?.outputBuffers
} catch (e: Exception) {
return false
}
return true
}
private fun pushBufferToDecoder(): Boolean {
var inputBufferIndex = mCodec!!.dequeueInputBuffer(1000)
var isEndOfStream = false
if (inputBufferIndex >= 0) {
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
if (sampleSize < 0) {
//如果数据已经取完,压入数据结束标志:MediaCodec.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
}
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
}
private fun sleepRender() {
val passTime = System.currentTimeMillis() - mStartTimeForSync
val curTime = getCurTimeStamp()
if (curTime > passTime) {
Thread.sleep(curTime - passTime)
}
}
private fun release() {
try {
Log.i(TAG, "解码停止,释放解码器")
mState = DecodeState.STOP
mIsEOS = false
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
mStateListener?.decoderDestroy(this)
} catch (e: Exception) {
}
}
/**
* 解码线程进入等待
*/
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)
}
}
override fun pause() {
mState = DecodeState.DECODING
}
override fun goOn() {
mState = DecodeState.DECODING
notifyDecode()
}
override fun seekTo(pos: Long): Long {
return 0
}
override fun seekAndPlay(pos: Long): Long {
return 0
}
override fun stop() {
mState = DecodeState.STOP
mIsRunning = false
notifyDecode()
}
override fun isDecoding(): Boolean {
return mState == DecodeState.DECODING
}
override fun isSeeking(): Boolean {
return mState == DecodeState.SEEKING
}
override fun isStop(): Boolean {
return mState == DecodeState.STOP
}
override fun setSizeListener(l: IDecoderProgress) {
}
override fun setStateListener(l: IDecoderStateListener?) {
mStateListener = l
}
override fun getWidth(): Int {
return mVideoWidth
}
override fun getHeight(): Int {
return mVideoHeight
}
override fun getDuration(): Long {
return mDuration
}
override fun getCurTimeStamp(): Long {
return mBufferInfo.presentationTimeUs / 1000
}
override fun getRotationAngle(): Int {
return 0
}
override fun getMediaFormat(): MediaFormat? {
return mExtractor?.getFormat()
}
override fun getTrack(): Int {
return 0
}
override fun getFilePath(): String {
return mFilePath
}
override fun withoutSync(): IDecoder {
mSyncRender = false
return this
}
/**
* 检查子类参数
*/
abstract fun check(): Boolean
/**
* 初始化数据提取器
*/
abstract fun initExtractor(path: String): IExtractor
/**
* 初始化子类自己特有的参数
*/
abstract fun initSpecParams(format: MediaFormat)
/**
* 配置解码器
*/
abstract fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean
/**
* 初始化渲染器
*/
abstract fun initRender(): Boolean
/**
* 渲染
*/
abstract fun render(outputBuffer: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo)
/**
* 结束解码
*/
abstract fun doneDecode()
}
- 首先定义线程相关的资源,定义变量mIsRunning来判断当前是否一直处于解码状态,还有挂起线程的mLock等
- 然后,就是定义解码相关的资源,比如创建MediaCodec,配置相关参数,还有定义输入输出缓冲,解码状态等;其中解码状态为DecodeState,音视频数据读取器定义为IExtractor。
定义解码状态
为了方便查看解码状态,使用一个枚举进行定义
js
enum class DecodeState {
/**开始状态*/
START,
/**解码中*/
DECODING,
/**解码暂停*/
PAUSE,
/**正在快进*/
SEEKING,
/**解码完成*/
FINISH,
/**解码器释放*/
STOP
}
定义音视频数据分离器 音视频文件可能来自于本地,也可能来自于服务器,为了方便验证,我们这里先拿本地的mp4文件作为原始数据,不断的喂数据给输入缓冲区,Android自带有一个音视频数据读取器MediaExtractor,需要实现的方法使用接口进行定义
js
interface IExtractor {
fun getFormat(): MediaFormat?
/**
* 读取音视频数据
*/
fun readBuffer(byteBuffer: ByteBuffer): Int
/**
* 获取当前帧时间
*/
fun getCurrentTimestamp(): Long
fun getSampleFlag(): Int
/**
* Seek到指定位置,并返回实际帧的时间戳
*/
fun seek(pos: Long): Long
fun setStartPos(pos: Long)
/**
* 停止读取数据
*/
fun stop()
}
这里面最重要的就是方法readBuffer,用于读取音视频数据流
定义解码流程
前面BaseDecoder这个解码器中含有解码流程的部分,这部分是最重要的,主要体现在Runnable的run方法中
- 【解码步骤:1、初始化、并启动解码器】
js
private fun init(): Boolean {
if (mFilePath.isEmpty() || !File(mFilePath).exists()) {
Log.w(TAG, "文件路径为空")
mStateListener?.decoderError(this, "文件路径为空")
return false
}
if (!check()) return false
//初始化数据提取器
mExtractor = initExtractor(mFilePath)
if (mExtractor == null ||
mExtractor!!.getFormat() == null) {
Log.w(TAG, "无法解析文件")
return false
}
//初始化参数
if (!initParams()) return false
//初始化渲染器
if (!initRender()) return false
//初始化解码器
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 {
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = type?.let { MediaCodec.createDecoderByType(it) }
if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
waitDecode()
}
mCodec!!.start()
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 configCodec(codec: MediaCodec, format: MediaFormat): Boolean
/**
* 初始化渲染器
*/
abstract fun initRender(): Boolean
/**
* 渲染
*/
abstract fun render(outputBuffer: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo)
/**
* 结束解码
*/
abstract fun doneDecode()
初始化MediaCodec的过程中,分为5个步骤
1、检查参数是否完整:路径是否有效等
2、初始化数据提取器:初始化Extractor
3、初始化参数:提取一些必须的参数,如duration、width、height等
4、初始化渲染器:视频不需要渲染器,音频需要AudioTracker 5、初始化解码器:初始化MediaCodec
在initCodec()方法中,
js
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
初始化MediaCodec的时候:
1、首先,通过Extractor获取到音视频数据的编码信息MediaFormat
2、然后,查询MediaFormat中的编码类型(如video/avc,即H264;audio/mp4a-latm,即AAC)
3、最后,调用createDecoderByType创建解码器
- 【解码步骤:2、将数据压入解码器输入缓冲】
看一下方法pushBufferToDecoder方法,代码如下:
js
private fun pushBufferToDecoder(): Boolean {
var inputBufferIndex = mCodec!!.dequeueInputBuffer(1000)
var isEndOfStream = false
if (inputBufferIndex >= 0) {
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
if (sampleSize < 0) {
//如果数据已经取完,压入数据结束标志:MediaCodec.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
}
这里调用了下面的方法: 1、查询缓冲是否可用,并返回缓冲的索引,其中2000代码可以等待2000ms,如果填入-1则进行无限等待。
js
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
2、通过缓冲索引inputBufferIndex获取可用的缓冲区,从Extractor媒体文件的数据存储到缓冲区中,并返回大小
js
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
3、调用queueInputBuffer将数据压入解码器
js
mCodec!!.queueInputBuffer(inputBufferIndex, 0, sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
注意:如果步骤2中的sampleSize返回-1,说明没有更多的数据了。这时候,queueInputBuffer的最后一个参数传入结束标记MediaCodec.BUFFER_FLAG_END_OF_STREAM。
- 【解码步骤:3、将解码好的数据从缓冲区拉取出来】
直接进入pullBufferFromDecoder()环节
js
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表示无限等待
js
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就是输出缓冲索引
-
【解码步骤:4、渲染】
使用抽象方法进行定义,主要给音频渲染使用的
-
【解码步骤:5、释放输出缓冲】
调用releaseOutputBuffer方法,释放输出缓冲区
注意,这个方法里面的第二个参数,是boolean类型的,这个参数在视频解码时,决定是否要将这一帧数据展示出来,也就是大家经常使用到的封面图
js
mCodec!!.releaseOutputBuffer(index, true)
- 【解码步骤:6、判断解码是否完成】
在前面的流程中,数据压入解码器之后,当没有数据的时候,sampleSize<0,此时压入一个结束标记。解码器就知道所有的数据已经接收完毕,并且在最后一帧数据加上结束标记信息,即
js
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH mStateListener?.decoderFinish(this)
}
- 【解码步骤:7、释放解码器】
在解码循环整个流程结束后,也就是代码里面的while循环结束后,释放掉所有的资源,至此,一次解码结束
js
private fun release() {
try {
Log.i(TAG, "解码停止,释放解码器")
mState = DecodeState.STOP
mIsEOS = false
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
mStateListener?.decoderDestroy(this)
} catch (e: Exception) {
}
}
解码器定义的其他方法,比如pause、goOn、stop等,不再细说,等到最后一篇文章里面把所有的源码开放