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个步骤,每一步都有坑:

  1. 编码器输出:MediaCodec.dequeueOutputBuffer() 拿到编码后的数据
  2. 提取BufferInfo:获取时间戳、数据大小、关键帧标志
  3. 轨道匹配:将数据送入对应的轨道(视频轨或音频轨)
  4. 写入Muxer:调用MediaMuxer.writeSampleData()
  5. 释放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的完整流转过程。你看一眼就能明白整个协同机制:

MediaCodec → MediaMuxer 数据流转图 MediaCodec 编码器(生产者) Output Buffer + BufferInfo MediaMuxer 封装器(消费者) dequeue writeSampleData BufferInfo 关键字段 • offset:数据在buffer中的起始位置 • size:有效数据长度(字节) • presentationTimeUs:时间戳(微秒) • flags:标志位(关键帧/结束符等) ⚠️ 常见陷阱 1. 时间戳必须单调递增,不能回退 2. 必须先 addTrack 再 start()

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