目录

Android-音视频开发打怪升级音视频硬解码篇二音视频硬解码流程封装基础解码框架1

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

【附】相关架构及资料

源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,和技术大牛一起讨论交流解决问题。

https://i-blog.csdnimg.cn/blog_migrate/512ddf3c999b41155f236511d945dfe5.png

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 其中,有一个解码状态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,这个参数在视频解码时,用于决定是否要将这一帧数据显示出来。

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)

如何成为Android高级架构师!

架构师必须具备抽象思维和分析的能力,这是你进行系统分析和系统分解的基本素质。只有具备这样的能力,架构师才能看清系统的整体,掌控全局,这也是架构师大局观的形成基础。 你如何具备这种能力呢?一是来自于经验,二是来自于学习。

架构师不仅要具备在问题领域上的经验,也需要具备在软件工程领域内的经验。也就是说,架构师必须能够准确得理解需求,然后用软件工程的思想,把需求转化和分解成可用计算机语言实现的程度。经验的积累是需要一个时间过程的,这个过程谁也帮不了你,是需要你去经历的。

但是,如果你有意识地去培养,不断吸取前人的经验的话,还是可以缩短这个周期的。这也是我整理架构师进阶此系列的始动力之一。


成为Android架构师必备知识技能

https://i-blog.csdnimg.cn/blog_migrate/0b206a3953aacd85ace2bac34bacb330.png

对应导图的学习笔记(由阿里P8大牛手写,我负责整理成PDF笔记)

https://i-blog.csdnimg.cn/blog_migrate/57d50f2bdcc4bafcd8798fd9f6e91050.png

部分内容展示

《设计思想解读开源框架》

  • 目录

    https://i-blog.csdnimg.cn/blog_migrate/7452893dd2122847fb0e0d98a2d19a24.png

  • 热修复设计

    https://i-blog.csdnimg.cn/blog_migrate/b6769886554c29992ff4b28cf39d9f08.png

  • 插件化框架设计

    https://i-blog.csdnimg.cn/blog_migrate/d95fc2590377429b594368d4bb99b48f.png

    《360°全方面性能优化》

    https://i-blog.csdnimg.cn/blog_migrate/da464097ac489e013149fa4a2a86857b.png

  • 设计思想与代码质量优化

    https://i-blog.csdnimg.cn/blog_migrate/57d4a8bf907c754bccb787b7bfa73aec.png

  • 程序性能优化

    https://i-blog.csdnimg.cn/blog_migrate/71bf2695b61eafac55886ee54ea5d8a3.png

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

]

  • 插件化框架设计

    [外链图片转存中…(img-hNJCKrSe-1715272409414)]

    《360°全方面性能优化》

    [外链图片转存中…(img-zWYonn4w-1715272409414)]

  • 设计思想与代码质量优化

    [外链图片转存中…(img-MI4Oq1u4-1715272409414)]

  • 程序性能优化

    [外链图片转存中…(img-pBQeMPCV-1715272409415)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!