游戏陪玩源码开发,音视频硬解码流程-封装基础解码框架
既然在游戏陪玩源码开发时使用了实时音视频技术,那就涉及到了音视频传输流程,其中音视频数据的编解码是很关键的一个环节,所以接下来我们要了解的就是音视频硬解码的相关流程,封装基础解码框架。
定义解码器
因此,我们将整个解码流程抽象为一个解码基类: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回调方法中,集成了整个解码流程:
【游戏陪玩源码解码步骤: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个步骤,看起很复杂,实际很简单。
检查参数是否完整:路径是否有效等
初始化数据提取器:初始化Extractor
初始化参数:提取一些必须的参数:duration,width,height等
初始化渲染器:视频不需要,音频为AudioTracker
初始化解码器:初始化MediaCodec
在initCodec()中,
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
初始化MediaCodec的时候:
首先,通过Extractor获取到游戏陪玩源码音视频数据的编码信息MediaFormat;
然后,查询MediaFormat中的编码类型(如video/avc,即H264;audio/mp4a-latm,即AAC);
最后,调用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 } }
调用了以下方法:
查询是否有可用的输入缓冲,返回缓冲索引。其中参数2000为等待2000ms,如果填入-1则无限等待。
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
通过缓冲索引 inputBufferIndex 获取可用的缓冲区,并使用Extractor提取待解码数据,填充到缓冲区中。
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
调用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,这个参数在视频解码时,用于决定是否要将这一帧数据显示出来。
mCodec!!.releaseOutputBuffer(index, true)
【解码步骤:6. 判断解码是否完成】
还记得我们在把数据压入解码器时,当sampleSize < 0 时,压入了一个结束标记吗?
当接收到这个标志后,游戏陪玩源码中的解码器就知道所有数据已经接收完毕,在所有数据解码完成以后,会在最后一帧数据加上结束标记信息,即
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { mState = DecodeState.FINISH mStateListener?.decoderFinish(this) }
【解码步骤:7. 释放解码器】
在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等)不再细说,可查看工程源码。以上就是游戏陪玩源码开发,音视频硬解码流程-封装基础解码框架的全部内容了,希望对大家有帮助。
本文转载自网络,转载仅为分享干货知识,如有侵权欢迎联系云豹科技进行删除处理
链接:https://juejin.cn/post/6844903952165634055