Android MediaCodec解析

article/2025/10/6 20:18:36

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, "EgetTrackFormat format:" + format);// Create a MediaCodec decoder, and configure it with the MediaFormat from the
// extractor.  It's very important to use the format from the extractor because
// it contains a copy of the CSD-0/CSD-1 codec-specific data chunks.
String mime = format.getString(MediaFormat.KEY_MIME);
Log.d(TAG, "createDecoderByType mime:" + mime);
//通过视频mime类型初始化解码器
MediaCodec decoder = MediaCodec.createDecoderByType(mime);

此时MediaCodec处于Stopped状态中的Uninitialized状态,接下来开始启动MediaCodec

 //配置解码器,指定MediaFormat以及视频输出的Surface,解码器进入configure状态decoder.configure(format, mOutputSurface, null, 0);//启动解码器,开始进入Executing状态// Immediately after start() the codec is in the Flushed sub-state, where it holds all the buffersdecoder.start();//具体的解码流程doExtract(extractor, trackIndex, decoder, mFrameCallback);

此时MediaCodec已经启动,此时已经进入input端和output端的大循环阶段

/*** 循环工作。直到视频用完或被告知停止。*/
private void doExtract(MediaExtractor extractor, int trackIndex, MediaCodec decoder,FrameCallback frameCallback) {//获取解码输出数据的超时时间final int TIMEOUT_USEC = 0;//输入ByteBuffer数组(较高版本的MediaCodec已经用getInputBuffer取代了,可直接获取buffer)ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();//记录传入了第几块数据int inputChunk = 0;//用于log每帧解码时间long firstInputTimeNsec = -1;boolean outputDone = false;boolean inputDone = false;while (!outputDone) {if (VERBOSE) Log.d(TAG, "loop");if (mIsStopRequested) {Log.d(TAG, "Stop requested");return;}// 将更多数据馈送到解码器.if (!inputDone) {//拿到可用的ByteBuffer的indexint inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);if (inputBufIndex >= 0) {if (firstInputTimeNsec == -1) {firstInputTimeNsec = System.nanoTime();}//根据index得到对应的输入ByteBufferByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];Log.d(TAG, "decoderInputBuffers inputBuf:" + inputBuf + ",inputBufIndex:" + inputBufIndex);//从媒体文件中读取的一个sample数据大小int chunkSize = extractor.readSampleData(inputBuf, 0);if (chunkSize < 0) {//文件读到末尾,设置标志位,发送一个空帧,给后面解码知道具体结束位置decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,MediaCodec.BUFFER_FLAG_END_OF_STREAM);Log.d(TAG, "queueInputBuffer");inputDone = true;if (VERBOSE) Log.d(TAG, "sent input EOS");} else {if (extractor.getSampleTrackIndex() != trackIndex) {Log.w(TAG, "WEIRD: got sample from track " +extractor.getSampleTrackIndex() + ", expected " + trackIndex);}//得到当前数据的播放时间点long presentationTimeUs = extractor.getSampleTime();//把inputBufIndex对应的数据传入MediaCodecdecoder.queueInputBuffer(inputBufIndex, 0, chunkSize,presentationTimeUs, 0 /*flags*/);Log.d(TAG, "queueInputBuffer inputBufIndex:" + inputBufIndex);if (VERBOSE) {Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +chunkSize);}//记录传入了第几块数据inputChunk++;//extractor读取游标往前挪动extractor.advance();}} else {if (VERBOSE) Log.d(TAG, "input buffer not available");}}if (!outputDone) {//如果解码成功,则得到解码出来的数据的buffer在输出buffer中的index。并将解码得到的buffer的相关信息放在mBufferInfo中。// 如果不成功,则得到的是解码的一些状态int outputBufferIndex = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);Log.d(TAG, "dequeueOutputBuffer decoderBufferIndex:" + outputBufferIndex + ",mBufferInfo:" + mBufferInfo);if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {if (VERBOSE) Log.d(TAG, "no output from decoder available");} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {if (VERBOSE) Log.d(TAG, "decoder output buffers changed");} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {MediaFormat newFormat = decoder.getOutputFormat();if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);} else if (outputBufferIndex < 0) {throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " +outputBufferIndex);} else { // decoderStatus >= 0if (firstInputTimeNsec != 0) {// Log the delay from the first buffer of input to the first buffer// of output.long nowNsec = System.nanoTime();Log.d(TAG, "startup lag " + ((nowNsec - firstInputTimeNsec) / 1000000.0) + " ms");firstInputTimeNsec = 0;}boolean doLoop = false;if (VERBOSE) Log.d(TAG, "surface decoder given buffer " + outputBufferIndex +" (output mBufferInfo size=" + mBufferInfo.size + ")");//判断是否到了文件结束,上面设置MediaCodec.BUFFER_FLAG_END_OF_STREAM标志位在这里判断if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {if (VERBOSE) Log.d(TAG, "output EOS");if (mLoop) {doLoop = true;} else {outputDone = true;}}//如果解码得到的buffer大小大于0,则需要渲染boolean doRender = (mBufferInfo.size != 0);if (doRender && frameCallback != null) {//渲染前的回调,这里具体实现是通过一定时长的休眠来尽量确保稳定的帧率frameCallback.preRender(mBufferInfo.presentationTimeUs);}//得到输出Buffer数组,较高版本已经被getOutputBuffer代替ByteBuffer[] decoderOutputBuffers = decoder.getOutputBuffers();Log.d(TAG, "ecoderOutputBuffers.length:" + decoderOutputBuffers.length);//将输出buffer数组的第outputBufferIndex个buffer绘制到surface。doRender为true绘制到配置的surfacedecoder.releaseOutputBuffer(outputBufferIndex, doRender);if (doRender && frameCallback != null) {//渲染后的回调frameCallback.postRender();}if (doLoop) {Log.d(TAG, "Reached EOS, looping");//需要循环的话,重置extractor的游标到初始位置。extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);inputDone = false;//重置decoder到Flushed状态,不然就没法开始新一轮播放decoder.flush();    // reset decoder stateframeCallback.loopReset();}}}}
}

4.2方法解释

  1. 询问Mediacodec当前有没有可以input的Buffer可以使用:
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);

TIMEOUT_USEC为等待超时时间。当返回的inputBufIndex大于等于0,则说明当前有可用的Buffer,此时inputBufIndex表示可用Buffer在Mediacodec中的序号。如果等待了TIMEOUT_USEC时间还没找到可用的Buffer,则返回inputBufIndex小于0,等下次循环再来取Buffer。

  1. 每次从MediaExtractor中的readSampleData方法读出视频一段数据放在ByteBuffer中,然后通过Mediacodec的queueInputBuffer将ByteBuffer传给Mediacodec内部处理。

    //从媒体文件中读取的一个sample数据大小到inputBuf中
    int chunkSize = extractor.readSampleData(inputBuf, 0);
    

    readSampleData方法是读取一帧的数据。返回值为读取到数据大小,所以如果返回值大于0,则说明是有读取到数据的,则将数据传入MediaCodec中:

//得到当前数据的播放时间点
long presentationTimeUs = extractor.getSampleTime();
//把inputBufIndex对应的数据传入MediaCodec
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,presentationTimeUs, 0 /*flags*/);

如果readSampleData方法返回值,即读到的数据大小为负数,则说明已经读到视频文件尾部了,则还是调用queueInputBuffer方法,但是需要特殊处理:

decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,MediaCodec.BUFFER_FLAG_END_OF_STREAM);

发送一个空帧,标志位传BUFFER_FLAG_END_OF_STREAM,告诉MediaCodec,已经到文件尾部了,这个文件没有剩下需要传的数据了。

input端的代码就到这,然后立刻到ouptut端去尝试获取一下output的buffer。

int outputBufferIndex = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);

如果不成功,则得到的是解码的一些状态,以下几种常见 的状态:

1.MediaCodec.INFO_TRY_AGAIN_LATER:表示等了TIMEOUT_USEC时间长,也暂时还没有解码出成功的数据。一般来说,一个是等待时间还不够,另一个就是输入端是B帧,需要后面一帧P帧来作为参考帧才可以解码。

2.MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:输出Buffer数组已经过时,需要及时更换,由于较新版本已经用getOutputBuffer获取输出Buffer了,所以该标志位也过时了。

3.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:输出数据的MediaFormat发生了变化。

如果解码成功,则得到解码出来的数据的buffer在输出buffer中的index。并将解码得到的buffer的相关信息放在mBufferInfo中。然后执行非常关键的一段代码:

decoder.releaseOutputBuffer(outputBufferIndex, doRender);

将输出buffer数组的第outputBufferIndex个buffer绘制到surface(还记得configure方法传了的Surface对象么)。doRender为true,绘制到配置的surface。可以理解这行代码就类似Android中Canvas的draw方法,调用就绘制一帧,并将Buffer回收。


http://chatgpt.dhexx.cn/article/CpPe48OX.shtml

相关文章

Android原生编解码接口MediaCodec详解

作者&#xff1a;躬行之 了解了音视频的相关知识&#xff0c;可以先阅读同系列文章&#xff1a; 音视频开发基础知识音频帧、视频帧及其同步Camera2、MediaCodec录制mp4 MediaCodec 是 Android 中的编解码器组件&#xff0c;用来访问底层提供的编解码器&#xff0c;通常与 Me…

MediaCodec视频解码流程详解及参考demo

一、MediaCodec简介 MediaCodec是Android自带的底层多媒体支持架构的一部分&#xff08;通常与 MediaExtractor&#xff0c;MediaSync&#xff0c;MediaMuxer&#xff0c;MediaCrypto&#xff0c;MediaDrm&#xff0c;Image&#xff0c;Surface 和 AudioTrack 一起使用&#xf…

Android MediaCodec简单总结

#.MedaiCodec简介 MediaCodec是Android中提供的音视频编码、解码工具。它主要是完成上层接口的封装&#xff0c;提供给开发者使用&#xff0c;编解码功能实际是在native底层服务中完成的。 #.MediaCodec工作的宏观流程&#xff1a; ##.包换两个缓冲区队列 一个输入缓冲区队列&a…

软件测试面试指导之自我介绍

面试自我介绍虽然人人都准备&#xff0c;但是做到让人印象深刻可不容易啊。 本篇就具体来聊聊人人都要经历的面试&#xff0c;怎么做自我介绍&#xff0c;才能让你在面试官的眼睛里像金子一样闪闪发光&#xff1f; 面试是什么&#xff1f; 它是个机会&#xff0c;让面试官更…

软件测试面试要注意的细节以及处理(自我介绍篇)

面试问题第一问&#xff0c;95%都会是&#xff1a; 请简单的做个自我介绍吧~ 分以下几点说明。 一、个人的基本信息&#xff0c;扬长避短 1、年纪太大与太小&#xff0c;都不需要主动去说明。 比如我年纪只有21岁 例子&#xff1a;面试官您好&#xff0c;我叫***&#xff…

【软件测试】企业测试面试题9道,从自我介绍到项目考察+回答......

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、自我介绍 您好…

软件测试面试怎样介绍自己的项目?会问到什么程度?

最近收到很多粉丝的私信说找不到工作&#xff0c;简历投了百十来份&#xff0c;邀约都没几个&#xff0c;更别说offer了&#xff0c;是不是软件测试要黄了&#xff1f; 说句实话&#xff0c;现在大环境确实不好&#xff0c;互联网大厂裁员这是摆在明面上的原因。时代的一粒沙&…

软件测试面试怎样介绍自己的测试项目?会问到什么程度?

想知道面试时该怎样介绍测试项目&#xff1f;会问到什么程度&#xff1f;那就需要换位思考&#xff0c;思考HR在这个环节想知道什么。 HR在该环节普遍想获得的情报主要是下面这2个方面&#xff1a; 1&#xff09;应聘者的具体经验和技术能力&#xff0c; 2&#xff09;应聘者的…

【软件测试】面试中介绍项目你该这么说!

黑马程序员视频库 播妞微信号&#xff1a;heiniu526 传智播客旗下互联网资讯、学习资源免费分享平台 测试人员在找工作的过程中&#xff0c;通常有一个问题是很难绕开的。就是要如何向别人介绍自己之前做过的项目。下面我们就这个问题简单的做一些分析。 大体上可以分为如下几个…

软件测试面试自我介绍/项目介绍居然还有模板?我要是早点发现就好了

目录 1、自我介绍 2、项目介绍 2.1、最全电商项目介绍 2.2、电商项目介绍 2.3、在线教育项目介绍 2.4、互联网金融项目介绍 总结 1、自我介绍 以XXX简历来举例&#xff08;参照下面的案例&#xff0c;编写你的自我介绍&#xff0c;框架就是&#xff1a;我是谁&#xff0…

软件测试面试,如何自我介绍?如何介绍项目?如何介绍个人技术?(提供面试话术)

前准备不足而导致面试失败那可就亏大了&#xff01;为了提高面试成功率&#xff0c;帮助大家尽快拿到高薪offer&#xff0c;我们盘点了面试环节必问的三类问题&#xff0c;希望对即将参加软件测试面试的小伙伴们有所帮助。 01 如何自我介绍 面试过程中一定要放慢语速&#xf…

软件测试面试,如何自我介绍?

01 如何自我介绍 面试过程中一定要放慢语速&#xff0c;做到条理清晰。特别是做自我介绍时&#xff0c;可以适当多介绍自己会什么&#xff0c;有哪些重要经验。 例如&#xff1a; 面试官&#xff0c;上午/下午好。 我是XXX&#xff0c;今天来面试贵公司的软件测试工程师岗位&a…

软件测试工程师面试如何做好自我介绍?

听了很多提问者和我的学生&#xff0c;在做自我自我介绍的时候&#xff0c;一般存在的问题&#xff1a; 1、表述不太流畅。多练习表述&#xff0c;自己录音&#xff0c;听回放&#xff0c;有问题改正。 2、表述太溜&#xff0c;语速太快。不自信表现&#xff0c;隐含紧张的情绪…

软件测试三分钟自我介绍

目录 一、个人的基本信息&#xff0c;扬长避短 二、突出自己的工作经验 三、突出自己的技能 四、个人兴趣爱好与结尾 自我介绍在面试中常常作为第一个问题而出现&#xff0c;好的自我介绍可以带来良好的第一印象&#xff0c;如何讲好自我介绍呢。我建议从三部分下手 一、个…

软件测试面试指导之自我介绍 (干货)

面试自我介绍虽然人人都准备&#xff0c;但是做到让人印象深刻可不容易啊。 本篇就具体来聊聊人人都要经历的面试&#xff0c;怎么做自我介绍&#xff0c;才能让你在面试官的眼睛里像金子一样闪闪发光&#xff1f; 面试是什么&#xff1f; 它是个机会&#xff0c;让面试官更…

软件测试面试必考题:自我介绍

面试问题第一问&#xff0c;95%都会是&#xff1a; 请简单的做个自我介绍吧~ 分以下几点说明。 一、个人的基本信息&#xff0c;扬长避短 1、年纪太大与太小&#xff0c;都不需要主动去说明。 比如我年纪只有21岁 例子&#xff1a;面试官您好&#xff0c;我叫***&#xff0…

软件测试工程师自我介绍(范本)

一、先介绍自己的基本信息&#xff0c;要注意扬长避短 1、年纪太大与太小&#xff0c;就不要主动去说明。 比如你的年纪只有20岁 例子&#xff1a;我叫***&#xff0c;从事软件测试工作有几年了。 2、不是计算机相关专业毕业的也不要过多的去提 比如你的专业是机械专业 例…

软件测试面试01:自我介绍

2.1 简单的自我介绍下 面试宫&#xff0c;您好&#xff0c;我叫XXX&#xff0c;来自于XXXX&#xff0c;目前从事软件测试工作&#xff0c;已经三年工作经验&#xff0c;个人性格&#xff0c;比较开朗&#xff0c;跟人关系比较好&#xff0c;做事也比较细心三年测试工作经验中&…

IntelliJ IDEA 打jar包后,运行没有反应

问题现象&#xff1a; 双击没有反应&#xff0c;CMD下运行java -jar xxx.jar 后提示:xxx.jar中没有主清单属性 问题分析: 1.可能是没有maven插件 spring-boot-maven-plugin。 2. 没有指定入口&#xff08;main方法&#xff09;&#xff0c;或者指定错误。 解决方法&#xf…

IDEA中如何导入jar包

IDEA中如何导入jar包&#xff0c;有两种方法。 此处的Directory eclipse中的folder 方法一&#xff1a; 方法二&#xff1a;