MediaCodec原理与流程(重生之我要成为音视频开发大佬)

MediaCodec 是 Android 中的编解码器组件,用来访问底层提供的编解码器,通常与 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface 和 AudioTrack 一起使用,MediaCodec 几乎是 Android 播放器硬解码的标配。

MediaCodec工作流程

MediaCodec的数据流分为input和output流,并通过异步的方式处理两路数据流,直到手动释放output缓冲区,MediaCodec才将数据处理完毕。

  • input流:客户端输入待解码或者待编码的数据
  • output流:客户端输出的已解码或者已编码的数据

流程代码:

- createByCodeName/createEncoderByType/createDecoderByType: (静态工厂构造MediaCodec对象)--Uninitialized状态- configure:(配置) -- configure状态- start        (启动)--进入Running状态- while(1) {    try{       - dequeueInputBuffer    (从编解码器获取输入缓冲区buffer)       - queueInputBuffer      (buffer被生成方client填满之后提交给编解码器)       - dequeueOutputBuffer   (从编解码器获取输出缓冲区buffer)       - releaseOutputBuffer   (消费方client消费之后释放给编解器)    } catch(Error e){       - error                   (出现异常 进入error状态)    }    }- stop                          (编解码完成后,释放codec)- release

MediaCodec生命周期

MediaCodec 有三种状态,分别是执行(Executing)、停止(Stopped)和释放(Released),其中执行和停止分别有三个子状态,执行的三个字状态分别是 Flushed、Running 和 Stream-of-Stream,停止的三个子状态分别是 Uninitialized、Configured 和 Error,MediaCodec 生命周期示意图如下:

  • 停止状态(Stopped)
 1// 创建MediaCodec进入Uninitialized子状态 2public static MediaCodec createByCodecName (String name) 3public static MediaCodec createEncoderByType (String type) 4public static MediaCodec createDecoderByType (String type) 5// 配置MediaCodec进入Configured子状态,crypto和descrambler会在后文中进行说明 6public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) 7public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler) 8// Error 9// 编解码过程中遇到错误进入Error子状态
  • 执行状态(Executing)
1// start之后立即进入Flushed子状态2public final void start()3// 第一个输入缓冲区出队的时候进入Running子状态4public int dequeueInputBuffer (long timeoutUs)5// 输入缓冲区与流结束标记排队时,编解码器将转换为End-of-Stream子状态6// 此时MediaCodec将不接受其他输入缓冲区,但会生成输出缓冲区7public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
  • 释放状态(Released)
1// 编解码完成结束后释放MediaCodec进入释放状态(Released)2public void release ()

MediaCodec的创建

前面已经提到过当创建 MediaCodec 的时候进入Uninitialized 子状态,其创建方式如下:

1// 创建MediaCodec2public static MediaCodec createByCodecName (String name)3public static MediaCodec createEncoderByType (String type)4public static MediaCodec createDecoderByType (String type)

使用 createByCodecName 时可以借助 MediaCodecList 获取支持的编解码器,下面是获取指定 MIME 类型的编码器:

 1/** 2 * 查询指定MIME类型的编码器 3 */ 4fun selectCodec(mimeType: String): MediaCodecInfo? { 5    val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) 6    val codeInfos = mediaCodecList.codecInfos 7    for (codeInfo in codeInfos) { 8        if (!codeInfo.isEncoder) continue 9        val types = codeInfo.supportedTypes10        for (type in types) {11            if (type.equals(mimeType, true)) {12                return codeInfo13            }14        }15    }16    return null17}  1/** 2 * 查询指定MIME类型的编码器 3 */ 4fun selectCodec(mimeType: String): MediaCodecInfo? { 5    val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS) 6    val codeInfos = mediaCodecList.codecInfos 7    for (codeInfo in codeInfos) { 8        if (!codeInfo.isEncoder) continue 9        val types = codeInfo.supportedTypes10        for (type in types) {11            if (type.equals(mimeType, true)) {12                return codeInfo13            }14        }15    }16    return null17}

当然 MediaCodecList 也提供了相应的获取编解码器的方法,如下:

1// 获取指定格式的编码器2public String findEncoderForFormat (MediaFormat format)3// 获取指定格式的解码器4public String findDecoderForFormat (MediaFormat format)

对于上述方法中的参数 MediaFormat 格式中不能包含任何帧率的设置,如果已经设置了帧率需要将其清除再使用。

上面提到了 MediaCodecList,这里简单说一下,使用 MediaCodecList 可以方便的列出当前设备支持的所有的编解码器,创建 MediaCodec 的时候要选择当前格式支持的编解码器,也就是选择的编解码器需支持对应的 MediaFormat,每个编解码器都被包装成一个 MediaCodecInfo 对象,据此可以查看该编码器的一些特性,比如是否支持硬件加速、是软解还是硬解编解码器等,常用的简单如下:

 1// 是否软解 2public boolean isSoftwareOnly () 3// 是Android平台提供(false)还是厂商提供(true)的编解码器 4public boolean isVendor () 5// 是否支持硬件加速 6public boolean isHardwareAccelerated () 7// 是编码器还是解码器 8public boolean isEncoder () 9// 获取当前编解码器支持的合适10public String[] getSupportedTypes ()11// ...

软解和硬解应该是音视频开发中必须掌握的,当使用 MediaCodec 的时候不能说全是硬解,到底使用硬解还是软解还是要看使用的编码器,一般厂商提供的编解码器都是硬解编解码器,比如高通(qcom)等,一般如系统提供的则是软解编解码器,如带有 android 字样的编解码器,下面是本人(MI 10 Pro)自己手机的部分编解码器:

 1// 硬解编解码器 2OMX.qcom.video.encoder.heic 3OMX.qcom.video.decoder.avc 4OMX.qcom.video.decoder.avc.secure 5OMX.qcom.video.decoder.mpeg2 6OMX.google.gsm.decoder 7OMX.qti.video.decoder.h263sw 8c2.qti.avc.decoder 9...10// 软解编解码器11c2.android.aac.decoder12c2.android.aac.decoder13c2.android.aac.encoder14c2.android.aac.encoder15c2.android.amrnb.decoder16c2.android.amrnb.decoder17...  1// 硬解编解码器 2OMX.qcom.video.encoder.heic 3OMX.qcom.video.decoder.avc 4OMX.qcom.video.decoder.avc.secure 5OMX.qcom.video.decoder.mpeg2 6OMX.google.gsm.decoder 7OMX.qti.video.decoder.h263sw 8c2.qti.avc.decoder 9...10// 软解编解码器11c2.android.aac.decoder12c2.android.aac.decoder13c2.android.aac.encoder14c2.android.aac.encoder15c2.android.amrnb.decoder16c2.android.amrnb.decoder17...

MediaCodec初始化

创建 MediaCodec 之后进入 Uninitialized 子状态,此时需要对其进行一些设置如指定 MediaFormat、如果使用的是异步处理数据的方式,在 configure 之前要设置 MediaCodec.Callback,关键 API 如下:

 1// 1. MediaFormat 2// 创建MediaFormat 3public static final MediaFormat createVideoFormat(String mime,int width,int height) 4// 开启或关闭功能,具体参见MediaCodeInfo.CodecCapabilities 5public void setFeatureEnabled(@NonNull String feature, boolean enabled) 6// 参数设置 7public final void setInteger(String name, int value) 8 9// 2. setCallback10// 如果使用的是异步处理数据的方式,在configure 之前要设置 MediaCodec.Callback11public void setCallback (MediaCodec.Callback cb)12public void setCallback (MediaCodec.Callback cb, Handler handler)1314// 3. 配置15public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)16public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)

上面 configure 配置中涉及到几个参数,其中 surface 表示解码器要渲染的 Surface,flags 则是指定当前编解码器是作为编码器还是解码器来使用的,crypto 和 descrambler 都和解密有关,比如某些 vip 视频就需要特定的密钥来配合解码,只有用户登录校验后才会对视频内容进行解密,要不然某些需要付费才能观看的视频下载之后就能随意传播了,更多细节可以查看音视频中的数字版权技术。

此外某些特定格式比如 AAC 音频以及 MPEG4、H.264、H.265 视频格式,这些格式包含一些用于 MediaCodec 的初始化特定的数据,当解码处理这些压缩格式时,必须在 start 之后且在任何帧数据处理之前将这些特定数据提交给 MediaCodec,即在对 queueInputBuffer 的调用中使用标志 BUFFER_FLAG_CODEC_CONFIG 标记此类数据,这些特定的数据也可以通过 MediaFormat 设置 ByteBuffer 的方式进行配置,如下:

1// csd-0、csd-1、csd-2同理2val bytes = byteArrayOf(0x00.toByte(), 0x01.toByte())3mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(bytes))

其中 csd-0、csd-1 这些键可以从 MediaExtractor#getTrackFormat 获取的MediaFormat中获取,这些特定的数据会在start 时自动提交给 MediaCodec,无需直接提交此数据,如果在输出缓冲区或格式更改之前调用了 flush,则会丢失提交的特定数据,就需要在 queueInputBuffer 的调用中使用标志 BUFFER_FLAG_CODEC_CONFIG 标记这类数据。

Android 使用以下特定于编解码器的数据缓冲区,为了正确配置 MediaMuxer 轨道,还需要将它们设置为轨道格式,每个参数集和标有(*)的编解码器专用数据部分必须以“ \ x00 \ x00 \ x00 \ x01”的起始代码开头,参考如下:

MediaCodec数据处理方式

每个创建已经创建的编解码器都维护一组输入缓冲区,有两种处理数据的方式,同步和异步方式,根据 API 版本不同有所区别,在 API 21 也就是从 Android5.0 开始,推荐使用 ButeBuffer 的方式进行数据的处理,在此之前只能使用 ButeBuffer 数组的方式进行数据的处理,如下:

 1// 获取输入缓冲区(同步) 2public int dequeueInputBuffer (long timeoutUs) 3public ByteBuffer getInputBuffer (int index) 4// 获取输出缓冲区(同步) 5public int dequeueOutputBuffer (MediaCodec.BufferInfo info, long timeoutUs) 6public ByteBuffer getOutputBuffer (int index) 7// 输入、输出缓冲区索引从MediaCodec.Callback的回调中获取,在获取对应的输入、输出缓冲区(异步) 8public void setCallback (MediaCodec.Callback cb) 9public void setCallback (MediaCodec.Callback cb, Handler handler)10// 提交数据11public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)12public void queueSecureInputBuffer (int index, int offset, MediaCodec.CryptoInfo info, long presentationTimeUs, int flags)13// 释放输出缓冲区14public void releaseOutputBuffer (int index, boolean render)15public void releaseOutputBuffer (int index, long renderTimestampNs)

下面主要介绍介绍适用于 Android 5.0 之后的 ButeBuffer 的方式,

Android 5.0 开始 Deprecated 了 ButeBuffer 数组的方式,官网上提到 ButeBuffer 相较 ButeBuffer 数组的方式做了一定优化,故在设备满足条件的情况下尽量使用 ButeBuffer 对应的 API,且推荐使用异步模式处理数据,同步和异步处理方式代码参考如下:

  • 同步处理模式
 1MediaCodec codec = MediaCodec.createByCodecName(name); 2 codec.configure(format, …); 3 MediaFormat outputFormat = codec.getOutputFormat(); // option B 4 codec.start(); 5 for (;;) { 6  int inputBufferId = codec.dequeueInputBuffer(timeoutUs); 7  if (inputBufferId >= 0) { 8    ByteBuffer inputBuffer = codec.getInputBuffer(…); 9    // 使用有效数据填充输入缓冲区10    …11    codec.queueInputBuffer(inputBufferId, …);12  }13  int outputBufferId = codec.dequeueOutputBuffer(…);14  if (outputBufferId >= 0) {15    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);16    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A17    // bufferFormat与outputFormat是相同的18    // 输出缓冲区已准备后被处理或渲染了19    …20    codec.releaseOutputBuffer(outputBufferId, …);21  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {22    // 输出格式改变,后续采用新格式,此时使用getOutputFormat()获取新格式23    // 如果使用getOutputFormat(outputBufferId)获取特定缓冲区的格式,则无需监听格式变化24    outputFormat = codec.getOutputFormat(); // option B25  }26 }27 codec.stop();28 codec.release();
  • 异步处理模式
 1MediaCodec codec = MediaCodec.createByCodecName(name); 2 codec.configure(format, …); 3 MediaFormat outputFormat = codec.getOutputFormat(); // option B 4 codec.start(); 5 for (;;) { 6  int inputBufferId = codec.dequeueInputBuffer(timeoutUs); 7  if (inputBufferId >= 0) { 8    ByteBuffer inputBuffer = codec.getInputBuffer(…); 9    // 使用有效数据填充输入缓冲区10    …11    codec.queueInputBuffer(inputBufferId, …);12  }13  int outputBufferId = codec.dequeueOutputBuffer(…);14  if (outputBufferId >= 0) {15    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);16    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A17    // bufferFormat与outputFormat是相同的18    // 输出缓冲区已准备后被处理或渲染了19    …20    codec.releaseOutputBuffer(outputBufferId, …);21  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {22    // 输出格式改变,后续采用新格式,此时使用getOutputFormat()获取新格式23    // 如果使用getOutputFormat(outputBufferId)获取特定缓冲区的格式,则无需监听格式变化24    outputFormat = codec.getOutputFormat(); // option B25  }26 }27 codec.stop();28 codec.release();

当要处理的数据结束时(End-of-stream),需要标记流的结束,可以在最后一个有效的输入缓冲区上使用 queueInputBuffer 提交数据的时候指定 flags 为 BUFFER_FLAG_END_OF_STREAM 标记其结束,也可以在最后一个有效输入缓冲区之后提交一个空的设置了流结束标志的输入缓冲区来标记其结束,此时不能够再提交输入缓冲区,除非编解码器被 flush、stop、restart,输出缓冲区继续返回直到最终通过在 dequeueOutputBuffer 或通过 Callback#onOutputBufferAvailable 返回的 BufferInfo 中指定相同的流结束标志,最终通知输出流结束为止。

如果使用了一个输入 Surface 作为编解码器的输入,此时没有可访问的输入缓冲区,输入缓冲区会自动从这个 Surface 提交给编解码器,相当于省略了输入的这个过程,这个输入 Surface 可由 createInputSurface 方法创建,此时调用 signalEndOfInputStream 将发送流结束的信号,调用后,输入表面将立即停止向编解码器提交数据,关键 API 如下:

1// 创建输入Surface,需在configure之后、start之前调用2public Surface createInputSurface ()3// 设置输入Surface4public void setInputSurface (Surface surface)5// 发送流结束的信号6public void signalEndOfInputStream ()

同理如果使用了输出 Surface,则与之相关的输出缓冲区的相关功能将会被代替,可以通过 setOutputSurface 设置一个 Surface 作为编解码器的输出,可以选择是否在输出 Surface 上渲染每一个输出缓冲区,关键 API 如下:

1// 设置输出Surface2public void setOutputSurface (Surface surface)3// false表示不渲染这个buffer,true表示使用默认的时间戳渲染这个buffer4public void releaseOutputBuffer (int index, boolean render)5// 使用指定的时间戳渲染这个buffer6public void releaseOutputBuffer (int index, long renderTimestampNs)

自适应播放支持

当 MediaCodec 作为视频解码器的时候,可以通过如下方式检查解码器是否支持自适应播放,也就是此时解码器是否支持无缝的分辨率修改:

1// 是否支持某项功能,CodecCapabilities#FEATURE_AdaptivePlayback对应对应自适应播放支持2public boolean isFeatureSupported (String name)

此时只有在将解码器配置在 Surface 上解码时,自适应播放的功能才会被激活,视频解码时当 strat 或 flush 调用后,只有关键帧(key-frame)才能完全独立解码,也就是通常说的 I 帧,其他帧都是据此来解码的,不同格式对应关键帧如下:

MediaCodec的异常处理

关于 MediaCodec 使用过程中的异常处理,这里提一下 CodecException 异常,一般是由编解码器内部异常导致的,比如媒体内容损坏、硬件故障、资源耗尽等,可以通过如下方法判断以做进一步的处理:

1// true表示可以通过stop、configure、start来恢复2public boolean isRecoverable ()3// true表示暂时性问题,编码或解码操作会在后续重试进行4public boolean isTransient ()

如果 isRecoverable 和 isTransient 都是返回 false,则需要通过 reset 或 release 操作释放资源后重新工作,两者不可能同时返回 true。

有关MediaCodec 的介绍到此为止。音视频的领域它的技术范围很宽,内容需要很深入的学习,许多人在音视频学习中慢慢的陷入其中,越学越迷糊。还有的中途放弃。这是为什么?很多人犯了一个错误,一开始没有路线的学习,导致“ 先帝创业未半而中道崩殂 ”所以学习一定需要系统性学习,特别是音视频这种涉及的技术范围很广的领域。这里我带来一张音视频学习路线思维导图,大家可以参考进行总结;如下图:

可以根据这个“蜘蛛网”思维导图,一条龙学习。

既然思维导图都给出了,我这里就再附上一份《音视频入门到精通》的学习资料文档。大家觉得总结不错的,多加点赞评论哦!资料私信:“手册”,需要的可以获取哟!

【私信:“手册”获取】《全套音视频入门到精通手册》

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章