1. MediaCodec工作原理

 MediaCodec类Android提供的用于访问低层多媒体编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor、MediaMuxer、AudioTrack结合使用,能够编解码诸如H.264、H.265、AAC、3gp等常见的音视频格式。广义而言,MediaCodec的工作原理就是处理输入数据以产生输出数据。具体来说,MediaCodec在编解码的过程中使用了一组输入/输出缓存区来同步或异步处理数据:首先,客户端向获取到的编解码器输入缓存区写入要编解码的数据并将其提交给编解码器,待编解码器处理完毕后将其转存到编码器的输出缓存区,同时收回客户端对输入缓存区的所有权;然后,客户端从获取到编解码输出缓存区读取编码好的数据进行处理,待处理完毕后编解码器收回客户端对输出缓存区的所有权。不断重复整个过程,直至编码器停止工作或者异常退出。

2. MediaCodec编码过程

 在整个编解码过程中,MediaCodec的使用会经历配置、启动、数据处理、停止、释放几个过程,相应的状态可归纳为停止(Stopped),执行(Executing)以及释放(Released)三个状态,而Stopped状态又可细分为未初始化(Uninitialized)、配置(Configured)、异常( Error),Executing状态也可细分为读写数据(Flushed)、运行(Running)和流结束(End-of-Stream)。MediaCodec整个状态结构图如下:

 从上图可知,当MediaCodec被创建后会进入未初始化状态,待设置好配置信息并调用start()启动后,MediaCodec会进入运行状态,并且可进行数据读写操作。如果在这个过程中出现了错误,MediaCodec会进入Stopped状态,我们就是要使用reset方法来重置编解码器,否则MediaCodec所持有的资源最终会被释放。当然,如果MediaCodec正常使用完毕,我们也可以向编解码器发送EOS指令,同时调用stop和release方法终止编解码器的使用。

免费学习地址:https://ke.qq.com/course/3202131?flowToken=1042495

【文章福利】免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

2.1 创建编/解码器

 MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)两个方法来创建编解码器,它们均需要传入一个MIME类型多媒体格式。常见的MIME类型多媒体格式如下:

● "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
● "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
● "video/avc" - H.264/AVC video
● "video/mp4v-es" - MPEG4 video
● "video/3gpp" - H.263 video
● "audio/3gpp" - AMR narrowband audio
● "audio/amr-wb" - AMR wideband audio
● "audio/mpeg" - MPEG1/2 audio layer III
● "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
● "audio/vorbis" - vorbis audio
● "audio/g711-alaw" - G.711 alaw audio
● "audio/g711-mlaw" - G.711 ulaw audio

 当然,MediaCodec还提供了一个createByCodecName (String name)方法,支持使用组件的具体名称来创建编解码器。但是该方法使用起来有些麻烦,且官方是建议最好是配合MediaCodecList使用,因为MediaCodecList记录了所有可用的编解码器。当然,我们也可以使用该类对传入的minmeType参数进行判断,以匹配出MediaCodec对该mineType类型的编解码器是否支持。以指定MIME类型为“video/avc”为例,代码如下:

 private static MediaCodecInfo selectCodec(String mimeType) {// 获取所有支持编解码器数量int numCodecs = MediaCodecList.getCodecCount();for (int i = 0; i < numCodecs; i++) {// 编解码器相关性信息存储在MediaCodecInfo中MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);// 判断是否为编码器if (!codecInfo.isEncoder()) {continue;}// 获取编码器支持的MIME类型,并进行匹配String[] types = codecInfo.getSupportedTypes();for (int j = 0; j < types.length; j++) {if (types[j].equalsIgnoreCase(mimeType)) {return codecInfo;}}}return null;}

2.2 配置、启动编/解码器

 编解码器配置使用的是MediaCodec的configure方法,该方法首先对MediaFormat存储的数据map进行提取,然后调用本地方法native_configure实现对编解码器的配置工作。在配置时,configure方法需要传入format、surface、crypto、flags参数,其中format为MediaFormat的实例,它使用"key-value"键值对的形式存储多媒体数据格式信息;surface用于指明解码器的数据源来自于该surface;crypto用于指定一个MediaCrypto对象,以便对媒体数据进行安全解密;flags指明配置的是编码器(CONFIGURE_FLAG_ENCODE)。

MediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480);     // 创建MediaFormat
mFormat.setInteger(MediaFormat.KEY_BIT_RATE,600);       // 指定比特率
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);  // 指定帧率
mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat);  // 指定编码器颜色格式
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,10); // 指定关键帧时间间隔
mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE); 

 以上代码是在编码H.264时的配置方法,createVideoFormat("video/avc", 640 ,480)为"video/avc"类型(即H.264)编码器的MediaFormat对象,需要指定视频数据的宽高,如果编解码音频数据,则调用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)的方法。除了一些诸如视频帧率、音频采样率等配置参数,这里需要着重讲解一下MediaFormat.KEY_COLOR_FORMAT配置属性,该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。比如,我们都知道Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:

       原始数据                                      编码器
NV12(YUV420sp) --------->  COLOR_FormatYUV420PackedSemiPlanar
NV21            ----------> COLOR_FormatYUV420SemiPlanar
YV12(I420)          ----------> COLOR_FormatYUV420Planar

 当编解码器配置完毕后,就可以调用MediaCodec的start()方法,该方法会调用低层native_start()方法来启动编码器,并调用低层方法ByteBuffer[] getBuffers(input)来开辟一系列输入、输出缓存区。start()方法源码如下:

public final void start() {native_start();synchronized(mBufferLock) {cacheBuffers(true /* input */);cacheBuffers(false /* input */);}}

2.3 数据处理

 MediaCodec支持两种模式编解码器,即同步synchronous、异步asynchronous,所谓同步模式是指编解码器数据的输入和输出是同步的,编解码器只有处理输出完毕才会再次接收输入数据;而异步编解码器数据的输入和输出是异步的,编解码器不会等待输出数据处理完毕才再次接收输入数据。这里,我们主要介绍下同步编解码,因为这种方式我们用得比较多。我们知道当编解码器被启动后,每个编解码器都会拥有一组输入和输出缓存区,但是这些缓存区暂时无法被使用,只有通过MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法获取输入输出缓存区授权,通过返回的ID来操作这些缓存区。下面我们通过一段官方提供的代码,进行扩展分析:

 MediaCodec codec = MediaCodec.createByCodecName(name);codec.configure(format, …);MediaFormat outputFormat = codec.getOutputFormat(); // option Bcodec.start();for (;;) {int inputBufferId = codec.dequeueInputBuffer(timeoutUs);if (inputBufferId >= 0) {ByteBuffer inputBuffer = codec.getInputBuffer(…);// fill inputBuffer with valid data…codec.queueInputBuffer(inputBufferId, …);}int outputBufferId = codec.dequeueOutputBuffer(…);if (outputBufferId >= 0) {ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A// bufferFormat is identical to outputFormat// outputBuffer is ready to be processed or rendered.…codec.releaseOutputBuffer(outputBufferId, …);} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {// Subsequent data will conform to new format.// Can ignore if using getOutputFormat(outputBufferId)outputFormat = codec.getOutputFormat(); // option B}}codec.stop();codec.release();

 从上面代码可知,当编解码器start后,会进入一个for(;;)循环,该循环是一个死循环,以实现不断地去从编解码器的输入缓存池中获取包含数据的一个缓存区,然后再从输出缓存池中获取编解码好的输出数据。

  • 获取编解码器的输入缓存区,写入数据

 首先,调用MediaCodec的dequeueInputBuffer(long timeoutUs)方法从编码器的输入缓存区集合中获取一个输入缓存区,并返回该缓存区的下标index,如果index=-1说明暂时可用缓存区,当timeoutUs=0时dequeueInputBuffer会立马返回。接着调用MediaCodec的getInputBuffer(int index),该方法会将index传入给本地方法getBuffer(true /* input */, index)返回该缓存区的ByteBuffer,并且将获得的ByteBuffer对象及其index存储到BufferMap对象中,以便输入结束后对该缓存区作释放处理,交还给编解码器。getInputBuffer(int index)源码如下:

    @Nullablepublic ByteBuffer getInputBuffer(int index) {ByteBuffer newBuffer = getBuffer(true /* input */, index);synchronized(mBufferLock) {invalidateByteBuffer(mCachedInputBuffers, index);// mDequeuedInputBuffers是BufferMap的实例mDequeuedInputBuffers.put(index, newBuffer);}return newBuffer;}

 然后,在获得输入缓冲区后,将数据填入数据并使用queueInputBuffer将其提交到编解码器中处理,同时将输入缓存区释放交还给编解码器。queueInputBuffer源码如下:

    public final void queueInputBuffer(int index,int offset, int size, long presentationTimeUs, int flags)throws CryptoException {synchronized(mBufferLock) {invalidateByteBuffer(mCachedInputBuffers, index);// 移除输入缓存区mDequeuedInputBuffers.remove(index);}try {native_queueInputBuffer(index, offset, size, presentationTimeUs, flags);} catch (CryptoException | IllegalStateException e) {revalidateByteBuffer(mCachedInputBuffers, index);throw e;}}

 由上述代码可知,queueInputBuffer主要通过调用低层方法native_queueInputBuffer实现,该方法需要传入5个参数,其中index是输入缓存区的下标,编解码器就是通过index找到缓存区的位置;offset为有效数据存储在buffer中的偏移量;size为有效输入原始数据的大小;presentationTimeUs为缓冲区显示时间戳,通常为0;flags为输入缓存区标志,通常设置为 BUFFER_FLAG_END_OF_STREAM。

  • 获取编解码器的输出缓存区,读出数据

 首先,与上述通过dequeueInputBuffer和getInputBuffer获取输入缓存区类似,MediaCodec也提供了dequeueOutputBuffer和getOutputBuffer方法用来帮助我们获取编解码器的输出缓存区。但是与dequeueInputBuffer不同的是,dequeueOutputBuffer还需要传入一个MediaCodec.BufferInfo对象。MediaCodec.BufferInfo是MediaCodec的一个内部类,它记录了编解码好的数据在输出缓存区中的偏移量和大小。

  public final static class BufferInfo {public void set(int newOffset, int newSize, long newTimeUs, @BufferFlag int newFlags) {offset = newOffset;size = newSize;presentationTimeUs = newTimeUs;flags = newFlags;}public int offset // 偏移量public int size;  // 缓存区有效数据大小public long presentationTimeUs; // 显示时间戳public int flags;                   // 缓存区标志@NonNullpublic BufferInfo dup() {BufferInfo copy = new BufferInfo();copy.set(offset, size, presentationTimeUs, flags);return copy;}};

 然后,通过dequeueOutputBuffer的源码可知,当dequeueOutputBuffer返回值>=0时,输出缓存区的数据才是有效的。当调用本地方法native_dequeueOutputBuffer返回INFO_OUTPUT_BUFFERS_CHANGED时,会调用cacheBuffers方法重新获取一组输出缓存区mCachedOutputBuffers(ByteBuffer[])。这就解释了如果我们使用getOutputBuffers方法(API21后被弃用,使用getOutputBuffer(index)代替)来获取编解码器的输出缓存区,那么就需要在调用dequeueOutputBuffer判断其返回值,如果返回值为MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED,则需要重新获取输出缓存区集合。此外,这里还要dequeueOutputBuffer的另外两个返回值:MediaCodec.INFO_TRY_AGAIN_LATER、MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,前者表示获取编解码器输出缓存区超时,后者表示编解码器数据输出格式改变,随后输出的数据将使用新的格式。因此,我们需要在调用dequeueOutputBuffer判断返回值是否为INFO_OUTPUT_FORMAT_CHANGED,需要通过MediaCodec的getOutputFormat重新设置MediaFormt对象。

  public final int dequeueOutputBuffer(@NonNull BufferInfo info, long timeoutUs) {int res = native_dequeueOutputBuffer(info, timeoutUs);synchronized(mBufferLock) {if (res == INFO_OUTPUT_BUFFERS_CHANGED) {// 将会调用getBuffers()底层方法cacheBuffers(false /* input */);} else if (res >= 0) {validateOutputByteBuffer(mCachedOutputBuffers, res, info);if (mHasSurface) {mDequeuedOutputInfos.put(res, info.dup());}}}return res;}

 最后,当输出缓存区的数据被处理完毕后,通过调用MediaCodec的releaseOutputBuffer释放输出缓存区,并交还给编解码器,该输出缓存区将不能被使用,直到下一次通过dequeueOutputBuffer获取。releaseOutputBuffer方法接收两个参数:Index、render,其中,Index为输出缓存区索引;render表示当配置编码器时指定了surface,那么应该置为true,输出缓存区的数据将被传递到surface中。源码如下:

   public final void releaseOutputBuffer(int index, boolean render) {BufferInfo info = null;synchronized(mBufferLock) {invalidateByteBuffer(mCachedOutputBuffers, index);mDequeuedOutputBuffers.remove(index);if (mHasSurface) {info = mDequeuedOutputInfos.remove(index);}}releaseOutputBuffer(index, render, false /* updatePTS */, 0 /* dummy */);}

转载自无名之辈FTER

音视频探索(6):浅析MediaCodec工作原理相关推荐

  1. CoreCLR源码探索(八) JIT的工作原理(详解篇)

    在上一篇 我们对CoreCLR中的JIT有了一个基础的了解,这一篇我们将更详细分析JIT的实现. JIT的实现代码主要在https://github.com/dotnet/coreclr/tree/m ...

  2. Android 音视频编解码(一) -- MediaCodec 初探

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  3. 浏览器中的音视频知识总结v1.0(工作中需要和视频打交道必看!)

    视频是什么 视频,其实就是一系列连续播放的图片,如果1s钟播放24张图片,那么人眼看到的就不再是一张张独立的图片,而是动起来的画面.其中一张图片称为一帧,1s播放的图片数称为帧率.常见的帧率有24帧/ ...

  4. 音视频探索(2):AAC编码解析

    1.AAC编码格式分析 1.1 AAC简介  高级音频编码(AdvancedAudio Coding,AAC)一种基于MPEG-4的音频编码技术,它由杜比实验室.AT&T等公司共同研发,目的是 ...

  5. 朗强:HDMI视频画面分割器基本工作原理和性能

    在有多个摄像机组成的电视监控系统中,通常采用视频切换器使多路图像在一台监视器上轮流显示.但有时为了让监控人员能同时看到所有监控点的情况,往往采用多画面分割器使得多路图像同时显示在一台监视器上.当采用几 ...

  6. Android音视频开发-音频篇-音频的原理

    致知在格物,物格而后知至.所谓致知在格物者,言欲致吾之知,在即物而穷其理也 意思讲的是要探究事物的原理,从而获得智慧. 写代码也是如此,只有了解其中的原理,才能运用自如 所以我们要想学好Android ...

  7. 【音视频】编/解码 - 编码器底层原理学习顺序

    # 目的:如何进行编码器的选择,在做编码效率测评的时候,需要去了解编码器的工作原理 # 简单知识储备 编码器类型: H.264 H.265 微帧 底层编码器:opus.VP8.VP9.AV1 和 HE ...

  8. 【音特电子】整流二极管的工作原理与选型

    整流二极管工作原理: 整流二极管是利用PN结的单向导电特性,把交流电变成脉动直流电的半导体器件.选用整流二极管时,主要应考虑其最大整流电流.最大反向工作电流.截止频率及反向恢复时间等参数. 根据芯片工 ...

  9. 音视频技术开发周刊 | 224

    每周一期,纵览音视频技术领域的干货. 新闻投稿:contribute@livevideostack.com. 高性能且灵活的 iOS 视频剪辑与特效开源框架 – VideoLab 随着移动互联网时代的 ...

最新文章

  1. ASP.NET导出文件FileResult的使用
  2. crosstab交叉表_透视图和交叉表
  3. 基于三代测序技术的高产糖化酶黑曲霉工业菌株基因组组装与注释及功能基因比较研究
  4. POJ 1201 amp; HDU1384 amp; ZOJ 1508 Intervals(差分约束+spfa 求最长路径)
  5. 17个之多!Windows Vista各版本功能区别详解
  6. 【图示解析】不同进制之间的表示与转换
  7. tomcat报404
  8. Cacti监控mysql数据库server实现过程
  9. UVA-1602 Lattice Animals 搜索问题(打表+set)
  10. IOS开发之异步加载网络图片并缓存本地实现瀑布流(一)
  11. 数据结构学习笔记(二) 线性表的顺序存储和链式存储
  12. python过滤敏感词汇_Python过滤敏感词汇
  13. neo4j实现Louvain算法
  14. VS2013密钥(所有版本)
  15. typroa 思维导图_Markdown转思维导图及Typora导出opml(pandoc)
  16. 买房税费大攻略!哪些费用必须交?
  17. 2018再见|2019你好
  18. 解析微分电路和积分电路的区别
  19. oracle10g闪回恢复数据表
  20. Quantopian自学笔记02

热门文章

  1. 阿里云短信验证码注册及使用
  2. 中国噪音计市场趋势报告、技术动态创新及市场预测
  3. 风电机组状态监测系统(CMS)
  4. 全面注册制对量化交易的影响 | A+CLUB 2023专题峰会圆桌论坛
  5. python运行启动报错解决方法_51testing:iOS自动化测试的那些干货:关于appium启动报错问题的解决办法...
  6. 110_cs江湖2_苹果双子星:两个史蒂夫
  7. 20165334《java程序设计》第4周学习总结
  8. 微信小程序 canvas 卡顿 闪退
  9. 帆软报表-鼠标悬停改变背景色
  10. 106:vue+openlayers 图片分解成颜色块渲染 (代码示例)