22. 实时性优化:减少预览延迟、降低拍照快门延迟、录像启动延迟优化
各位做相机开发的同行,今天我们来聊聊实时性。说白了,就是用户按下快门到听到“咔嚓”声之间的那段时间,到底去哪了?
我做了这么多年相机功耗优化,发现一个有意思的现象:功耗和延迟,往往是同一枚硬币的两面。你优化了延迟,功耗自然就降下来了;反过来,粗暴地降功耗,延迟一定会飙升。今天我就把这三个最关键的延迟场景——预览、拍照、录像启动——掰开揉碎了讲清楚。
核心观点:实时性优化的本质,是让硬件和软件在“最短时间”内完成“最少必要工作”,然后立刻进入低功耗状态。
22.1 预览延迟优化:让用户“所见即所得”
预览延迟,是用户打开相机到看到画面之间的时间。我见过有些项目,这个延迟能做到80ms以内,有些却要300ms以上。差距在哪?
22.1.1 管道深度与缓冲策略
先看一个典型的Camera HAL预览管道:
// 传统做法:3~4帧缓冲
requestQueue.depth = 4;
// 优化做法:减少到1~2帧
requestQueue.depth = 1;
// 同时启用快速轮询模式
streamConfig.setFastPoll(true);
嗯,这里要注意:减少缓冲深度会降低延迟,但也会增加丢帧风险。我个人习惯的做法是:在启动阶段用1帧缓冲,稳定后切换到2帧。这样既保证了启动速度,又避免了画面撕裂。
22.1.2 传感器配置优化
传感器出图模式对延迟影响巨大。我遇到过一款传感器,默认用4-lane MIPI跑30fps,预览延迟120ms。后来改成2-lane + 双VDMA,延迟直接降到65ms。
| 配置项 | 默认值 | 优化值 | 延迟改善 |
|---|---|---|---|
| MIPI Lane数 | 4 | 2 | -30% |
| 帧率 | 30fps | 60fps | -50% |
| 输出分辨率 | 全尺寸 | 2x2 binning | -40% |
我的经验:预览阶段不需要全分辨率。用binning模式把分辨率降到1080p甚至720p,延迟和功耗都能大幅改善。等用户按下快门时再切回全分辨率,完全来得及。
22.2 拍照快门延迟:从按下到捕获的“黄金100ms”
拍照快门延迟,是用户最敏感的体验指标。超过200ms,用户就会觉得“卡”。我做过一个项目,最初快门延迟350ms,被产品经理追着骂了三天。
22.2.1 3A收敛加速
为什么快门延迟高?很多时候是3A(自动对焦、自动曝光、自动白平衡)在拖后腿。传统做法是:按下快门→重新做3A→捕获。这太慢了。
// 优化前:按下快门后重新做3A
captureRequest.setAfMode(AF_MODE_AUTO);
captureRequest.setAeMode(AE_MODE_ON);
// 等待3A收敛...
// 耗时:150~250ms
// 优化后:使用预览帧的3A结果
captureRequest.setAfMode(AF_MODE_CONTINUOUS_PICTURE);
captureRequest.setAeMode(AE_MODE_ON_AUTO_FLASH);
// 直接使用预览管道的最新3A数据
// 耗时:30~50ms
说白了,预览时3A已经在跑了,为什么还要重新做?直接拿预览帧的3A结果来拍照,延迟能砍掉一大半。
22.2.2 零快门延迟(ZSL)技术
ZSL(Zero Shutter Lag)是高端相机标配。原理很简单:预览管道里始终保留一帧全分辨率图像。用户按下快门时,直接取这一帧,而不是重新捕获。
注意:ZSL会带来额外的内存和功耗开销。我建议只在拍照场景下启用,预览时切回低分辨率模式。我曾经见过一个项目,全程开着ZSL,结果预览功耗高了30%,电池直接崩了。
22.3 录像启动延迟:从点击到录制的“心理预期”
录像启动延迟,是用户点击录制按钮到看到时间码开始跳动的时间。这个延迟如果超过500ms,用户会怀疑手机是不是死机了。
22.3.1 编码器预热
录像启动慢,很大原因是编码器初始化太慢。H.264/H.265编码器启动需要分配内存、建立参考帧列表、初始化码率控制……这些操作加起来可能要200~400ms。
// 优化前:点击录制后才初始化编码器
MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
// 耗时:200~400ms
// 优化后:预览阶段就预热编码器
// 在预览管道中创建一个“虚拟编码会话”
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();
// 但不送入真实数据,只保持编码器“热”状态
// 点击录制时,直接切换数据流
// 耗时:10~30ms
嗯,这里有个坑:编码器预热会增加预览功耗。我建议的做法是:检测到用户手指放在录制按钮上时(Touch Down事件),再启动预热。这样既不会浪费功耗,又能保证启动速度。
22.3.2 文件系统与存储优化
录像启动还有一个隐藏瓶颈:文件创建。在FAT32文件系统上创建一个空文件,可能就要50~100ms。如果再加上目录遍历、权限检查……
- 预创建文件:在预览阶段就创建好一个临时文件,录制时直接写入
- 使用异步IO:文件创建和元数据写入放到后台线程
- 选择高性能存储:UFS 3.1比eMMC 5.1在文件创建上快3~5倍
避坑指南:我曾经在一个项目上遇到录像启动偶尔卡顿的问题。查了两天才发现,是文件系统在做垃圾回收。解决方案很简单:录制前强制触发一次fstrim,或者预留10%的存储空间。
22.4 知识体系总览
下面这张图,是我对实时性优化核心逻辑的总结。你可以把它当作一个检查清单:
22.5 综合优化策略
最后,我分享一个我在项目中实际用过的综合优化策略。这个策略帮我把相机启动到录像启动的总延迟,从1.2秒降到了0.6秒。
- 预热阶段(用户打开相机时):
- 传感器以binning模式输出1080p
- 编码器创建虚拟会话,保持热状态
- 预创建录像文件
- 预览阶段(用户取景时):
- 使用1帧缓冲,快速轮询
- 3A持续运行,结果缓存
- 检测Touch Down事件,准备ZSL
- 捕获阶段(用户按下快门/录制时):
- 直接使用预览帧的3A结果
- ZSL取最新全分辨率帧
- 编码器切换数据流,开始录制
最后说一句:实时性优化没有银弹。每个项目、每款传感器、每个平台都不一样。我建议你拿到一个新平台时,先跑一遍延迟基线,然后对照我上面这张图,一个一个瓶颈去排查。相信我,当你把延迟从300ms优化到100ms时,那种成就感,比写一万行代码都爽。