Android音视频开发:MediaCodec使用指南

Android 音视频开发:MediaCodec 使用指南

在 Android 音视频开发领域,MediaCodec 类扮演着至关重要的角色。它是一个底层 API,用于访问 Android 设备的硬件或软件编解码器,允许开发者对音视频数据进行编码(Encode)和解码(Decode)操作。无论是开发视频播放器、实时视频流应用,还是视频编辑工具,MediaCodec 都是核心组件之一。

本文将深入探讨 MediaCodec 的使用方法,包括其工作原理、配置流程、同步与异步处理模式、常见问题及优化策略,旨在帮助开发者更好地掌握这一强大工具。

1. MediaCodec 概述

1.1 什么是 MediaCodec?

MediaCodec 是 Android Framework 提供的一个用于访问底层媒体编解码器(Codec)的类。这里的编解码器可以是硬件实现的(通常由设备制造商提供,性能更优),也可以是软件实现的(兼容性更好)。

MediaCodec 的主要功能:

  • 解码(Decode):将压缩的音视频数据(如 H.264、AAC)转换为原始的、未压缩的音视频帧(如 YUV 视频帧、PCM 音频帧)。
  • 编码(Encode):将原始的音视频帧转换为压缩的音视频数据,以便于存储或传输。

1.2 MediaCodec 的优势

  • 硬件加速:利用设备的硬件编解码器,可以实现高效的编解码操作,降低 CPU 占用率,节省电量。
  • 底层控制MediaCodec 提供了对编解码过程的底层控制,开发者可以精细地调整编解码参数,以满足不同的需求。
  • 灵活性:支持多种音视频格式,可以根据需要选择合适的编解码器。

1.3 MediaCodec 的工作原理

MediaCodec 的工作流程可以概括为以下几个步骤:

  1. 创建与配置:根据需要编解码的媒体格式(MIME 类型),创建一个 MediaCodec 实例,并进行相应的配置,如设置分辨率、帧率、码率等。

  2. 输入缓冲区(Input Buffers)MediaCodec 内部维护了一组输入缓冲区。对于解码器,开发者需要将待解码的压缩数据填入这些缓冲区;对于编码器,开发者需要将原始的音视频帧填入这些缓冲区。

  3. 输出缓冲区(Output Buffers)MediaCodec 内部也维护了一组输出缓冲区。对于解码器,解码后的原始帧会被放入这些缓冲区;对于编码器,编码后的压缩数据会被放入这些缓冲区。

  4. 数据处理循环

    • MediaCodec 获取一个可用的输入缓冲区索引。
    • 将数据填入该输入缓冲区。
    • 将该输入缓冲区提交给 MediaCodec 进行处理。
    • MediaCodec 获取一个已处理完成的输出缓冲区索引。
    • 从该输出缓冲区中读取处理后的数据(解码后的帧或编码后的数据)。
    • 释放该输出缓冲区。
    • 重复以上步骤,直到所有数据处理完毕。
  5. 停止与释放:当编解码操作完成后,停止 MediaCodec,并释放相关资源。

下图展示了 MediaCodec 的基本数据流:

+---------------------+
| Input Data |
+---------------------+
|
V
+-----------------+ +---------------------+ +-----------------+
| Input Buffers | --> | MediaCodec | --> | Output Buffers |
+-----------------+ +---------------------+ +-----------------+
^
|
+---------------------+
| Output Data |
+---------------------+

2. MediaCodec 的详细使用步骤

2.1 创建 MediaCodec 实例

创建 MediaCodec 实例有两种方式:

  1. 按名称创建:如果知道具体的编解码器名称,可以使用 MediaCodec.createByCodecName(String name) 方法。

    java
    String codecName = "OMX.qcom.video.encoder.avc"; // 示例:高通的 H.264 编码器
    MediaCodec encoder = MediaCodec.createByCodecName(codecName);

  2. 按类型创建:更常见的方式是根据 MIME 类型创建,让系统自动选择合适的编解码器。

    java
    String mimeType = "video/avc"; // H.264 视频的 MIME 类型
    MediaCodec decoder = MediaCodec.createDecoderByType(mimeType); // 创建解码器
    MediaCodec encoder = MediaCodec.createEncoderByType(mimeType); // 创建编码器

2.2 配置 MediaCodec

创建 MediaCodec 实例后,需要对其进行配置。配置是通过 MediaFormat 对象来完成的。MediaFormat 包含了描述媒体数据格式的各种键值对。

```java
// 创建 MediaFormat 对象
MediaFormat format = new MediaFormat();

// 设置 MIME 类型
format.setString(MediaFormat.KEY_MIME, mimeType);

// 对于视频,通常需要设置以下参数:
format.setInteger(MediaFormat.KEY_WIDTH, width); // 视频宽度
format.setInteger(MediaFormat.KEY_HEIGHT, height); // 视频高度
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); // 颜色格式(YUV420SP)
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate); // 比特率
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); // 帧率
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); // I 帧间隔

// 对于音频,通常需要设置以下参数:
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelCount); // 声道数
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate); // 采样率
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); // AAC Profile

// 使用 MediaFormat 对象配置 MediaCodec
mediaCodec.configure(format, surface, crypto, flags);
```

参数说明:

  • format:包含媒体格式信息的 MediaFormat 对象。
  • surface:对于解码器,如果要将解码后的视频帧渲染到 Surface 上,需要传入一个 Surface 对象;否则,传入 null。对于编码器,如果要从 Surface 上获取原始视频帧进行编码,需要传入一个 Surface 对象(通过 createInputSurface() 方法创建);否则,传入 null
  • crypto:如果媒体数据是加密的,需要传入一个 MediaCrypto 对象;否则,传入 null
  • flags:配置标志。对于解码器,通常设置为 0;对于编码器,设置为 MediaCodec.CONFIGURE_FLAG_ENCODE

2.3 启动 MediaCodec

配置完成后,需要调用 start() 方法启动 MediaCodec

java
mediaCodec.start();

2.4 获取输入输出缓冲区

MediaCodec 使用一组输入缓冲区和一组输出缓冲区来处理数据。开发者需要通过以下方法获取这些缓冲区的索引:

  • dequeueInputBuffer(long timeoutUs):获取一个可用的输入缓冲区索引。
  • dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs):获取一个已处理完成的输出缓冲区索引。

timeoutUs 参数指定了超时时间(单位:微秒)。如果设置为 0,表示立即返回;如果设置为 -1,表示无限期等待,直到有可用的缓冲区;如果设置为正数,表示等待指定的时长。

2.5 数据处理循环

数据处理循环是 MediaCodec 使用的核心。以下是一个典型的解码器数据处理循环示例:

```java
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
boolean sawInputEOS = false; // 输入结束标志
boolean sawOutputEOS = false; // 输出结束标志

while (!sawOutputEOS) {
if (!sawInputEOS) {
// 1. 获取可用的输入缓冲区索引
int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeoutUs);
if (inputBufferIndex >= 0) {
// 2. 获取输入缓冲区
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
// 3. 将待解码的数据填入输入缓冲区
int sampleSize = extractor.readSampleData(inputBuffer, 0); // 从 MediaExtractor 读取数据
if (sampleSize < 0) {
// 没有更多数据,发送 EOS 信号
mediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS = true;
} else {
long presentationTimeUs = extractor.getSampleTime();
// 4. 将输入缓冲区提交给 MediaCodec
mediaCodec.queueInputBuffer(inputBufferIndex, 0, sampleSize, presentationTimeUs, 0);
extractor.advance(); // 移动到下一个采样
}
}
}

// 5. 获取已处理完成的输出缓冲区索引
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, timeoutUs);
switch (outputBufferIndex) {
    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
        // 输出格式发生变化,通常需要重新配置渲染器
        MediaFormat newFormat = mediaCodec.getOutputFormat();
        // ...
        break;
    case MediaCodec.INFO_TRY_AGAIN_LATER:
        // 超时,稍后重试
        break;
    default:
        if (outputBufferIndex >= 0) {
            // 6. 获取输出缓冲区
            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
            // 7. 处理输出缓冲区中的数据(例如,渲染到 Surface 或写入文件)
            // ...

            // 8. 释放输出缓冲区
            mediaCodec.releaseOutputBuffer(outputBufferIndex, render); // render 参数指示是否渲染到 Surface
        }
        break;
}

// 检查是否已到达输出流的末尾
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    sawOutputEOS = true;
}

}
```

编码器的数据处理循环与解码器类似,主要区别在于:

  • 输入数据是原始的音视频帧。
  • 输出数据是压缩后的音视频数据。
  • 可能需要使用 createInputSurface() 创建一个 Surface 作为输入源。

2.6 停止与释放

当编解码操作完成后,需要调用 stop() 方法停止 MediaCodec,并调用 release() 方法释放相关资源。

java
mediaCodec.stop();
mediaCodec.release();
mediaCodec = null;

3. 同步与异步处理模式

MediaCodec 支持两种处理模式:

  • 同步模式(Synchronous Mode):使用 dequeueInputBuffer()dequeueOutputBuffer() 方法同步地获取缓冲区。这种模式下,调用会阻塞,直到有可用的缓冲区或超时。

  • 异步模式(Asynchronous Mode):使用 setCallback() 方法设置一个回调对象。当有输入缓冲区可用、输出缓冲区可用、发生错误或输出格式更改时,MediaCodec 会通过回调方法通知应用。

异步模式可以避免阻塞主线程,提高应用的响应性。以下是异步模式的示例:

```java
mediaCodec.setCallback(new MediaCodec.Callback() {
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
// 处理输入缓冲区
}

@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {
    // 处理输出缓冲区
}

@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
    // 处理错误
}

@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
    // 处理输出格式更改
}

});
```

4. 常见问题及优化策略

4.1 兼容性问题

不同的 Android 设备可能支持不同的编解码器和颜色格式。在创建 MediaCodec 实例之前,最好先查询设备支持的编解码器列表,并选择合适的编解码器。

可以使用 MediaCodecList 类来查询设备支持的编解码器:

java
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
MediaCodecInfo[] codecInfos = codecList.getCodecInfos();
for (MediaCodecInfo codecInfo : codecInfos) {
if (codecInfo.isEncoder()) { // 检查是否为编码器
// ...
}
String[] supportedTypes = codecInfo.getSupportedTypes(); // 获取支持的 MIME 类型
// ...
MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType); // 获取特定 MIME 类型的能力
// ...
}

4.2 颜色格式处理

不同的编解码器可能支持不同的颜色格式。常见的颜色格式有:

  • COLOR_FormatYUV420Planar (I420)
  • COLOR_FormatYUV420SemiPlanar (NV12)
  • COLOR_FormatYUV420PackedPlanar (YV12)
  • COLOR_FormatYUV420PackedSemiPlanar (NV21)

在配置 MediaCodec 时,需要选择一个编解码器支持的颜色格式。如果需要进行颜色格式转换,可以使用 Image 类(API 21+)或自行编写转换代码。

4.3 性能优化

  • 使用硬件编解码器:尽可能使用硬件编解码器,以提高性能和降低功耗。
  • 避免频繁创建和销毁 MediaCodec 实例:创建和销毁 MediaCodec 实例是比较耗时的操作。如果需要频繁地进行编解码操作,可以考虑复用 MediaCodec 实例。
  • 合理设置缓冲区大小:根据实际需要调整缓冲区大小,避免过大或过小。
  • 使用异步模式:异步模式可以避免阻塞主线程,提高应用的响应性。
  • 合理设置关键帧间隔:关键帧(I 帧)的间隔会影响视频的质量和大小。根据实际需要调整关键帧间隔。
  • 合理设置比特率:比特率越高,视频质量越好,但文件大小也越大。根据实际需要调整比特率。

5. 总结

MediaCodec 是 Android 平台提供的强大工具,用于进行音视频编解码操作。本文详细介绍了 MediaCodec 的工作原理、使用步骤、同步与异步处理模式、常见问题及优化策略。希望本文能够帮助开发者更好地理解和使用 MediaCodec,开发出高质量的音视频应用。

在实际开发中,还需要结合具体的应用场景和需求,灵活运用 MediaCodec 的各项功能,并进行充分的测试和优化,才能达到最佳效果。

THE END