第25章 音视频录制系统:Camera2采集、音频采集、同步录制、MP4封装
做音视频开发这么多年,我始终觉得录制系统是最能体现综合功力的模块。你想想看,要同时管好摄像头、麦克风,还得让它们步调一致地往MP4里写数据——任何一个环节出问题,录出来的视频不是没声音就是音画不同步。今天我就把这一整套流程拆开揉碎了讲给你听。
核心要点:一个完整的录制系统 = Camera2视频采集 + AudioRecord音频采集 + 同步时钟 + MediaMuxer封装。四者缺一不可。
25.1 整体架构设计
先看整体流程。我个人习惯把录制系统分成三层:
- 采集层:Camera2负责视频帧,AudioRecord负责音频数据
- 同步层:用MediaCodec编码后,通过时间戳对齐
- 封装层:MediaMuxer把编码后的音视频流写入MP4
下面这张图能帮你快速建立全局认知:
25.2 Camera2 视频采集实战
Camera2 的 API 设计得比较灵活,但初次接触容易懵。我建议你抓住三个核心对象:CameraManager、CameraDevice、CaptureSession。
先看初始化流程:
// 获取 CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
// 打开摄像头
manager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
// 创建 CaptureSession
camera.createCaptureSession(Arrays.asList(surface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
// 开始预览/录制
session.setRepeatingRequest(request, callback, handler);
}
}, handler);
}
}, handler);
我的经验:创建 CaptureSession 时,surface 列表的顺序很重要。第一个 surface 通常用于预览,第二个用于录制。如果你把顺序搞反了,预览画面可能黑屏——我踩过这个坑。
视频采集的关键参数设置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 分辨率 | 1920x1080 | 1080p 是兼容性最好的选择 |
| 帧率 | 30fps | 大部分设备稳定支持 |
| 编码格式 | H.264 | 兼容性优于 H.265 |
| 码率 | 8-12 Mbps | 1080p 下画质与文件大小的平衡点 |
25.3 音频采集与编码
音频采集我用的是 AudioRecord,配合 MediaCodec 做 AAC 编码。这里有个细节:音频的采样率最好和视频的帧率形成整数倍关系,比如 44100Hz 配 30fps,这样时间戳计算起来更干净。
// 音频采集配置
int sampleRate = 44100;
int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
AudioRecord recorder = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
);
// 启动采集线程
new Thread(() -> {
recorder.startRecording();
byte[] buffer = new byte[4096];
while (isRecording) {
int bytesRead = recorder.read(buffer, 0, buffer.length);
// 送入 MediaCodec 编码
feedToEncoder(buffer, bytesRead);
}
}).start();
注意:音频采集线程不能和 UI 线程混在一起。我曾经在项目里直接在主线程读音频数据,结果录了不到 10 秒就 ANR 了。音频采集必须开独立线程。
25.4 音画同步的核心机制
说白了,音画同步就是让视频帧和音频帧的时间戳对齐。MediaCodec 编码后的输出 buffer 会带一个 presentationTimeUs 字段,这个就是时间戳。
我的做法是这样的:
- 视频帧的时间戳用
System.nanoTime()换算成微秒 - 音频帧的时间戳也用同一个时钟源
- 写入 MediaMuxer 时,确保两个 track 的时间戳是单调递增的
// 视频编码输出回调
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outputIndex = videoCodec.dequeueOutputBuffer(info, TIMEOUT_US);
if (outputIndex >= 0) {
ByteBuffer outputData = videoCodec.getOutputBuffer(outputIndex);
// 关键:时间戳必须单调递增
if (info.presentationTimeUs < lastVideoTimestamp) {
info.presentationTimeUs = lastVideoTimestamp + 1;
}
lastVideoTimestamp = info.presentationTimeUs;
muxer.writeSampleData(videoTrackIndex, outputData, info);
videoCodec.releaseOutputBuffer(outputIndex, false);
}
避坑指南:有些设备上 MediaCodec 输出的时间戳会跳变。我曾经在小米某款机型上遇到过时间戳回退的情况,导致 MP4 文件损坏。解决方案就是上面代码里那样——手动检查并修正时间戳的单调性。
25.5 MediaMuxer 封装 MP4
MediaMuxer 的使用相对简单,但有几个点要注意:
- 必须先 addTrack 再 start,顺序不能乱
- 视频 track 和音频 track 的添加顺序不影响播放
- stop 之后不能再写入数据
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 添加视频轨
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 8000000);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
int videoTrackIndex = muxer.addTrack(videoFormat);
// 添加音频轨
MediaFormat audioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", sampleRate, 2);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
int audioTrackIndex = muxer.addTrack(audioFormat);
// 开始封装
muxer.start();
// ... 写入数据 ...
// 结束封装
muxer.stop();
muxer.release();
个人建议:录制过程中如果发生异常,一定要在 finally 块里调用 muxer.stop() 和 muxer.release()。否则生成的 MP4 文件可能无法播放。我见过太多因为忘记释放资源导致文件损坏的案例了。
25.6 完整录制流程的状态机
录制系统本质上是一个状态机。我习惯用四个状态来管理:
| 状态 | 说明 | 关键操作 |
|---|---|---|
| IDLE | 空闲状态 | 等待用户点击录制按钮 |
| RECORDING | 录制中 | 音视频同时采集、编码、封装 |
| PAUSED | 暂停录制 | 停止采集但保留编码器状态 |
| STOPPING | 正在停止 | 等待编码器 flush 完成,释放资源 |
嗯,这里要注意:暂停功能在 MediaMuxer 里没有直接支持。我的做法是暂停时停止向 muxer 写数据,但保留编码器。恢复时重新开始采集,时间戳继续累加。这样生成的 MP4 文件在播放器里看起来就是一段完整的视频。
做录制系统这么多年,我最大的体会是:稳定性比功能更重要。用户宁可要一个功能少但每次都能录成功的 App,也不要一个功能花哨但经常崩溃的 App。所以,异常处理、资源释放、状态管理这些看似枯燥的代码,恰恰是最值得花时间打磨的地方。
公众号:蓝海资料掘金营,微信deep3321