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
的工作流程可以概括为以下几个步骤:
-
创建与配置:根据需要编解码的媒体格式(MIME 类型),创建一个
MediaCodec
实例,并进行相应的配置,如设置分辨率、帧率、码率等。 -
输入缓冲区(Input Buffers):
MediaCodec
内部维护了一组输入缓冲区。对于解码器,开发者需要将待解码的压缩数据填入这些缓冲区;对于编码器,开发者需要将原始的音视频帧填入这些缓冲区。 -
输出缓冲区(Output Buffers):
MediaCodec
内部也维护了一组输出缓冲区。对于解码器,解码后的原始帧会被放入这些缓冲区;对于编码器,编码后的压缩数据会被放入这些缓冲区。 -
数据处理循环:
- 从
MediaCodec
获取一个可用的输入缓冲区索引。 - 将数据填入该输入缓冲区。
- 将该输入缓冲区提交给
MediaCodec
进行处理。 - 从
MediaCodec
获取一个已处理完成的输出缓冲区索引。 - 从该输出缓冲区中读取处理后的数据(解码后的帧或编码后的数据)。
- 释放该输出缓冲区。
- 重复以上步骤,直到所有数据处理完毕。
- 从
-
停止与释放:当编解码操作完成后,停止
MediaCodec
,并释放相关资源。
下图展示了 MediaCodec
的基本数据流:
+---------------------+
| Input Data |
+---------------------+
|
V
+-----------------+ +---------------------+ +-----------------+
| Input Buffers | --> | MediaCodec | --> | Output Buffers |
+-----------------+ +---------------------+ +-----------------+
^
|
+---------------------+
| Output Data |
+---------------------+
2. MediaCodec 的详细使用步骤
2.1 创建 MediaCodec 实例
创建 MediaCodec
实例有两种方式:
-
按名称创建:如果知道具体的编解码器名称,可以使用
MediaCodec.createByCodecName(String name)
方法。java
String codecName = "OMX.qcom.video.encoder.avc"; // 示例:高通的 H.264 编码器
MediaCodec encoder = MediaCodec.createByCodecName(codecName); -
按类型创建:更常见的方式是根据 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
的各项功能,并进行充分的测试和优化,才能达到最佳效果。