6、Video Recording优化:视频编码器选择与参数调优

视频录制优化,说白了就是跟编码器打交道。我做了这么多年Camera HAL,发现很多团队在编码这块踩的坑,其实都是基础概念没理清。今天咱们就聊聊H.264、H.265、AV1怎么选,编码参数怎么调,音视频怎么同步,还有录制和预览并发怎么处理。

6.1 编码器选择:H.264 vs H.265 vs AV1

先说说我的个人习惯。做产品选编码器,我一般先看目标平台和场景。不是越新越好,也不是越压缩越好,得看你的硬件撑不撑得住。

编码器 压缩效率 硬件支持 适用场景
H.264 基准 几乎所有设备 兼容性优先、低端设备
H.265 比H.264高30-50% 中高端Android设备 高清录制、存储敏感
AV1 比H.265高20-30% 仅部分旗舰SoC 流媒体、未来兼容

我在项目中遇到过一件事。有个客户非要上AV1,说压缩率高。结果跑在骁龙8 Gen1上,编码一帧要30ms,帧率直接掉到20fps。后来我建议他换H.265,同样的码率,画质几乎没差别,帧率稳稳的30fps。你想想看,硬件编码器不是万能的,得看芯片有没有硬编模块。

核心建议:

  • 兼容性优先 → H.264 Baseline/Main Profile
  • 画质与存储平衡 → H.265 Main Profile
  • 未来趋势 → AV1,但先确认硬件支持

6.2 编码参数调优:Bitrate、GOP、Profile

参数调优这块,我习惯从三个维度入手:码率控制、关键帧间隔、编码档次。说白了就是:要多大文件?多快能跳转?兼容哪些设备?

6.2.1 Bitrate(码率)

码率不是越高越好。我见过有人给1080p设了20Mbps,结果文件大得离谱,画质却没提升多少。为什么?因为编码器在低复杂度场景下,高码率就是浪费。

我个人习惯用CBR(恒定码率)配合目标码率。比如1080p 30fps,我一般设8-12Mbps。如果是动态场景多的视频(比如运动相机),我会用VBR,峰值码率设到15Mbps,平均码率10Mbps。

// 示例:MediaCodec配置
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, 10_000_000); // 10Mbps
format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // GOP=1秒

避坑指南:我曾经在低端设备上设了VBR,结果编码器在复杂场景下码率飙升,导致CPU过载、掉帧。后来我改成CBR+码率上限,问题就解决了。

6.2.2 GOP(关键帧间隔)

GOP就是两个I帧之间的间隔。我一般设1秒(即30帧一个I帧)。为什么?因为视频录制需要支持随机seek,GOP太长,用户拖动进度条就得等半天。

但GOP太短也有问题。I帧比P/B帧大得多,GOP=0.5秒的话,文件体积会增大20-30%。所以我的经验是:普通录制GOP=1秒,直播场景可以设2-3秒,因为直播不需要频繁seek。

6.2.3 Profile(编码档次)

Profile决定了编码器的功能集。H.264有Baseline、Main、High三种。我建议:

  • Baseline:兼容性最好,但压缩率低。适合低端设备或实时通信。
  • Main:平衡点。我大部分项目都用这个。
  • High:压缩率最高,但解码要求高。适合本地存储。

注意:有些设备声称支持High Profile,但实际编码质量很差。我建议在目标设备上做一次编码测试,看看实际输出是否符合预期。

6.3 音视频同步策略

音视频同步,说白了就是让嘴巴和声音对上。我见过最离谱的bug,视频播完了音频还在响,差了整整2秒。

核心思路是用PTS(Presentation Time Stamp)对齐。音频和视频各自有独立的时钟,但必须映射到同一个时间轴上。我一般用AudioTrack的时钟作为主时钟,因为人耳对音频抖动更敏感。

// 音视频同步伪代码
long audioPts = audioTrack.getTimestamp(); // 音频当前播放位置
long videoPts = videoDecoder.getOutputFrameTime(); // 视频帧时间戳
long diff = videoPts - audioPts;

if (diff > 30_000_000) { // 视频超前30ms以上
    // 丢帧或延迟显示
    videoDecoder.releaseOutputBuffer(index, false);
} else if (diff < -30_000_000) { // 视频落后30ms以上
    // 重复上一帧或加速显示
    videoDecoder.releaseOutputBuffer(index, true);
} else {
    // 正常显示
    videoDecoder.releaseOutputBuffer(index, true);
}

嗯,这里要注意。不要用System.currentTimeMillis()做同步,那个是wall clock,会被NTP调整。要用MediaCodec输出的PTS,那个是单调递增的。

6.4 录制与预览并发处理

录制和预览并发,说白了就是一边给用户看画面,一边存文件。这里有个坑:预览和录制的分辨率、帧率可能不一样。

我建议用两条独立的编码链:

  • 预览链:直接从Camera HAL拿帧,走SurfaceView/TextureView显示。分辨率可以低一些(比如1080p),帧率30fps。
  • 录制链:从Camera HAL拿帧后,经过编码器写入MP4。分辨率可以高一些(比如4K),帧率30fps或60fps。

但两条链共享同一个Camera Session。我遇到过一个问题:预览链和录制链同时请求帧,导致Camera HAL的buffer分配冲突。后来我改用ImageReader + 异步回调,把帧拷贝到编码器的输入队列,才解决。

并发处理要点:

  1. 使用两个独立的Surface:一个给预览,一个给编码器。
  2. 编码器的输入Surface要设置足够的buffer数量(至少4个)。
  3. 如果预览和录制帧率不同,用丢帧策略处理,不要阻塞Camera HAL。
视频录制并发处理流程图 Camera HAL 预览链 (1080p 30fps) 录制链 (4K 30fps) SurfaceView/TextureView MediaCodec编码器 MP4写入 两条独立链,共享Camera Session,通过ImageReader异步拷贝帧

最后说一句,录制和预览并发时,别忘了处理旋转和裁剪。我见过一个bug,预览是竖屏,录制出来却是横屏。原因就是没在编码器配置里设置rotation参数。

小技巧:在MediaFormat里设置KEY_ROTATION,或者用MediaMuxer的setOrientationHint。我一般用后者,因为不影响编码器内部处理。


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