Android音视频学习: MediaCodec 硬编解码

官方文档 https://developer.android.google.cn/reference/android/media/MediaCodec

MediaCodec 是做硬件(GPU,充分利用GPU 的并行处理能力)编解码的。(通常结合 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack 使用)

codec (即 encoder + decoder ) :编解码器
MediaExtractor:解封装
MediaMuxer: 混合器(封装)(音视频合成)
MediaSync: 音视频同步
MediaCrypto: 加密

原始音频数据 (PCM )和 视频帧压缩编码(aac + h.264 等 )后要封装到一个容器(MP4 等)中进行传播。同理播放器播放前要先解封装,取出里面的音频部分和视频部分,然后解码成硬件可以直接播放和渲染的音频流和视频流。由于音频流和视频流是分别播放和渲染的,所以这里就有音视频同步的问题。

356361-88f9733c632b2786.png
jiagou.png

codec 处理输入数据产生输出数据。它通过输入缓冲集合和输出缓冲集合异步的处理数据。先请求一个空的 input buffer,然后填充上要处理的数据发送给 codec 处理。 codec 处理数据后会把结果写到一个空的 output buffer 中。最后你请求 output buffer 从里面读出处理后的数据就行了。output buffer 用完后释放回 codec 重新使用。

数据类型

codec 处理3种类型的数据, compressed data (待解码的数据 或 编码后的数据)、raw audio data (待编码或解码后的数据)和 raw video data (待编码或解码后的数据)。3种数据类型都可以用 ByteBuffers 处理。还可以用 Surface 来处理 raw video data 来提高性能。因为 Surface 可以直接使用 native video buffers (在 native 层分配的 buffer)而不需要映射或拷贝到 ByteBuffers (ByteBuffers 是分配在 JVM 堆中的缓冲区) 中。

状态

356361-6f45df58f7d1958f.png
state.png

概念上主要包含 Stopped、Executing、Released 3种状态。Stopped 包含 Configured、Uninitialized、Error 3个子状态。 Executing 包含 Flushed、Running、End of Stream 3个子状态。

codec 实例化以后默认是 Uninitialized 状态,之后需要调用 configure 进入 Configured 状态,再调用 start 进入 Executing 状态。运行状态默认是 Flushed, 这时可以调用 dequeueInputBuffer 拿到一个 input buffer 开始处理数据,进入 Running 状态。当没有输入后需要写一个 end-of-stream marker 的标志(可以放在最后一个 Input buffer 中,也可以用一个单独的空 buffer,空 buffer 的 timestamp 会被忽略)。当 End of stream 后 codec 就不再接受输入了, 但仍然继续产生输出直到输出 buffer 遇到 end-of-stream marker 标志(这个标志是 codec 写的,前提是当没有输入时你必须给 input buffer 写一个 end-of-stream 的标志。这个标志可以作为 codec 处理完毕的标志)。在运行状态可以调用 flush() 方法回到 Flushed 状态。

运行时调用 stop 方法会重新回到 Uninitialized ,这时要重新 configue 、start 才能重新运行。 运行出错时会进入 Error 状态,这时可以调用 reset 方法恢复到 Uninitialized。 codec 不再使用时调用 release 方法释放资源进入 Released 状态。

创建

5.0 之后官方推荐用 MediaCodecList.findDecoderForFormat 传入一个 MediaFormat 来查找你要使用的 codec。 然后调用 MediaCodec.createByCodecName(String) 方法创建 codec。

MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
mediaFormat.setString(MediaFormat.KEY_BIT_RATE, null);
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
String name = mediaCodecList.findEncoderForFormat(mediaFormat);
Log.d(TAG, "name is " + name); // OMX.google.aac.encoder
try {
    mediaCodec = MediaCodec.createByCodecName(name);
} catch (IOException e) {
    e.printStackTrace();
}

也可以调用 MediaCodec.createDecoder/EncoderByType(String) 传入要处理数据的 MIME type 来创建。

try {
    // 5.0 之前可以这样写,aac 编解码一般都支持
    mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (IOException e) {
    e.printStackTrace();
}

初始化

调用 configue 方法,如果需要异步处理 buffer 可以先调用 setCallback 方法设置回调。

数据处理

输入输出 buffer 是用 buffer-ID 来标识的。调用 start 后通过 dequeueInput/OutputBuffer(…) 方法拿到一个 buffer。异步模式需要在 MediaCodec.Callback.onInput/OutputBufferAvailable(…) 回调中拿到 buffer。
拿到输出 buffer 的数据处理完毕后要调用 releaseOutputBuffer 将 buffer 释放回 codec 中。

输入输出 buffer 用完后都要及时提交到/释放回 codec 。毕竟 codec 的 buffer 数量是有限的,如果占满了,肯定就没法处理了。输入 buffer 被占满后 dequeueInputBuffer 会一直返回 -1, 输出 buffer 占满后 dequeueOutputBuffer 会一直返回 -1

5.0 之后官方推荐以异步方式处理 buffer

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     // 拿到一个输入 buffer -> 填充数据 ->入队交给 codec 处理
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 拿出一个 codec 处理完的输出 buffer -> 处理 -> 释放
     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();

同步处理方式(不推荐)

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();

同步方式使用 ByteBuffer 数组获取 buffer (已废弃)

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     // fill inputBuffers[inputBufferId] with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     // outputBuffers[outputBufferId] is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
   }
 }
codec.stop();
codec.release();

如果需要兼容 4.X 版本,还得用上面的方法。

End-of-stream Handling

当到达输入末尾时,在调用 queueInputBuffer 时要在参数里面写一个 BUFFER_FLAG_END_OF_STREAM 的标志,表示输入完毕了。标志可以写在最后一个 buffer 中,也可以最后再专门提交一个空的 buffer (没有可用数据)。如果用空 buffer ,buffer 的 timestamp 会被忽略。

输入结束后 codec 就不再接受输入了,但会继续产生输出。输出处理完毕后也会在最后一个有用 buffer 或 空buffer 中含有 end-of-stream 的标志。可以用这个标志来标识 codec 处理完毕。通过 MediaCodec.BufferInfo 可以拿到 buffer 的 flag。

发出 end-of-stream 的 buffer 后就不要再提交 Input buffer 了。除非 codec 被 flushed, or stopped and restarted。

Using an Output Surface

codec 的输出也可以直接关联到一个 Surface 上。如视频解码后可以直接渲染到 SurfaceView 上。但这时 output buffers 就不可用了。getOutputBuffer/Image(int) 会返回 null。getOutputBuffers() 也会返回一个全是 null 的数组。

你可以选择是否直接把输出渲染到 Surface 上。

  • Do not render the buffer: Call releaseOutputBuffer(bufferId, false).
  • Render the buffer with the default timestamp: Call releaseOutputBuffer(bufferId, true).
  • Render the buffer with a specific timestamp: Call releaseOutputBuffer(bufferId, timestamp).

Using an Input Surface

也可以用 Surface 作为 codec 的输入,同理这时 input buffer 就不可用了,调用 dequeueInputBuffer 会抛异常。

调用 signalEndOfInputStream() 后 surface 会停止向 codec 发送数据。

猜你喜欢

转载自blog.csdn.net/weixin_34357267/article/details/87579094

相关文章