MediaMuxer 核心 API 详解:addTrack、start、writeSampleData、stop

上一章我们聊了 MediaMuxer 是什么,说白了它就是个「封装器」,把视频流、音频流打包成 MP4 文件。今天咱们直接上手,把它的几个核心方法掰开揉碎讲清楚。

我个人习惯把 MediaMuxer 的生命周期分成四个阶段:准备 → 启动 → 写入 → 结束。对应的 API 就是 addTrack、start、writeSampleData、stop。嗯,咱们一个一个来。

1. addTrack:告诉 Muxer 你要封装什么

这个方法的作用,就是向 MediaMuxer 注册一个轨道。你想想看,一个 MP4 文件里可能有视频轨道、音频轨道,甚至字幕轨道。每个轨道都需要先「打个招呼」。

// 创建 MediaFormat,描述视频编码格式
MediaFormat videoFormat = MediaFormat.createVideoFormat(
    MediaFormat.MIME_TYPE_AVC,  // H.264
    1920, 1080
);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 5000000);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

// 注册轨道
int videoTrackIndex = muxer.addTrack(videoFormat);

关键点:addTrack 返回一个 int 类型的 trackIndex。这个索引很重要,后面 writeSampleData 的时候要指定往哪个轨道写数据。

我在项目中遇到过一个问题:addTrack 必须在 start 之前调用,而且调用顺序决定了 trackIndex 的顺序。如果你先 add 视频再 add 音频,那视频轨道的 index 就是 0,音频是 1。这个顺序在后续写入时一定要记清楚。

注意:addTrack 只能调用一次?不对。你可以调用多次,每个轨道调一次。但一旦调用了 start,就不能再 addTrack 了。我曾经在调试时犯过这个错——先 start 了再想加字幕轨道,结果抛了 IllegalStateException。

2. start:正式开启封装流程

start 方法很简单,没有参数,调用后 MediaMuxer 就进入「接收数据」状态。但这里有个坑——start 必须在所有 addTrack 完成之后调用。

muxer.start();

就一行代码?对,就一行。但这一行背后,MediaMuxer 会初始化 MP4 文件的头部结构,准备好写入容器。如果你在 start 之前调用 writeSampleData,会直接崩溃。

我记得有一次,同事在异步回调里调用了 start,但回调时机不对,导致 writeSampleData 先执行了。嗯,那场面……直接 crash。所以我的建议是:把 start 放在所有 addTrack 之后、写入循环之前,用同步的方式调用

3. writeSampleData:核心中的核心

这个方法才是真正干活的。它把编码后的数据(比如 H.264 的 NAL 单元、AAC 的帧)写入到 MP4 容器里。

// 从 MediaCodec 的输出缓冲区拿到数据
ByteBuffer outputBuffer = codecOutputBuffers[outputIndex];
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
// ... 解码器填充 bufferInfo ...

// 写入到 Muxer
muxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo);

这里要注意三个参数:

  • trackIndex:addTrack 返回的那个索引,告诉 Muxer 数据属于哪个轨道
  • byteBuf:编码后的数据,必须是 direct ByteBuffer
  • bufferInfo:包含四个关键字段——offset、size、presentationTimeUs、flags

避坑指南:我曾经在 writeSampleData 时忘了设置 bufferInfo.flags 里的 BUFFER_FLAG_KEY_FRAME 标志。结果生成的 MP4 文件在播放器里拖进度条时全是花屏。因为播放器找不到关键帧,没法随机访问。

bufferInfo 的 presentationTimeUs 尤其重要。它表示这一帧的显示时间戳,单位是微秒。如果你给的时间戳是乱序的,或者跳跃太大,生成的视频播放时就会卡顿、跳帧。

我个人的习惯是:在编码循环里,每拿到一帧数据,就立即写入 Muxer。不要攒一堆再写,那样容易内存溢出,而且时间戳容易搞乱。

4. stop 与 release:优雅地结束

所有数据写完后,调用 stop 告诉 Muxer「没数据了,收工吧」。然后调用 release 释放资源。

muxer.stop();
muxer.release();

stop 会做几件事:

  • 写入 MP4 文件的 moov box(包含索引信息)
  • 更新文件头中的时长信息
  • 关闭文件写入流

如果你忘了调 stop,生成的 MP4 文件可能无法播放,或者播放时无法拖动进度条。因为 moov box 没写进去,播放器不知道视频有多长、关键帧在哪儿。

警告:stop 之后不能再调用 writeSampleData。如果你想重新封装另一个文件,必须 new 一个新的 MediaMuxer 实例。我之前试图复用同一个 Muxer 对象,结果调了两次 start,直接抛异常。

5. 完整生命周期流程图

下面这张图把整个流程串起来了,你可以保存下来对照着写代码:

MediaMuxer 生命周期流程图 new MediaMuxer() addTrack() 注册轨道 start() 启动封装 writeSampleData() 循环写入 循环 stop() + release()

6. 各方法对比一览

方法 调用时机 可调用次数 常见错误
addTrack 构造之后、start 之前 每个轨道一次 start 后调用抛异常
start 所有 addTrack 之后 仅一次 重复调用抛异常
writeSampleData start 之后、stop 之前 任意次数 时间戳乱序、buffer 非 direct
stop 所有数据写入完毕 仅一次 忘记调用导致文件损坏
release stop 之后 仅一次 忘记调用导致资源泄漏

个人经验:我建议你在写代码时,把 MediaMuxer 的状态管理封装成一个状态机。用一个 int 变量记录当前状态(IDLE、TRACK_ADDED、STARTED、STOPPED),在每个方法入口做断言检查。这样能避免很多运行时错误。

好了,核心 API 就这些。说白了就是四个步骤:注册轨道、启动、写入数据、收尾。你只要记住每个方法的调用顺序和限制条件,MediaMuxer 用起来其实挺简单的。

公众号:蓝海资料掘金营,微信 deep3321