Android MediaCodec解析
1 引言
MediaCodec是Android平台提供的一个底层的音视频编解码框架,它是安卓底层多媒体基础框架的重要组成部分。它经常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack
一起使用。解码的作用,就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。反之,编码的作用,就是将非压缩的视频/音频原始数据转为视频/音频压缩编码数据。
1.1编写目的
本文档编制旨在说明和总结Android 音视频编解码相关知识,供开发人员查阅与参考。
1.2术语定义及说明
编码:编码就是将原始音频数据也就是PCM压缩的一个过程;或者是将原始的视频数据RGB或YUV压缩的一个过程。
解码:解码就是编码一个逆过程,比如将编码后的数据AAC解码成PCM给播放器播放;或者将编码后的H264数据解码成YUV或RGB给播放器渲染的过程
编解码又分为硬件编解码和软件编解码:
软编软解码:使用CPU进行编码,一般是执行代码运行算法指令编码。
硬编硬解码:使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等,一般是算法已经固化在芯片中。
一般来说,软编码会使CPU负载更重,所以性能相对比硬编要低,不过兼容性一般比硬编好,低码率下质量通常比硬编码要好一点。而硬编码一般性能比软编码好一些,但是兼容性就差一些,低码率下通常质量低于软编码的。
视频帧:视频的每一张静态图片就叫一帧
视频帧又分为I帧、B帧和P帧:
I帧:帧内编码帧,大多数情况下I帧就是关键帧,就是一个完整帧,无需任何辅助就能独立完整显示的画面。
B帧:帧是双向预测帧。参考前后图像帧编码生成。需要前面的 I/P 帧或者后面的 P 帧来协助形成一个画面。
P帧:前向预测编码帧。是一个非完整帧,通过参考前面的I帧或P帧生成画面。
所以 I 帧是很关键的存在,压缩 I 帧就可以很容易压制掉空间的大小,而压缩P帧和B帧可以压缩掉时间上的冗余信息 。
GOP:group of picture,就是两个 I 帧之间的距离,一般 GOP 设置得越大,画面的效果就会越好,到那时需要解码的时间就会越长。所以如果码率固定而 GOP 值越大,P/B帧 数量会越多,画面质量就会越高
所以在视频 seek 的时候,I 帧很关键,如果视频 seek 之后发生往前的跳动,有可能就是你要seek到的位置没用关键帧,这就需要处理了。好像Android自带的播放器就会有这个问题,有时候无法精确地seek到某个位置。
封装格式:封装格式业界也有人称音视频容器,比如我们经常看到的视频后缀名:mp4、rmvb、avi、mkv、mov等就是音视频的容器,它们将音频和视频甚至是字幕一起打包进去,封装成一个文件。
2.MediaCodec工作流程
左边是输入端,右边是输出端。其中有输入和输出端各有若干个buffer,输入端不断拿到一个空buffer,装上数据,再传入MediaCodec直到所有数据输入为止。输出端不断从MediaCodec获取到buffer,每次得到处理好的数据后,再将buffer交还给MediaCodec。
mediacodec接受三种数据格式:压缩数据、原始音频数据和原始视频数据。压缩数据一般是解码端的输入和编码端的输出,反之原始音频数据和原始视频数据一般是编码的输入和解码端的输出。
3.MediaCodec工作生命周期
MediaCodec就是一个状态机,在工作期间会经历多个状态阶段。具体来说是总共有三个大状态:Stopped, Executing ,Released,其中Stopped包含Uninitialized, Configured and Error三个小状态,Executing包含Flushed, Running and End-of-Stream三个小状态。
当MediaCodec对象实例刚创建好的时候,处于Stopped状态中的Uninitialized状态,此时需要调用configure方法,就能进入Configured状态,一个start方法的调用,此时进入Executing状态了,目前暂时处于Flushed状态,dequeueInputBuffer方法的调用返回值为bufferIndex。这也能看出api设计不人性化的地方,不是直接返回对应buffer,还要使用bufferIndex再获取一次buffer。再通过queueInputBuffer正式进入Running状态。MediaCodec工作阶段大部分时间都处于Running状态中,在Running状态不断的由input端queueInputBuffer,output端dequeueOutputBuffer,形成一个循环,直到input端加上BUFFER_FLAG_END_OF_STREAM标签,MediaCodec拿到此状态后不再接受任何新的数据输入,即进入End-of-Stream状态。调用stop此时又回到了Stopped状态中的Uninitialized状态。调用release方法来释放所有的资源进入Released状态。
中间过程可能会出现一些意外,就会进入Stopped中的Error状态,这时候有2个选择,一个是直接关门(release)进入Released状态,一个从Stopped状态中的Uninitialized状态重新开始。
4.MediaCodec代码实例
前面已经叙述了MediaCodec工作流程和工作周期状态机,下面从代码角度详细解析MediaCodec。
4.1解复用代码
复用,也可以叫做封装,即将已经压缩编码的视频数据和音频数据按照一定的格式打包到一起,比如我们都很熟悉的MP4,MKV,RMVB,TS,FLV,AVI,就是复用格式。比如FLV格式的数据,是由H.264编码的视频码流和AAC编码的音频码流打包一起。
通过MediaExtractor来获取视频的宽高:
//解复用
MediaExtractor extractor = null;
try {
extractor = new MediaExtractor();
//传入视频文件的路径
extractor.setDataSource(sourceFile.toString());
int trackIndex = selectTrack(extractor);
if (trackIndex < 0) {
throw new RuntimeException("No video track found in " + mSourceFile);
}
//选中得到的轨道(视频轨道),即后面都是对此轨道的处理
extractor.selectTrack(trackIndex);
//通过该轨道的MediaFormat得到对视频对应的宽高
MediaFormat format = extractor.getTrackFormat(trackIndex);
Log.d(TAG, "extractor.getTrackFormat format" + format);
//视频对应的宽高
mVideoWidth = format.getInteger(MediaFormat.KEY_WIDTH);
mVideoHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
if (VERBOSE) {
Log.d(TAG, "Video size is " + mVideoWidth + "x" + mVideoHeight);
}
} finally {
if (extractor != null) {
extractor.release();
}
}
通过获取到的mime类型来创建一个MediaCodec解码器:
MediaFormat format = extractor.getTrackFormat(trackIndex);
Log.d(TAG,