[Android] MediaCodec 的使用解析

MediaCodec

MediaCodec 是 Android 提供的用以音视频编解码的一个类。从广义上讲,一个编解码器处理一个输入数据而生成一个输出数据,通过使用一组输入和输出缓冲器异步处理数据。非常简单的做法是,你可以创建一个空的输入缓冲器,将其填充满并发送到编解码器进行处理,编解码器将里边的数据转换好后输出到输出缓冲器,最后我们将输出缓冲器的数据拿出来即可释放编解码器,当然如果后续有数据要继续处理将会重复上述过程。流程如下:

MediaCodec 支持压缩数据、原始音频数据和原始视频数据。在使用 MediaCodec 之前最好了解一下 ByteBuffer 的使用。

使用流程

上图是 MediaCodec 的各个状态,主要分为 Executing、 Stopped、 Released 三种,然后在这三种状态内又分为几个小状态。我们以视频编码器为例,当我们在使用时将会用以下代码创建一个视频编码器:

 MediaCodec.createByCodecName(mCodecInfo.getName());

其中还有另外一种创建方法:

MediaCodec createDecoderByType (String type)
MediaCodec createEncoderByType (String type)

其中 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

createByCodecName() 是 Android 中推荐的以避免传入不支持的类型,我们可以通过 MediaExtractor.getTrackFormat 来获取到相关的信息。

在未 configure 之前的状态为 Stopped 中的 Uninitialized 状态,我们需要调用以下代码将其转换为 Stopped 中的 Configured 状态:

mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

函数原型为:public void configure (MediaFormat format,  Surface surface,  MediaCrypto crypto,  int flags) 四个参数分别为:编码器所需格式,此编码器输出的 Surface(传 null 那么就是不生成原始视频输出或以 ByteBuffer 输出),指定加密对象(传 null 那么就是非安全的编码器),指定CONFIGURE_FLAG_ENCODE将组件配置为编码器。其中 MediaFormat 可以初始化格式,比如宽高、帧率等参数。

处理数据有两种方式:同步处理和异步处理。

当我们使用同步处理时,我们通过调用 mediaCodec.start() 将会使编码器进入 Executing 的 Flushed 状态,此时编码器拥有所有缓存,直到第一个输入缓存移出队列时编码器进入 Running 状态,当一个标记了 end-of-stream (即代码中设置:codec.queueInputBuffer(index,0,0,0,BUFFER_FLAG_END_OF_STREAM);)的缓存进入输入缓存队列时进入 End of Stream 状态。此时输入缓存队列不再有缓存进入,但输出队列仍有缓存进入,直到标记了 end-of-stream 的缓存进入输出缓存队列。我们可以在 Executing 的任何状态调用 flush() 方法进入 Flushed 状态。

官方代码:

 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(…);
     // 将数据放入 inputBuffer
     …
     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();

其中 codec.dequeueInputBuffer(timeoutUs) 返回的是缓冲区的 BufferId,返回值为 -1 则表示不可用。参数传入正数为等待时间,传入 0 会立即返回,传入负数则会无限等待。

codec.getInputBuffer() / codec.getOutputBuffer() 返回的是一个可写入/出的缓冲区,将数据写入此处后通过 codec.queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags) 将缓冲区交给 codec。一般来所 flags 有三个值: BUFFER_FLAG_CODEC_CONFIG – 配置信息, BUFFER_FLAG_END_OF_STREAM – 结束标志, BUFFER_FLAG_KEY_FRAME – 关键帧,不建议使用。在执行此方法后index指向的缓冲区将不可访问,继续使用将会抛出异常。

最后使用完后必须使用 codec.releaseOutputBuffer(int bufferId, …) 将缓冲区还给 codec 交给下一个数据使用。

Android 5.0 以后官方提供了一个异步处理的方案以提升效率。我们要在 configure 之前将回调监听设置上去。与同步不同的是我们必须 flush() 之后才能调用 start() 方法。

官方代码:

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     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(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

其中各个回调分别表示:

void onError(MediaCodec codec, MediaCodec.CodecException e)//发生错误时回调此方法

void onInputBufferAvailable(MediaCodec codec, int index)//当inputbuffer可用时回调此方法

void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)
//当output方法可用时回调此方法
void onOutputFormatChanged(MediaCodec codec, MediaFormat format)//当输出格式变化时回调此方法

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注