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. 完整生命周期流程图
下面这张图把整个流程串起来了,你可以保存下来对照着写代码:
6. 各方法对比一览
| 方法 | 调用时机 | 可调用次数 | 常见错误 |
|---|---|---|---|
| addTrack | 构造之后、start 之前 | 每个轨道一次 | start 后调用抛异常 |
| start | 所有 addTrack 之后 | 仅一次 | 重复调用抛异常 |
| writeSampleData | start 之后、stop 之前 | 任意次数 | 时间戳乱序、buffer 非 direct |
| stop | 所有数据写入完毕 | 仅一次 | 忘记调用导致文件损坏 |
| release | stop 之后 | 仅一次 | 忘记调用导致资源泄漏 |
个人经验:我建议你在写代码时,把 MediaMuxer 的状态管理封装成一个状态机。用一个 int 变量记录当前状态(IDLE、TRACK_ADDED、STARTED、STOPPED),在每个方法入口做断言检查。这样能避免很多运行时错误。
好了,核心 API 就这些。说白了就是四个步骤:注册轨道、启动、写入数据、收尾。你只要记住每个方法的调用顺序和限制条件,MediaMuxer 用起来其实挺简单的。