视频录制延迟优化:MediaCodec配置、编码器参数调优、音视频同步、录制帧率控制
视频录制延迟,说白了就是按下快门到真正开始录制的那个时间差。用户对这个特别敏感——你想想看,谁愿意举着手机等半秒才看到画面动起来?我做过一个项目,用户反馈说「按了录制键,画面卡了三四帧才开始」,结果一查,问题出在编码器初始化流程上。今天我们就来聊聊怎么把这部分延迟压到最低。
MediaCodec 配置:别让初始化拖后腿
MediaCodec 的配置,我个人习惯分成两步走:提前创建 + 异步模式。很多开发者会在点击录制按钮时才去创建编码器,这其实是个坑。编码器创建和配置本身就要几十毫秒,再加上 Surface 的绑定,轻松破百毫秒。
核心思路:在预览阶段就把编码器准备好,但先不给它输入数据。等用户点击录制时,直接 start() 就行。
// 提前创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
// 先不 start,等用户点击录制时再 start
这里有个细节:createInputSurface() 必须在 configure 之后、start 之前调用。我遇到过有人把顺序搞反,结果 Surface 一直拿不到,报了一堆奇怪的错误。
小技巧:如果你用 Camera2 的预览 Surface,可以提前把编码器的 Surface 注册到 CaptureSession 里。这样预览流和录制流共用同一个 Surface,省去了切换的延迟。
编码器参数调优:平衡画质与延迟
编码器参数不是越大越好,也不是越小越好。我见过有人为了降低延迟把帧率设到 15fps,结果画面卡得像幻灯片。反过来,有人把码率设得特别高,编码器处理不过来,反而导致丢帧。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| KEY_FRAME_RATE | 30 | 低于 24 会明显卡顿,高于 60 对编码器压力大 |
| KEY_BIT_RATE | 10-20 Mbps(1080p) | 太低画质差,太高编码延迟增加 |
| KEY_I_FRAME_INTERVAL | 1 | 每秒一个关键帧,方便剪辑和同步 |
| KEY_COLOR_FORMAT | COLOR_FormatSurface | 必须用 Surface 模式,否则性能差 |
嗯,这里要特别说一下 I 帧间隔。我习惯设成 1 秒,也就是每 30 帧一个关键帧。为什么?因为音视频同步时,如果关键帧间隔太长,seek 或剪辑时会出现明显的等待。但如果你对文件大小特别敏感,可以适当放宽到 2-3 秒。
注意:不要盲目追求低延迟而把编码器设成「实时模式」。有些芯片的实时模式会牺牲画质,甚至出现马赛克。我建议先跑一下默认参数,再根据实际效果微调。
音视频同步:别让嘴型对不上
音视频不同步,是录制延迟里最让人头疼的问题。说白了,音频和视频是两个独立的流,它们的时间戳必须对齐。我遇到过最离谱的一次,音频比视频快了 200 毫秒,用户录了一段演讲,结果嘴型完全对不上。
解决思路其实不复杂:用同一个时钟源。音频和视频的时间戳都基于 System.nanoTime() 换算,而不是各自用不同的时钟。
// 音频录制时
long presentationTimeUs = (System.nanoTime() - startTime) / 1000;
audioEncoder.queueInputBuffer(inputIndex, offset, size, presentationTimeUs, 0);
// 视频录制时
long presentationTimeUs = (System.nanoTime() - startTime) / 1000;
videoEncoder.queueInputBuffer(inputIndex, offset, size, presentationTimeUs, 0);
这里有个坑:音频的采样率。如果音频采样率是 44100Hz,每帧音频的时长是固定的。但视频帧的时长可能因为丢帧或跳帧而不稳定。我的做法是:在音频端用固定的时间戳增量,视频端用实际时间戳,最后在 Muxer 里统一对齐。
避坑指南:我曾经在某个低端芯片上发现,音频编码器输出的时间戳偶尔会跳变。后来加了校验逻辑:如果时间戳比上一帧还小,就强制修正。这个 bug 查了我整整两天。
录制帧率控制:别让编码器吃撑
录制帧率控制,说白了就是让编码器以稳定的速率处理帧。如果预览帧率是 60fps,但编码器只能处理 30fps,那多余的帧就会被丢掉。但丢帧也有讲究——不能乱丢。
我个人习惯用 丢帧策略:当编码器来不及处理时,优先丢 B 帧,其次是 P 帧,最后才丢 I 帧。因为 I 帧是关键帧,丢了会导致后面的帧都解码不出来。
// 丢帧逻辑示例
if (encoder.dequeueInputBuffer(timeoutUs) < 0) {
// 编码器忙,跳过当前帧
// 但要注意:不能连续跳过太多帧,否则画面会卡
skippedFrames++;
if (skippedFrames > 5) {
// 强制丢一帧 I 帧?不,这里应该降低预览帧率
requestLowerPreviewFps();
}
return;
}
嗯,这里要提醒一下:不要用 Thread.sleep() 来控制帧率。sleep 的精度太差,而且会阻塞主线程。我建议用 Choreographer 或者 SurfaceView 的回调来控制帧率,这样更精准。
核心原则:录制帧率不要超过编码器的处理能力。如果编码器标称 30fps,你就老老实实给 30fps。强行给 60fps 只会导致丢帧和延迟增加。
整体延迟优化流程
最后,我画了一张图来总结整个优化流程。你可以把它当作一个检查清单,每做一步就对照一下。
这张图从左到右展示了优化的四个核心步骤。你每做完一步,就可以对照下面的检查点看看有没有遗漏。我个人习惯在项目里把这个流程做成一个工具类,每次录制前自动检查一遍,省得手忙脚乱。
最后说一句:延迟优化没有银弹。不同芯片、不同 Android 版本的表现差异很大。我建议你在真机上反复测试,用 dumpsys media.codec 查看编码器的实际表现,再针对性调整参数。
公众号:蓝海资料掘金营,微信deep3321