热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

目录一、Android音视频硬解码篇:1,音视频基础知识2,音视频硬解码流程:封装基础解码框架3,音视频播放&



目录


一、Android音视频硬解码篇:

  • 1,音视频基础知识

  • 2,音视频硬解码流程:封装基础解码框架

  • 3,音视频播放:音视频同步

  • 4,音视频解封和封装:生成一个MP4


二、使用OpenGL渲染视频画面篇

  • 1,初步了解OpenGL ES

  • 2,使用OpenGL渲染视频画面

  • 3,OpenGL渲染多视频,实现画中画

  • 4,深入了解OpenGL之EGL

  • 5,OpenGL FBO数据缓冲区

  • 6,Android音视频硬编码:生成一个MP4


三、Android FFmpeg音视频解码篇

  • 1,FFmpeg so库编译

  • 2,Android 引入FFmpeg

  • 3,Android FFmpeg视频解码播放

  • 4,Android FFmpeg+OpenSL ES音频解码播放

  • 5,Android FFmpeg+OpenGL ES播放视频

  • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装

  • 7,Android FFmpeg视频编码




本文你可以了解到



本文主要简介Android使用硬解码API实现硬解码的流程,包含MediaCodec输入输出缓冲、MediaCodec解码流程、解码代码封装和讲解。



一、简介


MediaCodec 是Android 4.1(api 16)版本引入的编解码接口,同时支持音视频的编码和解码。



一定要好好理解接下来这两幅图,因为后续的代码就是基于这两幅图来编写的。



数据流

首先,来看看MediaCodec的数据流,也是官方Api文档中的,很多文章都会引用。



仔细看一下,MediaCodec将数据分为两部分,分别为input(左边)和output(右边),即输入和输出两个数据缓冲区。


input:是给客户端输入需要解码的数据(解码时)或者需要编码的数据(编码时)。


output:是输出解码好(解码时)或者编码好(编码时)的数据给客户端。



MediaCodec内部使用异步的方式对input和output数据进行处理。MediaCodec将处理好input的数据,填充到output缓冲区,交给客户端渲染或处理



注:客户端处理完数据后,必须手动释放output缓冲区,否则将会导致MediaCodec输出缓冲被占用,无法继续解码。


状态

依然是一副来自官方的状态图



再仔细看看这幅图,整体上分为三个大的状态:Sotpped、Executing、Released。


  • Stoped:包含了3个小状态:Error、Uninitialized、Configured。


首先,新建MediaCodec后,会进入Uninitialized状态;
其次,调用configure方法配置参数后,会进入Configured;

  • Executing:同样包含3个小状态:Flushed、Running、End of Stream。


再次,调用start方法后,MediaCodec进入Flushed状态;
接着,调用dequeueInputBuffer方法后,进入Running状态;
最后,当解码/编码结束时,进入End of Stream(EOF)状态。
这时,一个视频就处理完成了。

  • Released:最后,如果想结束整个数据处理过程,可以调用release方法,释放所有的资源。


那么,Flushed是什么状态呢?


从图中我们可以看到,在Running或者End of Stream状态时,都可以调用flush方法,重新进入Flushed状态。


当我们在解码过程中,进入了End of Stream后,解码器就不再接收输入了,这时候,需要调用flush方法,重新进入接收数据状态。


或者,我们在播放视频过程中,想进行跳播,这时候,我们需要Seek到指定的时间点,这时候,也需要调用flush方法,清除缓冲,否则解码时间戳会混乱。



再次强调一下,一定要好好理解这两幅图,因为后续的代码就是基于这两幅图来编写的。



二、解码流程


MediaCodec有两种工作模式,分别为异步模式和同步模式,这里我们使用同步模式,异步模式可以参考官网例子。


根据官方的数据流图和状态图,画出一个最基础的解码流程如下:



经过初始化和配置以后,进入循环解码流程,不断的输入数据,然后获取解码完数据,最后渲染出来,直到所有数据解码完成(End of Stream)。


三、开始解码


根据上面的流程图,可以发现,无论音频还是视频,解码流程基本是一致的,不同的地方只在于【配置】、【渲染】两个部分。


定义解码器

因此,我们将整个解码流程抽象为一个解码基类: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? = 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) {
//如果数据已经取完&#xff0c;压入数据结束标志&#xff1a;BUFFER_FLAG_END_OF_STREAM
mCodec!!.queueInputBuffer(inputBufferIndex, 0, 0,
0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEndOfStream &#61; true
} else {
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
}
}
return isEndOfStream
}
}

调用了以下方法&#xff1a;


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


var inputBufferIndex &#61; mCodec!!.dequeueInputBuffer(2000)

  1. 通过Extractor获取缓冲区&#xff0c;并往缓冲区填充数据


val inputBuffer &#61; mInputBuffers!![inputBufferIndex]
val sampleSize &#61; mExtractor!!.readBuffer(inputBuffer)

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


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


注意&#xff1a;如果SampleSize返回-1&#xff0c;说明没有更多的数据了。
这个时候&#xff0c;queueInputBuffer的最后一个参数要传入结束标记MediaCodec.BUFFER_FLAG_END_OF_STREAM。



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


直接进入pullBufferFromDecoder()


abstract class BaseDecoder: IDecoder {
//省略上面已有代码
......

private fun pullBufferFromDecoder(): Int {
// 查询是否有解码完成的数据&#xff0c;index >&#61;0 时&#xff0c;表示数据有效&#xff0c;并且index为缓冲区索引
var index &#61; mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
when (index) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {}
MediaCodec.INFO_TRY_AGAIN_LATER -> {}
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
mOutputBuffers &#61; mCodec!!.outputBuffers
}
else -> {
return index
}
}
return -1
}
}

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


var index &#61; mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)

第二、判断index类型&#xff1a;


MediaCodec.INFO_OUTPUT_FORMAT_CHANGED&#xff1a;输出格式改变了


MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED&#xff1a;输入缓冲改变了


MediaCodec.INFO_TRY_AGAIN_LATER&#xff1a;没有可用数据&#xff0c;等会再来


大于等于0&#xff1a;有可用数据&#xff0c;index就是输出缓冲索引


  • 【解码步骤&#xff1a;4. 渲染】


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


  • 【解码步骤&#xff1a;5. 释放输出缓冲】


调用releaseOutputBuffer方法&#xff0c; 释放输出缓冲区。



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



mCodec!!.releaseOutputBuffer(index, true)

  • 【解码步骤&#xff1a;6. 判断解码是否完成】


还记得我们在把数据压入解码器时&#xff0c;当sampleSize <0 时&#xff0c;压入了一个结束标记吗&#xff1f;


当接收到这个标志后&#xff0c;解码器就知道所有数据已经接收完毕&#xff0c;在所有数据解码完成以后&#xff0c;会在最后一帧数据加上结束标记信息&#xff0c;即


if (mBufferInfo.flags &#61;&#61; MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState &#61; DecodeState.FINISH
mStateListener?.decoderFinish(this)
}

  • 【解码步骤&#xff1a;7. 释放解码器】


在while循环结束后&#xff0c;释放掉所有的资源。至此&#xff0c;一次解码结束。


abstract class BaseDecoder: IDecoder {
//省略上面已有代码
......

private fun release() {
try {
mState &#61; DecodeState.STOP
mIsEOS &#61; false
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
mStateListener?.decoderDestroy(this)
} catch (e: Exception) {
}
}
}

最后&#xff0c;解码器定义的其他方法&#xff08;如pause、goOn、stop等&#xff09;不再细说&#xff0c;可查看工程源码。


结尾


本来打算把音频和视频播放部分也放到本篇来讲&#xff0c;最后发现篇幅太长&#xff0c;不利于阅读&#xff0c;看了会累。所以把真正实现播放部分和下一篇【音视频播放&#xff1a;音视频同步】做一个整合&#xff0c;内容和长度都会更合理。




技术交流&#xff0c;欢迎加我微信&#xff1a;ezglumes &#xff0c;拉你入技术交流群。



推荐阅读&#xff1a;


音视频面试基础题


OpenGL ES 学习资源分享


一文读懂 YUV 的采样与格式


OpenGL 之 GPUImage 源码分析


推荐几个堪称教科书级别的 Android 音视频入门项目


觉得不错&#xff0c;点个在看呗~





推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
author-avatar
文逸博166293
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有