MediaCodec工作原理

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

通过上图可以看出,mediacodec的作用是处理输入的数据生成输出数据。首先生成一个输入数据缓冲区,将数据填入缓冲区提供给codec,codec会采用异步的方式处理这些输入的数据,然后将填满输出缓冲区提供给消费者,消费者消费完后将缓冲区返还给codec。

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方法终止编解码器的使用。

(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) 配置、启动编/解码器
编解码器配置使用的是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 */);}}

(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 */);}

多媒体系统之MediaCodec基本原理及使用相关推荐

  1. 多媒体系统之MediaCodec基本原理及使用(四)

    阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680 MediaCodec工作原理 MediaCodec类Androi ...

  2. Android:MediaCodec基本原理

    最近需要使用MediaCodec做一些工作,因此对MediaCodec做了些研究和代码编写,在此先对MediaCodec的一些基础原理.工作流程.常用API等做个初步总结,方便后续开发过程中查阅. 1 ...

  3. Android原生编解码接口 MediaCodec 之——完全解析

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/gb702250823/article/ ...

  4. Android音视频API - MediaCodec/MediaMuxer/MediaStore/MediaController等

    AudioTrack播放音频PCM.[Android] 混音器AudioMixer. MediaPlayer/MediaRecorder, AudioTrack/AudioRecorder, Medi ...

  5. Computer OS系统基本原理

    Computer OS系统基本原理 第一章 绪论(考概念) 什么是OS? o 操作系统是一组控制和管理计算机软硬件资源.合理地对各类作业进行调度以及方便用户使用的程序集合. o 操作系统是位于硬件层( ...

  6. XGBoost4J-Spark基本原理

    XGBoost4J-Spark基本原理 XGBoost4J-Spark是一个项目,旨在通过使XGBoost适应Apache Spark的MLLIB框架,无缝集成XGBoost和Apache Spark ...

  7. Docker基本原理概述

    Docker基本原理概述 Docker是一个用于开发,交付和运行应用程序的开放平台.Docker能够将应用程序与基础架构分开,从而可以快速交付软件.借助Docker,可以以与管理应用程序相同的方式来管 ...

  8. 多机多卡训练基本原理

    多机多卡训练基本原理 在工业实践中,许多较复杂的任务需要使用更强大的模型.强大模型加上海量的训练数据,经常导致模型训练耗时严重.比如在计算机视觉分类任务中,训练一个在ImageNet数据集上精度表现良 ...

  9. MindSpore基本原理

    MindSpore基本原理 • MindSpore介绍 o 自动微分 o 自动并行 • 安装 o pip方式安装 o 源码编译方式安装 o Docker镜像 • 快速入门 • 文档 MindSpore ...

最新文章

  1. yd的拔钉子之路之 POI 2017
  2. UI组件库从1到N开发心得-组件篇
  3. web编程 端口分配_以编程方式衡量分配
  4. CentOS6.5升级内核
  5. MATLAB去除人声
  6. Codeforces蓝名紫名黄名纪念贴
  7. excel组合工具使用
  8. 史上最简单的 IntelliJ IDEA 教程
  9. WINDOWS 7 PRO X64 2015年9月增量补丁包微软官方下载地址
  10. Convex Clustering(凸聚类)
  11. linux上删除rime方案_超强的输入法:rime的配置(linux)
  12. 制作一个电影推荐系统的案例
  13. 电脑浏览器一直显示邮箱服务器失败,各种PC客户端都无法连接Outlook.com邮箱
  14. MySQL为什么会抖一下
  15. Android Studio 布局 - ScrollView和HorizontalScrollView
  16. 【老生谈算法】matlab实现轮盘赌算法-Roulette——轮盘赌算法
  17. 决策树算法的理论和实践
  18. APP漏洞导致移动支付隐患重重,未来之路如何走?
  19. 解除360个人图书馆文档禁止复制限制Chrome浏览器插件
  20. 【来日复制粘贴】提取工作表和工作簿名称

热门文章

  1. java算法题Day9
  2. 2022北京冬奥会竞赛日程表以及json数据与python解析
  3. matlab二进制乘法器,用74ls138实现2位二进制乘法器 - 数字电路图
  4. Sci-learn LASSO算法进行线性回归
  5. 【亚马逊运营】不得不防!恶意投诉层出不穷,跨境卖家该如何应对?
  6. 由浅入深玩转华为WLAN—-5 AP上线与对接交换机接口配置注意事项
  7. hex文件、bin文件 合并删除编辑
  8. canvas 文字灯光环绕粒子闪烁效果 简单实例
  9. 【数据结构】图的存储结构—邻接表
  10. 你必须知道的ABI和CPU关系