以下为Android平台使用MediaCodec
实现视频文件解码→转码→压缩的全流程技术原理与核心代码解析,结合硬件加速与参数优化策略,涵盖关键状态机管理与性能陷阱规避方案。
🔧 一、全流程架构与核心组件
-
组件分工
- MediaExtractor:分离视频文件的音视频轨道,提取编码数据(如H.264)
- MediaCodec:硬件加速编解码器,负责解码原始数据→处理→重新编码
- MediaMuxer:封装编码后的数据为MP4等容器格式
-
流程概览
graph LR A[输入视频] --> B(MediaExtractor提取轨道) B --> C{选择视频轨道} C --> D[MediaCodec解码] D --> E[帧处理/压缩] E --> F[MediaCodec编码] F --> G[MediaMuxer封装] G --> H[输出视频]
⚙️ 二、解码原理与代码实现
1. 初始化解码器
ini
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(inputPath); // 输入文件路径
int videoTrackIndex = -1;
// 遍历轨道并选择视频轨道
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrackIndex = i;
extractor.selectTrack(i); // 选中轨道
break;
}
}
// 配置解码器(以H.264为例)
MediaCodec decoder = MediaCodec.createDecoderByType("video/avc");
decoder.configure(extractor.getTrackFormat(videoTrackIndex), null, null, 0); // Surface设为null
decoder.start();
2. 解码循环(关键步骤)
ini
ByteBuffer[] inputBuffers = decoder.getInputBuffers();
ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
boolean eos = false;
while (!eos) {
// 提交压缩数据到解码器
int inputBufferIndex = decoder.dequeueInputBuffer(10000);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {
decoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
eos = true;
} else {
decoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance(); // 移动到下一帧
}
}
// 获取解码后的原始帧(YUV格式)
int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);
if (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
// 此处可插入帧处理(如缩放、滤镜等)
decoder.releaseOutputBuffer(outputBufferIndex, false); // 不渲染到Surface
}
}
技术要点:
- 若需处理解码后的YUV数据,需通过
outputBuffer
获取原始帧- 结束标志
BUFFER_FLAG_END_OF_STREAM
必须发送,否则解码器会阻塞
🎚️ 三、转码压缩原理与参数配置
1. 编码器初始化(关键参数)
ini
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", targetWidth, targetHeight);
// 压缩核心参数
format.setInteger(MediaFormat.KEY_BIT_RATE, 1_000_000); // 1Mbps码率
format.setInteger(MediaFormat.KEY_FRAME_RATE, 25); // 帧率
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 关键帧间隔(秒)
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encoder.start();
2. 压缩参数影响分析
参数 | 典型值范围 | 压缩效果 |
---|---|---|
分辨率 | 1080p→720p | 像素量减少50%,文件体积显著下降 |
码率 (KEY_BIT_RATE) | 5Mbps→1Mbps | 码率降低80%,可能引入画面模糊 |
帧率 | 30fps→24fps | 动态细节减少,体积下降约20% |
关键帧间隔 | 1秒→5秒 | GOP增大,压缩率提升,但快进/seek可能卡顿 |
注意 :颜色格式需通过
MediaCodecInfo
查询设备支持,优先选COLOR_FormatYUV420Flexible
🔄 四、转码全流程桥接(解码→编码→封装)
1. MediaMuxer初始化
ini
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int videoTrack = -1; // 等待编码器输出格式确定后再添加轨道
2. 数据流转核心循环
ini
while (true) {
// 从解码器获取原始帧(YUV)
int decoderOutputIndex = decoder.dequeueOutputBuffer(bufferInfo, TIMEOUT);
if (decoderOutputIndex >= 0) {
ByteBuffer rawFrame = decoder.getOutputBuffer(decoderOutputIndex);
// 提交原始帧到编码器
int encoderInputIndex = encoder.dequeueInputBuffer(TIMEOUT);
if (encoderInputIndex >= 0) {
ByteBuffer encoderInputBuffer = encoder.getInputBuffer(encoderInputIndex);
encoderInputBuffer.put(rawFrame);
encoder.queueInputBuffer(encoderInputIndex, 0, bufferInfo.size, bufferInfo.presentationTimeUs, 0);
}
decoder.releaseOutputBuffer(decoderOutputIndex, false);
}
// 从编码器获取压缩数据
int encoderOutputIndex = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT);
if (encoderOutputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
videoTrack = muxer.addTrack(encoder.getOutputFormat()); // 添加轨道
muxer.start();
} else if (encoderOutputIndex >= 0) {
ByteBuffer encodedData = encoder.getOutputBuffer(encoderOutputIndex);
muxer.writeSampleData(videoTrack, encodedData, bufferInfo); // 写入文件
encoder.releaseOutputBuffer(encoderOutputIndex, false);
}
}
关键逻辑:
- 编码器输出格式变化(
INFO_OUTPUT_FORMAT_CHANGED
)时必须通知MediaMuxer- 时间戳
presentationTimeUs
需保持连续性,否则播放异常
🚀 五、性能优化与进阶技巧
-
异步模式(API 21+)
避免轮询阻塞,使用
Callback
提升效率:lessencoder.setCallback(new MediaCodec.Callback() { @Override public void onInputBufferAvailable(MediaCodec mc, int index) { // 填充数据到编码器 } @Override public void onOutputBufferAvailable(MediaCodec mc, int index, BufferInfo info) { // 处理压缩数据并提交给MediaMuxer } });
-
Surface输入(零拷贝)
直接绑定
Surface
到编码器,跳过CPU处理:iniSurface inputSurface = encoder.createInputSurface(); // 将解码器输出渲染到inputSurface(需OpenGL ES上下文)
-
动态码率调整
根据网络或存储需求实时调整码率:
iniBundle params = new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitrate); encoder.setParameters(params);
⚠️ 六、常见问题解决
-
绿屏/花屏
- 原因:颜色格式不匹配或帧数据未对齐
- 方案:确认设备支持的
COLOR_FORMAT
,YUV数据需满足分辨率对齐要求(通常是2的倍数)
-
帧率骤降
- 原因:编码器处理超时
- 优化:降低分辨率/码率,或使用
Surface
输入跳过YUV转换
-
封装失败
- 原因:未添加轨道或时间戳错误
- 检查:确保在
INFO_OUTPUT_FORMAT_CHANGED
后才启动MediaMuxer
💎 总结与最佳实践
-
核心流程 :
Extractor→Decoder→Frame Processing→Encoder→Muxer
-
参数黄金法则:分辨率 > 码率 > 帧率 > GOP,按需求牺牲画质换体积
-
必选优化:
- 异步模式处理I/O避免阻塞
- Surface输入减少CPU-YUV拷贝
- 动态码率适应场景需求
完整代码参考:Android MediaCodec官方示例
深入性能调优:分析
dumpsys media.codec
获取编解码器实时状态