【Android 音视频开发打怪升级:音视频硬解码篇】二、音视频硬解码流程:封装基础解码框架

/**

  • 获取音视频对应的格式参数
    */
    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? = null

/**

  • 解码输出缓存区
    */
    protected var mOutputBuffers: Array? = 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回调方法中,集成了整个解码流程:

  • 【解码步骤:1. 初始化,并启动解码器】

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个步骤,看起很复杂,实际很简单。

  1. 检查参数是否完整:路径是否有效等

  2. 初始化数据提取器:初始化Extractor

  3. 初始化参数:提取一些必须的参数:duration,width,height等

  4. 初始化渲染器:视频不需要,音频为AudioTracker

  5. 初始化解码器:初始化MediaCodec

在initCodec()中,

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方法中

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
}
}

调用了以下方法:

  1. 查询是否有可用的输入缓冲,返回缓冲索引。其中参数2000为等待2000ms,如果填入-1则无限等待。

var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)

  1. 通过缓冲索引 inputBufferIndex 获取可用的缓冲区,并使用Extractor提取待解码数据,填充到缓冲区中。

val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)

  1. 调用queueInputBuffer将数据压入解码器。

mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)

注意:如果SampleSize返回-1,说明没有更多的数据了。

这个时候,queueInputBuffer的最后一个参数要传入结束标记MediaCodec.BUFFER_FLAG_END_OF_STREAM。

  • 【解码步骤:3. 将解码好的数据从缓冲区拉取出来】

直接进入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
}
}

第一、调用dequeueOutputBuffer方法查询是否有解码完成的可用数据,其中mBufferInfo用于获取数据帧信息,第二参数是等待时间,这里等待1000ms,填入-1是无限等待。

var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)

第二、判断index类型:

MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:输出格式改变了

MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:输入缓冲改变了

MediaCodec.INFO_TRY_AGAIN_LATER:没有可用数据,等会再来

大于等于0:有可用数据,index就是输出缓冲索引

  • 【解码步骤:4. 渲染】

这里调用了一个虚函数render,也就是将渲染交给子类

  • 【解码步骤:5. 释放输出缓冲】

调用releaseOutputBuffer方法, 释放输出缓冲区。

注:第二个参数,是个boolean,命名为render,这个参数在视频解码时,用于决定是否要将这一帧数据显示出来。

最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的24套腾讯、字节跳动、阿里、百度2020-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节

还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

一线互联网面试专题

379页的Android进阶知识大全

379页的Android进阶知识大全

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
关的我搜集整理的24套腾讯、字节跳动、阿里、百度2020-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节

还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

[外链图片转存中…(img-XMVC08GU-1715102127251)]

[外链图片转存中…(img-rdC9ayi2-1715102127252)]

[外链图片转存中…(img-Syjz2sAG-1715102127254)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值