本系列第 6 篇 | 2026-08 | 标签:libcamera、ISP/3A、性能归因、架构重构
这一篇是架构的定型篇:为什么最终删掉低延迟的 v4l2src 直采路径、统一走
libcamerasrc;"待机 CPU 60%" 怎么一步步归因到 libcamera IPA 的一个版本差异;
face_rec 怎么分两步完成订阅化迁移;以及一次"中途接入黑屏"的回归是怎么发生、
怎么修掉的。
决策:删掉低延迟路径,换 3A
第 2、4 篇里那条 ~8ms 的 v4l2src 直采路,被整体删除了 ------ 连同它的初始化脚本、
aux pipe 分支和各种 dmabuf 基准工具。
为什么?对讲场景的卖点是 3A(自动曝光/白平衡) :IMX335 无硬件 ISP,
裸 v4l2src 直采意味着没有 3A ------ 逆光、白炽灯下画面不可用。统一走
libcamerasrc 后,采集延迟从 ~8ms 变为 24~30ms (libcamera request 流转 +
IPA 跑 3A/AE 的固定开销),对对讲完全可接受。
架构取舍的通用判断式:牺牲可量化的延迟(24ms),换回不可量化的可用性
(逆光能看清人) 。延迟有探针盯着,画面质量没有数字,但用户一眼就能看出"看不清"。当两个指标冲突时,先想清楚哪个没有退路。
CPU 归因三步法:"60% 到底花在哪"
统一之后,发布进程待机涨到 ~60% 单核。是泄漏?是死循环?还是本来就该这样?
归因过程值得完整复盘:
第一步:对照实验隔离变量
加了一个诊断开关:TESTPATTERN=1 时用 videotestsrc 替掉相机源,
其余管线完全不动。结果同样管线只剩 ~15% (还含一处软格式转换)。
→ 差额 ~45 个点全在 libcamerasrc 这一层。
第二步:算总账,回答"正不正常"
编码 + 三环广播整条链只占 ~1.5 成;IMX335 无硬件 ISP,软 ISP/3A 本来就要烧 CPU;
~60% 单核 ≈ 双核 A35 整机 30%,待机属正常,不是可优化项 。
结论先于深挖:如果总账算出来"正常",后面的源码级定位就只是求证而非抢救。
第三步:日志计数,定位到模块
打开 libcamera IPA 的 DEBUG 日志,统计 ISP update 次数:
| 我们这块自制板 | ST 官方开发板 | |
|---|---|---|
| 5 秒内 ISP update 次数 | 267(≈每帧) | 9(仅启动收敛期) |
| 每帧重写的内容 | EXPOSURE + AWB(且 AWB 写的是同一个值) | 参数变化才写 |
决定性证据:每帧无条件重写未变化的参数 。继续对照两版驱动 patch 的源码:
官方版 AWB 有防振荡检测(统计没变就直接 return),我们镜像里的旧版(evision 分支)
简单算法 fallback 缺这个检测 ,每帧无条件 applyProfile。ST 官方后来已修复,
升级 libcamera 即消除;顺带发现单文件替换 IPA 库会因 ABI 不匹配直接 abort,
只能整版升级。
方法论沉淀成一句话:性能问题先用对照开关把变量隔离,再用日志计数定位到模块,
最后才读源码找差异。 从现象到证据链的完整路径,比结论本身更值钱。
libcamerasrc 双 pad 广播:两个一踩就中招的细节
发布进程从单一 view-finder 编码扩展为双 pad:
libcamerasrc ── 静态 src pad = view-finder(role=3)
└→ tee ─┬→ 编码链 → <stream>(H264)环
└→ <stream>_raw 环
── request src_%u pad = still-capture(role=1)
└→ DCMIPP 硬缩 128×128 → <stream>_det 环
细节一:caps 必须用显式 capsfilter 钉住 。gst_element_link_filtered 的 filter
不持久;libcamera 的协商靠向下游 peer 查询 caps ------ 只有 capsfilter 元件的 caps
属性能把 640×480 RGB16 / 128×128 RGB 真正传回去。(呼应第 3 篇:那边是插错转换元件
协商退化,这边是不钉 filter 根本协商不到想要的格式 ------ 同一个主题的两面。)
细节二:view-finder 必须用静态 src pad 。用 request pad(src_%u)的话,
静态 src 默认 role=video-recording 仍留在 pad 列表里,DCMIPP main pipe 被
默认配置占掉,still-capture 直接撞车,报 Main pipe already allocated。
板上验证:640×480 RGB565 + 128×128 BGR888 双流同出,det 环满帧写入,
人脸识别同时跑不互斥。
face_rec 订阅化:分两步走
重构期间 face_rec 曾短暂回到自己开相机的老路(双 pad 广播还没就绪)。
就绪后按步迁移,而不是一把梭:
- 第一步 :只订
<stream>_det环。删整条相机管线,起订阅线程
(200ms 重试挂环、5 秒无帧自动重挂 ------ 抄推流订阅器的模式)。det 字节 =
DCMIPP 直出 128×128 RGB24,零转换直接喂 BlazeFace 。此步识别/显示先禁用,
只做检测 + 发框 ------ 先在板上验证"共享 IMX335 不互斥、30fps 满帧订阅"再继续。 - 第二步 :补订
<stream>_raw环恢复识别 + 上屏,阈值对齐官方 int8 标定值 0.40
(第 3 篇讲过的量化压扁问题)。
分步策略的核心:每一步都让一个可观测的最小闭环跑通,失败时知道是哪一步引入的。
回归:一颗丢掉的 config-interval=-1
双 pad 重构把管线改成元素式写法时,把 h264parse config-interval=-1 弄丢了。
后果链:
config-interval 默认 0 → SPS/PPS 只在流开始发一次
→ 关键帧不再自包含(SPS+PPS+IDR 缺前两个)
→ WebRTC 中途接入 / 强制 IDR 之后黑屏
最迷惑的地方:启动链路一切正常 (第一帧本来就带参数集),
只有"中途接入"这个动作复现。这正是第 4 篇强调的关键帧自包含约束 ------
它不是优化项,是正确性前提。
教训:改管线写法时,逐属性 diff 新旧两端 。这类属性错误不报错,
只在你依赖它的场景下坏。
附:KVS SDK 的 UINT8 回绕
公网联调时 offer 直接失败,报 ICE 值缺失。根因在 SDK 内部:
浏览器 offer 里多余的 a=rtcp-fb 行把单个媒体段的属性数顶过了上限(256),
而 SDK 用 UINT8 计数 ,溢出后回绕覆盖,把 a=ice-ufrag/a=ice-pwd
盖掉了。修法:处理 offer 前剥掉多余属性行。
同一时期还修了 .local(mDNS)候选的阻塞问题:跨网时浏览器的 host 候选
必然解不出,但每条域名解析卡 ~4 秒超时,把队列里真正有用的 relay/srflx
候选堵住 → 首帧 ~10s 。解法:按网络模式分流,relay 模式直接丢 .local。
第三方 SDK 的坑往往不在文档里,在计数器类型和超时路径里 ------ 出现"不可能的报错"
时,去读它的错误码生成逻辑。
重构后的验收基线
| 读数 | 局域网 | 公网 5G |
|---|---|---|
| 呼叫→首帧渲染 | ~120ms | ~690ms |
| 稳态 RTT / fps / 丢帧 | 2ms / 30 / +0 | 88ms / 29 / +0 |
采集延迟 24~30ms 由 libcamerasrc 固定开销主导,端到端零丢帧 ------ 重构无回归,
架构定型。
下一篇:07 · 补齐对讲的另一半
------ 麦克风上行、强制 IDR 的反向通道选型、音频 PTS 坑。