MediaCodec相关知识

在Android中还可以通过MediaCodec播放视频

MediaCodec基本认识

MediaCodec是什么

MediaCodec类可以访问底层媒体编解码框架(StageFright或OMX),即编解码组件。这时Android low-level多媒体支持基础设施的一部分(通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface和AudioTrack一起使用)。它本身不是Codec,它通过调用底层编解码组件获得了Codec的能力

创建MediaCodec的方式

创建MediaCodec(按格式创建)
1.MediaCodec createDecoderByType(String type):创建解码器
2.MediaCodec createEncoderByType(String type):创建编码器
3.type:数据解析阶段的mimeType,如video/avc

创建MediaCodec(按Codec名字创建)
1.MediaCodec createByCodecName(String name)
2.OMX.google.h264.decoder:软解码
3.OMX.MTK.VIDEO.DECODER.AVC:硬解码

MediaCodec的工作方式

android mediacodec mpeg编码 android mediacodec详解_android

MediaCodec处理输入数据产生输出数据。当异步处理数据时,使用一组输入和输出Buffer队列。在逻辑上,客户端请求或接受数据后填入预先设定的空输入缓冲区,输入Buffer填满数据后将其传递到MediaCodec并进行编解码处理。之后MediaCodec编解码后的数据被填充到一个输出Buffer中。最后客户端请求或接收输出Buffer,消耗输出Buffer中的内容,用完后释放,给回MediaCodec重新填充输出数据

注意必须要保证输入和输出队列同时非空,即至少持有一个输入Buffer和输出Buffer才能工作

1.MediaCodec读取输入Buffer的数据,进行解码或编码,将处理完的数据填入输出Buffer
2.MediaCodec处理完一个输入Buffer的数据,将此Buffer移入客户端队列,客户端可以继续使用
3.MediaCodec将填充的输出Buffer放入客户端队列,客户端可以取出数据进行播放

MediaCodec状态周期图

android mediacodec mpeg编码 android mediacodec详解_音视频_02

在MediaCodec的生命周期内存在3种状态,即StoppedExecutingReleased。Stopped状态实际上还可处于3种子状态,即UninitializedConfiguredError,而Executing状态概念上的进展通过3个子状态进行,即FlushedRunningEnd-of-Stream

当使用工厂方法创建一个MediaCodec时,其处于未初始化状态。首先要通过configure(...)配置它,让其处于配置状态,然后调用start函数把它转变为Executing状态

在调用start函数后,MediaCodec会立刻刷新子状态,它拥有所有的Buffer。一旦第一个输入Buffer被从列中移除,MediaCodec将花费比较长的时间移动到正在运行的子状态上。当队列的输入Buffer带有end of stream标记,它会转换到end of stream子状态。这种状态下它不再接受进一步输入Buffer,但仍然生成输出Buffer,直到达到end of stream状态输出。在executing状态的任何时候调用flush函数可以回到flushed状态

调用stop函数返回的MediaCodec处于未初始化状态,因为它可能被再次配置。当再使用一个编解码器(Codec)时必须调用release函数否则会出现资源未被释放的问题。极少数情况下会遇到一个Error状态,可调用reset函数让编解码器再次可用

特定编解码格式数据

我们可以先理解为一些重要编解码信息的载体。在MediaCodec编解码过程中,当遇到AAC音频格式及MPEG-4、H.264/5视频格式时,需要设置大量的Buffer,或者将特定信息载体作为真实数据的开头。在处理这样的压缩格式,进行编解码操作前,调用start函数后,这些载体是编解码的依据,所以要立刻传给MediaCodec。当调用MediaCodec的queueInputBuffer函数时,这些数据必须使用flag:BUFFER_FLAG_CODEC_CONFIG,表明已经配置好,可以进行真正的编解码操作

特定编解码格式数据也可以包含在ByteBuffer条目的格式中传递给配置钥匙"csd-0/1"等。这些钥匙总是包含从MediaExtractor类中获取视频源的格式。当调用start函数时,特定编解码格式数据格式自动提交给编解码器,如果不包含特定编码的数据格式,你可以按照格式要求选择以正确的顺序提交到指定的缓冲区。对于H.264还可以连接所有信息载体数据并提交作为单个编解码配置缓冲区

Android使用下图所示的特定编解码格式数据缓冲区。这些也需要跟踪格式中设置适当的MediaMuxer Track配置。每个参数设置和特定编解码格式数据部分标注(*)必须以"\x00\x00\x00\x01"开始。必须注意在执行start函数之后需要马上刷新编解码器,在任何输出Buffer或输出格式被改变并返回之前,特定编解码格式数据也许会丢失在刷新过程中。在这种刷新中,要确保合适的编解码器做编解码操作不出问题,必须重新提交使用带BUFFER_FLAG_CODEC_CONFIG标志的Buffer数据

android mediacodec mpeg编码 android mediacodec详解_音视频_03

在任意有效的输出Buffer带有codec-config标志前,如果是编码过程,编码器将创建并返回特定编码的数据,如果是解码过程,解码器将解出对应的YUV数据

Codec数据处理过程

每个Codec维护的一组输入和输出Buffer都指向API调用的bufferid成功调用start函数后,客户端拥有输入和输出Buffer。在同步模式下,调用dequeueInput/OutputBuffer(...)来获得Codec的输入或输出Buffer。在异步模式下,通过MediaCodec.Callback.onInput/OutputBufferAvailable(...)回调函数将自动接收回调后的Buffer

Codec在异步模式下通过onOutputBufferAvailable回调函数将返回一个只读输出Buffer,或在同步模式下响应dequeueOutputBuffer调用。在输出Buffer被处理后,调用releaseOutputBuffer函数返回Codec的Buffer

android mediacodec mpeg编码 android mediacodec详解_数据_04

1.异步处理使用缓冲区

从5.0开始首选异步模式处理数据,处理方法是在调用configure函数之前设置一个回调。异步模式改变了状态转换,因为刷新后必须调用start函数,MediaCodec过渡到Running子状态,这时开始接受输入Buffer。同样在初始直接调用start函数后,MediaCodec进入Running状态,通过回调传递可用的输入Buffer

android mediacodec mpeg编码 android mediacodec详解_学习_05

2.同步解码和异步解码
同步方式

错误使用的原因是,开发者会主观上认为,MediaCodec的工作方式是提供一个input数据就输出一个对应的output函数。事实上MediaCodec在读入input数据之后,按照自己的逻辑编解码,可能拆分编解码数据,或者合并输出。MediaCodec的使用者应该认为input和output没有对应关系,使用者的角色是完全被动的,把Buffer的使用权交给MediaCodec

异步方式

用MediaCodec来编解码一个视频到SurfaceView显示

android mediacodec mpeg编码 android mediacodec详解_数据_06

从创建到Start过程

1.MediaCodec从创建到start过程(到JNI部分)

android mediacodec mpeg编码 android mediacodec详解_android_07

2.补充MediaCodec基本用法

使用MediaCodec的基本模式

(1)创建和配置MediaCodec对象
(2)进行以下循环:如果一个输入缓冲区准备好,读取部分数据,复制到缓冲区,如果一个输出缓冲区准备好,复制到缓冲区
(3)销毁MediaCodec对象

一个MediaCodec对象可以对特定类型的数据(如MP3或H.264)进行编码或解码。因为在原始数据上操作,所以任何文件头(比如ID3 tags)必须被剔除,MediaCodec不与任何更高层次的内容交互,所以无法通过扬声器播放音频或者从网络接收视频流。它只读入缓冲区数据,再输出到缓冲区。MediaCodec可以把大部分的外层数据去掉

必须将MediaCodec的输入处理成特定的格式,H.264视频编码输入的就是一帧数据,H.264解码指的是一个NAL单元。但你不可能一次只提交单个数据或数据在需要处理的时候才出现,这样看起来输入更像是一个流。实际上编解码器在输出前同时拥有多个Buffer

MediaCodec中的BufferInfo内部类

android mediacodec mpeg编码 android mediacodec详解_android_08

MediaCodec和MediaPlayer在很多地方有相似之处,当java层调用MediaCodec。createByCodeName、MediaCodec.recateDecoderByType、MediaCodec.createEncoderByType时,都会执行MediaCodec的构造函数,构造函数中都会调用native_setup

android mediacodec mpeg编码 android mediacodec详解_音视频_09

其中对应一段代码,相当于做了一次映射,如native_setup对应android_media_MediaCodec_native_setup等,接着进入android_media_MediaCodec_native_setup函数,主要用于构建JNI层中的MediaCodec,也就是JMediaCodec

android mediacodec mpeg编码 android mediacodec详解_android_10

创建JMediaCodec后,通过setMediaCodec设置JMediaCodec

android mediacodec mpeg编码 android mediacodec详解_数据_11

上面用到了智能指针中的成员函数incStrong和decStrong来维护引用计数器的值,这两个函数就是提供给智能指针来调用的。要注意在decStrong函数中,如果当前引用计数值为1,那么减一后就会变成0,于是就会删除这个对象。接下来看看JMediaCodec的构造函数

android mediacodec mpeg编码 android mediacodec详解_音视频_12

可以看出JMediaCodec最终执行到C++层MediaCodec的两个Create函数,通过以上几个步骤得到MediaCodec对象后,就开始执行java层调用MediaCodec.configure函数

android mediacodec mpeg编码 android mediacodec详解_android_13

获取format中的map,实际上是一个HashMap,然后遍历视频源的格式名并存放到两个数组中,再通过android_media_MediaCodec_native_configure向下传递

android mediacodec mpeg编码 android mediacodec详解_音视频_14

通过getMediaCodec获取对应的JMediaCodec,接着传入jsurface变量,表示JNI层中的Surface数据,这个jsurface变量不一定是真正的Surface(Surface实际上是一块Buffer),也有可能是SurfaceHolder(SurfaceHolder可以通过其中的getSurface获取Surface),最后调用JMediaCodec的configure函数

android mediacodec mpeg编码 android mediacodec详解_数据_15

上面最后的mCodec->configure最终会调用MediaCodec.cpp中的configure函数,构建一些编解码器。在MediaCodec.java中调用start函数后,会执行下图代码

android mediacodec mpeg编码 android mediacodec详解_学习_16

在上面的代码获取JMediaCodec后,调用JMediaCodec中的start函数,JMediaCodec中的start函数代码如下

android mediacodec mpeg编码 android mediacodec详解_数据_17

这个mCodec最后执行C++的MediaCodec的start函数。最后到达ACodec.cpp的start函数中去执行解码操作