视频录制延迟优化: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 只会导致丢帧和延迟增加。

整体延迟优化流程

最后,我画了一张图来总结整个优化流程。你可以把它当作一个检查清单,每做一步就对照一下。

视频录制延迟优化流程 1. 提前创建编码器 configure + createInputSurface 2. 配置编码参数 帧率30 / 码率10-20M 3. 统一时间戳源 System.nanoTime() 4. 丢帧策略 优先丢B帧 关键检查点 编码器是否在预览阶段创建? → 否的话,延迟至少 +50ms 帧率是否匹配编码器能力? → 不匹配会导致丢帧或卡顿 音视频时间戳是否同源? → 不同源会导致音画不同步 丢帧策略是否合理? → 乱丢 I 帧会导致解码异常

这张图从左到右展示了优化的四个核心步骤。你每做完一步,就可以对照下面的检查点看看有没有遗漏。我个人习惯在项目里把这个流程做成一个工具类,每次录制前自动检查一遍,省得手忙脚乱。

最后说一句:延迟优化没有银弹。不同芯片、不同 Android 版本的表现差异很大。我建议你在真机上反复测试,用 dumpsys media.codec 查看编码器的实际表现,再针对性调整参数。


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