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

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

原文链接https://blog.csdn.net/andrexpert/article/details/79578149
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

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

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

    MediaCodec工作原理 MediaCodec类Android提供的用于访问低层多媒体编/解码器接口,它是Android低层多媒体架构的一部分,通常与MediaExtractor.MediaMux ...

  2. Android:MediaCodec基本原理

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

  3. 【嵌入式--伺服电机】无刷电机FOC驱动基本原理与设计

    系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录 前言 一.无刷直流电机FOC原理[重点] 1.无刷直流电机的特点 2.无刷 ...

  4. 执行后可以查看python版本的是-【单选题】执行后可以查看Python的版本的是

    问题:[单选题]执行后可以查看Python的版本的是 更多相关问题 完成并配平下列各反应的方程式: (a)用浓H2SO4焙烧法处理氟碳铈镧矿 (b)镧.铈.镨.铽草 据报道,我国目前已发现甲型H1N1 ...

  5. rocketmq存储结构_阿里专家分享内部绝密RocketMQ核心原理与最佳实践笔记

    本文源码以RocketMQ 4.2.0 和 RocketMQ 4.3.0 为 基 础 , 从RocketMQ的实际使用到RocketMQ的源码分析,再到RocketMQ企业落地实践方案,逐步讲解.使读 ...

  6. Kali Linux 无线渗透测试入门指南 第六章 攻击客户端

    第六章 攻击客户端 作者:Vivek Ramachandran, Cameron Buchanan 译者:飞龙 协议:CC BY-NC-SA 4.0 简介 安全强度取决于最弱的部分. – 信息安全领域 ...

  7. clickhouse原理解析与应用实践 pdf_阿里专家分享内部绝密RocketMQ核心原理与最佳实践PDF...

    前言 本文源码以RocketMQ 4.2.0 和 RocketMQ 4.3.0 为 基 础 , 从RocketMQ的实际使用到RocketMQ的源码分析,再到RocketMQ企业落地实践方案,逐步讲解 ...

  8. 图像处理之给定任意四点不规则放缩

    基本原理: 计算四个点的增长斜率,使用双线性插值实现像素填充. 废话也懒得说啦,自己看代码吧,我从一个地方抄袭+修改了一下 源来的代码,原因是原来的代码太乱了,也太让人费解了. 运行效果: 滤镜源代码 ...

  9. 基于React Native的移动平台研发实践分享

    本文转自微信号EAWorld.扫描下方二维码,关注成功后,回复"普元方法+",将会获得热门课堂免费学习机会! 本文目录: 一.React Native 已经成为了移动前端技术的趋势 ...

最新文章

  1. 快速排序(二)最后修改
  2. 利用***+nat解决客户voip被封锁的问题
  3. VC 中字符串比较和查找
  4. SQL 创建索引的作用以及如何创建索引
  5. Redis中7种集合类型应用场景
  6. NPOI 1.2教程(目录)
  7. pcm 采样率转换_PCM编码与Waveform音频文件(.wav)格式详解
  8. 详细描述一下 Elasticsearch 搜索的过程?
  9. java进销存管理系统设计_基于JAVA的企业进销存管理系统的设计与实现
  10. JavaScript获取移动设备型号的实现代码(JS获取手机型号和系统)
  11. 为什么说衰老先从血管开始?
  12. 最先进的Git分布式仓库系统——是如何提升编程效率的?
  13. 数据科学导论实验:基于Twitter的网络结构和社会群体演化
  14. 第 1-6 课:Spring 的另一个核心机制 AOP
  15. 使用微软官方工具制作Windows10的U盘启动盘
  16. Linux内核分析 期中总结
  17. 如何在 Facebook 上实现高效社交营销
  18. 大众易融迎“端午”活动公告
  19. java我的世界填充方块,【我的世界手游如何快速填充方块】我的世界MC中如何快速填满一个平面的方块...
  20. RGBA与RGB和区别与转换

热门文章

  1. notify()方法、notifyAll()方法和wait()方法
  2. 最全面的系统测试总结报告
  3. 解决mysql重装时,3306端口被占用(完整版)
  4. 正则表达式专题(JS版)之4、正则表达式括号的作用
  5. 主叫无回铃音情况分析
  6. 骨传导蓝牙耳机好用吗,五款好用的骨传导耳机推荐
  7. css 文字倒影效果
  8. 2012年《Satellite Image Time Series Analysis Under Time Warping》:动态时间规整(DTW)在遥感中的应用
  9. 28-进程空间与 fork 函数原理
  10. 抱薪者说 | 王旭:非典型奶爸