/**
- 获取当前帧时间
*/
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)
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V:vip204888 备注Android获取(资料价值较高,非无偿)
建议
当我们出去找工作,或者准备找工作的时候,我们一定要想,我面试的目标是什么,我自己的技术栈有哪些,近期能掌握的有哪些,我的哪些短板 ,列出来,有计划的去完成,别看前两天掘金一些大佬在驳来驳去 ,他们的观点是他们的,不要因为他们的观点,膨胀了自己,影响自己的学习节奏。基础很大程度决定你自己技术层次的厚度,你再熟练框架也好,也会比你便宜的,性价比高的替代,很现实的问题但也要有危机意识,当我们年级大了,有哪些亮点,与比我们经历更旺盛的年轻小工程师,竞争。
-
无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!!!!!!!
-
准备想说怎么样写简历,想象算了,我觉得,技术就是你最好的简历
-
我希望每一个努力生活的it工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
-
有什么问题想交流,欢迎给我私信,欢迎评论
【附】相关架构及资料
内含往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术
平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!!!!!!!
-
准备想说怎么样写简历,想象算了,我觉得,技术就是你最好的简历
-
我希望每一个努力生活的it工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
-
有什么问题想交流,欢迎给我私信,欢迎评论
【附】相关架构及资料
[外链图片转存中…(img-CMtOvOT2-1711590895662)]
[外链图片转存中…(img-AIYtfOBk-1711590895662)]
内含往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术