16、视频录制进阶:MediaRecorder与MediaCodec混合方案、分段录制、断点续录
各位好,我是老张。今天聊点硬核的——视频录制的进阶玩法。
如果你只是用 MediaRecorder 录个短视频,那确实简单。但一旦遇到「录到一半内存不够」、「用户想暂停继续」、「需要自定义编码参数」这些场景,MediaRecorder 就有点力不从心了。这时候,就得请出 MediaCodec 这位老大哥。
核心思路:MediaRecorder 负责「简单录制」,MediaCodec 负责「精细控制」。两者混搭,才是生产环境下的正确姿势。
16.1 为什么需要混合方案?
MediaRecorder 封装得太好了。你只需要设置音频源、视频源、输出格式、输出文件,然后 start() 就完事。但问题也出在这里——你没法精细控制编码参数,比如码率、帧率、关键帧间隔。
MediaCodec 呢?它给了你完全的控制权。但代价是代码量翻倍,还得自己处理音视频同步、文件封装。说实话,我早期做项目时也踩过这个坑——用纯 MediaCodec 写了个录制模块,结果音视频不同步,调试了整整两天。
所以,我的建议是:用 MediaRecorder 做壳,用 MediaCodec 做核。什么意思?就是让 MediaRecorder 负责文件封装和音视频同步,但通过自定义的 Surface 把视频数据喂给 MediaCodec 做编码。
我的经验:在小米、华为等机型上,纯 MediaRecorder 的编码参数往往被厂商魔改过。用混合方案可以绕过这些限制,保证编码一致性。
16.2 MediaRecorder + MediaCodec 混合方案实战
具体怎么做?核心就三步:
- 创建 MediaCodec 编码器,配置编码参数(H.264、码率、帧率等)
- 从 MediaCodec 获取 Surface,传给 MediaRecorder 的 setInputSurface()
- MediaRecorder 负责封装,MediaCodec 负责编码
看代码:
// 创建 MediaCodec 编码器
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIME_TYPE_AVC, 1920, 1080);
format.setInteger(MediaFormat.KEY_BIT_RATE, 8_000_000); // 8Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔1秒
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 获取输入 Surface
Surface inputSurface = codec.createInputSurface();
// 配置 MediaRecorder
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 关键!
recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
recorder.setOutputFile(outputPath);
recorder.setInputSurface(inputSurface); // 传入编码器的 Surface
recorder.prepare();
recorder.start();
// 开始编码
codec.start();
嗯,这里要注意:setVideoSource(SURFACE) 是 MediaRecorder 在 API 21 之后才支持的。如果你还在用老版本,那只能走纯 MediaCodec 路线了。
我曾经踩过的坑:MediaRecorder 的 start() 和 MediaCodec 的 start() 调用顺序不能错。必须先 start MediaRecorder,再 start MediaCodec。否则会出现「Surface 未就绪」的诡异崩溃。
16.3 分段录制:解决「录到一半内存不够」
分段录制,说白了就是把一个长视频切成多个小片段。为什么要这么做?
- 内存限制:Android 对单个文件大小有限制(比如 4GB),超过就写不进去了
- 容错:如果录制过程中崩溃,至少前面的片段已经保存了
- 编辑方便:分段后可以单独处理每一段,比如删除某一段
实现思路其实很简单:监听 MediaRecorder 的 MaxDurationReached 或 MaxFileSizeReached 回调,然后停止当前录制,创建新的 MediaRecorder 实例继续录。
recorder.setOnInfoListener((mr, what, extra) -> {
if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
|| what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
// 停止当前录制
mr.stop();
mr.release();
// 创建新实例继续录
startNewSegment();
}
});
但这里有个细节:分段后的视频文件,时间戳是独立的。如果你想把它们合并成一个完整的视频,需要在后期用 MediaMuxer 重新封装,或者用 FFmpeg 拼接。
我的建议:分段录制时,每个片段的文件名里带上时间戳(比如 segment_20240101_120000.mp4),方便后期排序和拼接。
16.4 断点续录:用户暂停后还能继续
断点续录,就是用户点击「暂停」后,录制暂停;点击「继续」后,在同一个文件里接着录。
MediaRecorder 本身不支持暂停。你想想看,它只有 start() 和 stop(),没有 pause() 和 resume()。那怎么办?
有两种方案:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 方案A:分段+拼接 | 暂停时停止当前片段,继续时新建片段,最后合并 | 实现简单,兼容性好 | 需要后期合并,有短暂黑帧 |
| 方案B:MediaCodec 接管 | 用 MediaCodec 编码,暂停时停止输入数据,继续时恢复 | 无缝衔接,无黑帧 | 代码复杂,需要自己处理封装 |
我个人更推荐方案A,因为简单可靠。方案B虽然效果更好,但代码量至少翻三倍,而且容易出音视频同步问题。
方案A的具体实现:
// 暂停时
private void pauseRecording() {
if (isRecording) {
currentRecorder.stop();
currentRecorder.release();
isRecording = false;
// 记录暂停时间点
pauseTimestamp = System.currentTimeMillis();
}
}
// 继续时
private void resumeRecording() {
if (!isRecording) {
// 创建新的 MediaRecorder 实例
currentRecorder = new MediaRecorder();
// 配置参数...
currentRecorder.setOutputFile(getNextSegmentPath());
currentRecorder.prepare();
currentRecorder.start();
isRecording = true;
}
}
合并的时候,用 MediaMuxer 把所有片段的视频轨道和音频轨道按时间顺序写进去。注意要调整每个片段的时间戳偏移量。
一个小技巧:分段录制时,每个片段的起始时间戳不要从0开始,而是从上一个片段的结束时间戳继续。这样合并时就不需要重新计算时间戳了。
16.5 避坑指南
最后,分享几个我实际项目中踩过的坑:
- Surface 生命周期:MediaCodec 的 Surface 必须在 MediaRecorder 启动前创建好,否则会报 IllegalStateException
- 音频同步:分段录制时,音频和视频的采样率、声道数必须保持一致,否则合并后音画不同步
- 文件权限:Android 11 以上,分段录制需要提前申请 MANAGE_EXTERNAL_STORAGE 权限,否则写文件会失败
- 内存泄漏:MediaRecorder 和 MediaCodec 都是重量级对象,记得在 onDestroy() 里释放,否则会内存泄漏
嗯,以上就是视频录制进阶的全部内容。混合方案、分段录制、断点续录,这三招用好了,大部分录制场景都能搞定。如果你在实际项目中遇到什么问题,欢迎交流。