[Android 进阶]MediaCodec简介

两盒软妹~` 提交于 2019-11-30 10:30:10

[Android 进阶]MediaCodec简介

由于前段时间单位处理过录屏直播之类的需求,这边首推的一个系列着力于MediaCodec,该系列包含下面几片文章:

  • [Android 进阶]MediaCodec系列之MediaCodec简介
  • [Android 进阶]MediaCodec系列之MediaCodec+MediaEctractor播放器
  • [Android 进阶]MediaCodec+MediaProjection实现录制屏幕
  • [Android 进阶]MediaCodec+Camera API实现摄像头录制视频(上)
  • [Android 进阶]MediaCodec+Camera API实现摄像头录制视频(下)

本篇文章是Google官方文档的翻译,其中生命周期的概念是我重新整理过的,原文地址:

https://developer.android.google.cn/reference/android/media/MediaCodec.html
MediaCodec是用来访问系统底层编解码器的一个类,通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack等一起使用。作为与底层编解码器交流的工作类,MediaCodec的一般数据流图如下[官网图]:

客户端为MediaCodec提供需要编解码的数据[存在输入缓存区],MediaCodec与底层编解码器交互,拿到处理后结果填充在输出缓存区供客户端使用,当前这种处理过程是异步的。

编解码器简介

数据类型

编解码器支持三种数据类型:压缩数据,原始音频数据,原始视频数据,以上三种数据都可以使用ByteBuffer进行处理,需要注意的是对于原始视频数据我们应该使用Surface以提高编解码器性能,Surface可以直接使用本地视频缓冲区中的内容以规避将其映射或复制到ByteBuffer内,因此Surface更高效。通常情况下, 使用Surface时我们不能访问原始视频数据,不过我们可以通过ImageReader拿到一个不安全的解码数据帧。在ByteBuffer模式下,一些本地缓存数据可能被映射到ByteBuffer内,此时我们可以使用Image/getInput/OutputImage(int)来获取视频数据中的某一帧,这样仍然会比使用ByteBuffer高效。

压缩缓冲区

解码缓冲区和编码缓冲区中包含了按照特定格式生成的压缩数据,如果是视频类型,通常包含着视频中的某一帧压缩数据,如果是音频类型,则包含着一个可访问的音频单元(一个编码的音频单元通常包含几毫秒这种音频单元),但这个要求并不是特别高,有时缓冲区中也可能含有多个这样的音频单元,在这两种情况下,缓存不会在任意的字节边界上开始或结束,而是在帧或可访问单元的边界上开始或结束。

原始音频缓冲区

原始音频数据缓冲区包含完整的PCM音频数据帧,每一个音频数据帧都是按照通道顺序传递的每一个通道的样本数据,同时音频数据帧也是一个按照本机字节顺序的16位带符号整数。PCM时一种音频编码格式。
我们可以通过如下方式使用MediaCodec获取音频数据帧:

short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
   //获取输出缓存区的ByteBuffer
   ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
   MediaFormat format = codec.getOutputFormat(bufferId);
   ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
   int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
   if (channelIx < 0 || channelIx >= numChannels) {
     return null;
   }
   short[] res = new short[samples.remaining() / numChannels];
   for (int i = 0; i < res.length; ++i) {
     res[i] = samples.get(i * numChannels + channelIx);
   }
   return res;
 }

原始视频缓冲区

ByteBuffer模式下,视频缓存(video buffers)根据它们的颜色格式(color format)进行展现。你可以通过调用getCodecInfo().getCapabilitiesForType(…).colorFormats方法获得编解码器支持的颜色格式数组。视频编解码器可以支持三种类型的颜色格式:

  • 本地原始视频格式:这种格式通过COLOR_FormatSurface标记,并可以与输入或输出Surface一起使用;

  • 灵活的YUV缓存格式:(例如:COLOR_FormatYUV420Flexible)利用一个输入或输出Surface,或在在ByteBuffer模式下,可以通过调用getInput/OutputImage(int)方法使用这些格式;

  • 其他特定的格式:通常只在ByteBuffer模式下被支持。有些颜色格式是特定供应商指定的。其他的一些被定义在 MediaCodecInfo.CodecCapabilities中。这些颜色格式同flexible format相似,你仍然可以使用getInput/OutputImage(int)方法;

从Android5.1以后,所以的视频编解码器均支持YUV格式。

在旧设备上获取原始视频的ByteBuffer

在Android5.0和 Image API支持之前,我们可以通过KEYSTRIDE和KEYSLICE_HEIGHT这两个输出格式值来理解原始输出缓冲区的布局。

这里要注意在有些设备上slice-height的值为0,这样可能导致slice-height和视频帧的高度一样,或者slice-height的值会被指定成视频帧高度相关的值(通常是2的整数倍),然而我们并没有一个标准的或者简单的方式去告知slice-height的正确值,此外,垂直方向上的U分量也没有指定或定义,通常情况下该值是slice-height的一半。
KEYWIDTH 和KETHEIGHT指定了视频帧的宽度和高度,然而对于大多数视频帧而言,宽高取决于’crop rectangle’.

可以通过下列的key从输出格式中获取crop rectangle,如果不存在这些值,视频就是整个视频帧,crop rectangle作用于任何旋转操作之前,’crop-left’,’crop-top’,’crop-rigt’,’crop-bottom’四个值分别指定了视频帧的最大最小x,y方向的值,随后可以通过如下代码计算视频帧的完整宽高(旋转前):

 MediaFormat format = decoder.getOutputFormat(…);
 int width = format.getInteger(MediaFormat.KEY_WIDTH);
 if (format.containsKey("crop-left") && format.containsKey("crop-right")) {
     width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");
 }
 int height = format.getInteger(MediaFormat.KEY_HEIGHT);
 if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
     height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
 }

这里也要注意BuffeInfo.offset并不是在我们设备上都是一致的,有些设备上,offset指向crop rectangle的左顶点,有些设备上指向的是整个视频帧的左顶点。

MediaCodec状态变化

参考官网文件描述,我绘制了如下MediaCodec生命周期图:

这里写图片描述

如上图所示,当我们使用工厂方法创建一个MediaCodec时,该MediaCodec对象处于Uninitialized状态,通过调用configure函数使其转入Configured状态[需要配合使用MediaFormat],示例代码如下:

//create MediaCodec use factory mothod
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
   //MediaCodec configure
   MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
    format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
    format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
    mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

其中MIME_TYPE可以有以下几种情形:

  • “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/hevc” - H.265/HEVC 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#start方法使其进入Executing状态,这时编解码器就开始准备工作了,当需要停止编解码时,可以调用MediaCodec#stop方法,调用MediaCodec#reset方法可以将编解码器回置到Uninitiallized状态,使用完编解码器一定要调用MediaCodec#release方法释放资源。

创建MediaCodec

除了上文中提到的使用静态方法创建MediaCodec对象外,我们还可以使用MediaCodecList按照指定MdiaFormat创建MediaCodec对象,步骤如下:
当解码一个文件或者数据流时,我们可以通过MediaExtractor 。getTrackFormat获取MediaFormat,使用MediaFormat.setFeatureEnabled使MediaCodec支持特定属性,使用MediaCodecList.findDecoderForFormat获取所要创建的MediaCodec名字,使用createByCodecName创建MediaCodec对象

注意,在Android5.0上,使用MediaCodecList.findDecoder/EncoderForFormat 时,MediaFormat不能包含帧率信息,使用MediaFormat.setString(MediaFormat.KEYFRAMERATE,null)进行设置创建安全的解码器(Creating secure decoders)

  在Android 4.4(KITKAT_WATCH)及之前版本,安全的编解码器(secure codecs)没有被列在MediaCodecList中,但是仍然可以在系统中使用。安全编解码器只能够通过名字进行实例化,其名字是在常规编解码器的名字后附加.secure标识(所有安全编解码器的名字都必须以.secure结尾),调用createByCodecName(String)方法创建安全编解码器时,如果系统中不存在指定名字的编解码器就会抛出IOException异常。
  
  从Android 5.0(LOLLIPOP)及之后版本,你可以在媒体格式中使用FEATURE_SecurePlayback属性来创建一个安全编解码器。

MediaCodec的简单使用

使用Buffer异步处理数据

//根据名字创建MediaCodec对象
MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 //为MediaCodec设置回调接口
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data//获取到可用视频帧,回调该函数,触发MediaCodec进入running状态
     codec.queueInputBuffer(inputBufferId, …);
   }
   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     // 编码完成后回调,在这里处理视频流的保存或渲染
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }
   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }
   @Override
   void onError(…) {
     …
   }
 });
  //MediaCodec进入configure状态
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 //MediaCodec进入executing状态
 codec.start();
 // wait for processing to complete
 //释放MediaCodec
 codec.stop();
 codec.release();

使用Buffer同步处理数据

这里可以单独其一个线程,循环执行MediaCodec的编解码过程,在同步过程中,可以忽略MediaCodec对输入缓冲区的处理过程,根据codec.dequeueOutputBuffer(…)的返回值处理输出区结果即可。

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.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();

官网剩下的其他内容对于后面推文并不是必须的,有需要的小伙伴请自行查看官网链接。更多MediaCodec相关的信息会发布在微信公众号上,欢迎关注。
这里写图片描述

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!