4. MediaMuxer与MediaCodec的协同工作:编码后的数据如何送入Muxer
好,咱们接着聊。上一节我们把MediaCodec的编码流程跑通了,但有个关键问题:编码出来的H.264或AAC数据,怎么才能封装成MP4文件?
说白了,MediaCodec负责“生产”,MediaMuxer负责“包装”。这两兄弟得配合好,才能产出最终的视频文件。我在早期做项目时,就吃过它们配合不好的亏——编码器都输出数据了,Muxer却迟迟不写入,最后文件打不开。嗯,今天咱们就把这个协同机制彻底讲清楚。
4.1 核心流程:生产者-消费者模型
MediaCodec和MediaMuxer的关系,其实就是典型的生产者-消费者模型:
- 生产者:MediaCodec编码器,输出编码后的数据包(ByteBuffer)
- 消费者:MediaMuxer封装器,接收数据包并写入MP4容器
- 传输通道:MediaCodec的output buffer + MediaMuxer的writeSampleData方法
你想想看,如果生产者跑得比消费者快,buffer就会堆积。反之,消费者等生产者,就会卡顿。所以,我们需要一个合理的同步机制。
关键点:MediaCodec输出的数据,必须携带正确的BufferInfo信息(时间戳、标志位等),MediaMuxer才能正确封装。
4.2 数据传递的完整链路
我个人习惯把这条链路分成5个步骤,每一步都有坑:
- 编码器输出:MediaCodec.dequeueOutputBuffer() 拿到编码后的数据
- 提取BufferInfo:获取时间戳、数据大小、关键帧标志
- 轨道匹配:将数据送入对应的轨道(视频轨或音频轨)
- 写入Muxer:调用MediaMuxer.writeSampleData()
- 释放buffer:释放编码器输出buffer,继续下一轮
这里有个容易忽略的细节:时间戳必须单调递增。我曾经在项目里因为时间戳回退,导致Muxer直接报错,查了半天才发现是编码器配置的问题。
4.3 核心代码骨架
咱们直接看代码。这是我从实际项目中提炼出来的核心逻辑:
// 假设已经创建了MediaCodec编码器和MediaMuxer
MediaCodec encoder = ...;
MediaMuxer muxer = ...;
int trackIndex = ...; // 通过addTrack获取的轨道索引
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
ByteBuffer[] outputBuffers = encoder.getOutputBuffers();
while (isEncoding) {
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
// 暂时没有输出,稍后重试
continue;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 编码器输出格式变化(通常发生在开始编码时)
MediaFormat newFormat = encoder.getOutputFormat();
trackIndex = muxer.addTrack(newFormat);
muxer.start();
continue;
}
if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
// 跳过编码器配置数据(SPS/PPS等),Muxer会自动处理
bufferInfo.size = 0;
}
if (bufferInfo.size > 0) {
// 关键一步:将数据写入Muxer
outputBuffers[outputIndex].position(bufferInfo.offset);
outputBuffers[outputIndex].limit(bufferInfo.offset + bufferInfo.size);
muxer.writeSampleData(trackIndex, outputBuffers[outputIndex], bufferInfo);
}
// 释放buffer,还给编码器
encoder.releaseOutputBuffer(outputIndex, false);
// 检查是否结束
if (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
break;
}
}
小技巧:INFO_OUTPUT_FORMAT_CHANGED事件只会在编码开始时触发一次。如果你在编码过程中修改了参数,可能会再次触发。我建议在收到这个事件后再调用muxer.start(),否则会报错。
4.4 时间戳:最容易出错的环节
时间戳是MediaCodec和MediaMuxer协同中最容易出问题的地方。为什么?
- MediaCodec输出的时间戳单位是微秒(µs)
- MediaMuxer内部处理时,期望时间戳是单调递增的
- 不同编码器(硬件vs软件)的时间戳精度可能不同
我曾经遇到过一个bug:用某款国产手机编码时,时间戳偶尔会跳变,导致MP4文件播放时画面卡顿。后来发现是编码器驱动的问题,需要在写入Muxer前做一次时间戳校准。
避坑指南:千万不要假设编码器输出的时间戳是完美的。建议在写入Muxer前,自己维护一个时间戳计数器,或者对时间戳做一次线性化处理。
4.5 流程图:数据从编码器到Muxer的完整旅程
下面这张图,我画的是数据从MediaCodec到MediaMuxer的完整流转过程。你看一眼就能明白整个协同机制:
4.6 轨道添加的时机
有个细节很多人会搞错:什么时候调用muxer.addTrack()?
答案是:在收到编码器的INFO_OUTPUT_FORMAT_CHANGED事件之后。因为此时编码器才真正确定了输出格式(分辨率、帧率、码率等)。
我记得有个同事,在编码器还没准备好时就调了addTrack,结果传进去的MediaFormat是空的,Muxer直接崩溃。嗯,这个顺序一定要记牢。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 创建MediaCodec编码器 | 配置好输入格式 |
| 2 | 创建MediaMuxer | 指定输出文件路径 |
| 3 | 等待INFO_OUTPUT_FORMAT_CHANGED | 编码器输出格式确定 |
| 4 | 调用muxer.addTrack() | 传入编码器的输出格式 |
| 5 | 调用muxer.start() | 开始封装 |
| 6 | 循环写入数据 | writeSampleData |
| 7 | 调用muxer.stop()和release() | 完成封装 |
4.7 多轨道同步写入
如果你同时处理视频轨和音频轨,事情就更有意思了。两个编码器各自输出数据,Muxer需要同时接收两个轨道的数据。
我建议的做法是:用一个循环同时处理两个编码器的输出。不要先处理完视频再处理音频,那样会导致音画不同步。
// 伪代码:双轨道同步写入
while (!videoDone || !audioDone) {
// 处理视频输出
int videoIndex = videoEncoder.dequeueOutputBuffer(videoInfo, 0);
if (videoIndex >= 0) {
muxer.writeSampleData(videoTrack, videoBuffer, videoInfo);
videoEncoder.releaseOutputBuffer(videoIndex, false);
}
// 处理音频输出
int audioIndex = audioEncoder.dequeueOutputBuffer(audioInfo, 0);
if (audioIndex >= 0) {
muxer.writeSampleData(audioTrack, audioBuffer, audioInfo);
audioEncoder.releaseOutputBuffer(audioIndex, false);
}
}
核心原则:MediaCodec和MediaMuxer的协同,本质上是数据流的正确传递。只要保证时间戳正确、轨道匹配正确、释放时机正确,就能产出标准的MP4文件。
好了,这一节的内容就到这里。MediaMuxer和MediaCodec的配合,说白了就是“编码器吐数据,Muxer吃数据”。中间那个BufferInfo就是它们的通信协议。搞懂了这一点,后面遇到各种封装问题,你都能找到排查方向。
公众号:蓝海资料掘金营,微信deep3321