我用 AI 写了一套实时视频检测系统,然后推翻了它

我用 AI 写了一套实时视频检测系统,然后推翻了它

背景

我在做一个实时直播流 AI 检测系统:从 SRS 流媒体服务器拉取直播视频,交给 Python 端的 AI 模型做目标检测,再把检测结果(比如人物位置的边界框)同步叠加到前端播放画面上。

核心难题只有一个:怎么让检测结果和视频画面对齐。

V1:一个"优雅"的方案

我选择了 H.264 SEI(Supplemental Enhancement Information)。这是视频编码标准中专门用来携带附加数据的机制------把 AI 检测结果直接塞进视频帧的码流里,数据和画面在物理层面绑定,天然同步,不可能错位。

听起来很完美。但要实现它,得先理解 WebRTC 送过来的视频数据长什么样。

为什么实现 SEI 这么复杂

WebRTC 传输视频时,底层走的是 RTP 协议。一个视频帧不会完整地送过来------它会被拆成一堆 RTP 包。这些包到达时可能乱序、可能丢失,你需要按序号重组。

重组之后拿到的也不是"一帧画面",而是 H.264 的 NALU(Network Abstraction Layer Unit)。一个 RTP 包可能包含一个完整 NALU,也可能包含多个小 NALU(STAP-A 聚合包),也可能只是一个大 NALU 的一个分片(FU-A 分片包)。你需要处理所有这些情况才能拿到完整的 NALU。

而 NALU 本身也有不同类型:SPS(序列参数集)、PPS(图像参数集)、IDR(关键帧)、非 IDR Slice(普通帧)......SEI 需要被插入到正确的位置------SPS/PPS 之后、Slice 之前------而且必须是 Annex-B 格式(以 00 00 00 01 起始码分隔),还要符合 user_data_unregistered 规范(16 字节 UUID 前缀 + payload)。

这些概念------RTP 分包、NALU 类型、Annex-B 格式、SEI 编码规范------是实现 V1 方案的前置知识。 为了搞清楚这些,我查了大量 H.264 和 WebRTC 的资料。

理解了这些之后,架构变成了这样:

复制代码
SRS → WebRTC 拉流 → Go(pion 解包组帧) → FFmpeg 解码成 JPEG
  → NATS 发给 Python → AI 推理 → 结果写入 SEI → 重排序 → FFmpeg 推回 SRS

技术栈:Go + pion/webrtc + FFmpeg(×2) + NATS + Python。为了让它跑起来,我写了:

  • RTP 解包与 H.264 组帧:用 pion 从 WebRTC 连接中拉取 RTP 包,处理 STAP-A/FU-A 等封包格式,重组成完整的 Annex-B 格式 H.264 帧
  • 双 FFmpeg 管道:一个负责把 H.264 解码缩放成 JPEG 供 AI 推理,另一个负责把带 SEI 的 H.264 重新封装推回 SRS
  • NATS 多模型扇出:支持同时调用多个 AI 模型,并行 request-reply,动态超时
  • 15 Worker 并发池 + 重排序缓冲:并发处理帧以提高吞吐,再按帧序号重新排列保证输出有序
  • 动态超时机制:每帧的 NATS 超时 = 缓冲上限 - 已消耗时间 - 安全余量,推理时间不够就丢弃结果保证流不卡

代码量 500+ 行,Go 端单独就 8 个文件。用 640×360 30fps 的测试流验证,系统完整跑通,SEI 正确嵌入,前端播放器能实时解析并画框。

问题出现

当我把测试流换成 1920×1080 时,输出视频开始卡顿。

我的第一反应是做局部优化------分析 1080p 下的内存占用、GC 压力、SEI 扫描性能,列出 P0-P4 的优化清单。但优化完一轮后,卡顿依然存在。

这时候我停下来问了一个问题:如果从需求本身重新推导,当前的架构是不是一开始就走偏了?

拆解发现

回到需求:把 AI 检测结果同步显示在直播画面上。

为了实现"同步",我选了 SEI。但 SEI 要求修改 H.264 码流。修改码流要求拿到裸 NALU。拿裸 NALU 要求解封装。解封装后还得重新封装推回去。而且推回去的流必须是连续的、时间戳正确的。

每一步都是上一步的连锁后果。

更致命的是拉流协议的选择。我用了 WebRTC(WHEP)来从 SRS 拉流。WebRTC 基于 UDP,是为实时交互设计的------可以丢包、可以乱序、不保证每帧完整。但我的管道下游是 RTMP 推流,它需要一个连续、完整、时间戳准确的 H.264 码流。

于是出现了根本矛盾:用一个有损的实时协议拉流,处理后喂给一个需要无损输入的推流协议。

我写的那 200 多行 handler.go------worker pool、reorder buffer、动态超时------本质上都是在弥合这个矛盾。重排序缓冲靠墙钟等待来决定是否跳帧,这个机制天然引入抖动。原始流的时间戳在 WebRTC 传输后丢失,FFmpeg 只能靠帧到达间隔猜帧率。

卡顿不是某个 buffer 太小,而是架构本身在制造卡顿。

退一步看全局

跳出 SEI 方案,重新审视"把结果和帧对齐"这个需求,所有方案可以分成两类:

同路:结果和视频物理上走同一条路,不可分离。

方案 做法 代价
烧录进画面 服务端解码→画框→重编码→推流 重编码吃资源,框不可交互
SEI 嵌入 修改 H.264 码流 工程复杂度高,当前方案的问题
推送图片 JPEG over WebSocket,替代视频流 带宽高,画面不流畅

旁路:结果走独立信道,前端做同步匹配。

方案 离视频的距离 同步精度 代价
FLV script tag 近(同一容器) 高(容器时间戳) 锁定 HTTP-FLV,延迟 1-3 秒
WebRTC DataChannel 中(同一连接) 中 SRS 不原生支持,开发量大
WebSocket 远(独立连接) 低 两条连接可能漂移
SSE 远(独立连接) 低 同 WebSocket,但更简单

旁路方案的核心顾虑是"同步精度不够"。但仔细想想:

  • AI 推理本身需要 100-300ms,每秒只能产出 3-10 个结果
  • 前端直接用"最新一次推理结果"画框
  • 偏差最多 1-2 帧(33-66ms),人眼不可感知
  • 推理频率本身就远低于视频帧率,精确到帧的绑定是过度要求

结论

最终方案:视频原样走 WebRTC 播放,检测结果走 WebSocket 旁路推送。

复制代码
原始流 → SRS → WebRTC → 前端播放(不改流)
原始流 → 截帧 → Python 推理 → WebSocket → 前端 Canvas 画框

不需要改码流,不需要双 FFmpeg,不需要 reorder buffer,不需要 pion 组帧。卡顿问题从根源消失,因为原始流从未被修改过。

教训:和 AI 协作时,什么时候该踩刹车

这个项目全程有 AI 参与。在我提出"输出视频卡顿"时,AI 做了详细的性能分析------内存估算、GC 影响、管道吞吐量------然后给出了一长串优化建议。这些分析在局部上是合理的 (虽然有些细节算错了),但它们共同的盲区是:默认当前架构是对的,只需要优化。

直到我明确要求"从需求本身重新分析,看看是不是一开始就走偏了",AI 才跳出局部优化的框架,开始质疑 SEI 方案本身的必要性。

这暴露了一个 AI 协作中的模式问题:

什么时候该意识到需要重新审视

  • 优化了一轮但问题还在------如果局部修复没用,问题可能不在局部
  • 解释一个设计决策时,需要讲很长的"因为所以"链------因为用了 SEI → 所以需要改码流 → 所以需要拉流解封装 → 所以需要 WebRTC... 连锁越长,初始假设的杠杆越大
  • 发现自己在写大量的"对抗性"代码------reorder buffer 对抗乱序,动态超时对抗延迟耦合,残帧检测对抗丢包。如果代码主要在解决架构自身制造的问题,架构可能就是问题
  • AI 给出的优化建议越来越精细,但收益越来越边际------说明已经在局部最优附近打转了

怎么提示 AI 退后一步

当你意识到可能走偏了,不要问"这个 bug 怎么修"或"这段代码怎么优化"。试试这些方向:

"不要看代码实现,从需求本身重新推导,当前方案是不是最合理的路径?"
"如果我们从零开始设计,已知目标是 X,有哪些根本不同的方案?各自的 trade-off 是什么?"
"当前架构中,哪些复杂度是需求本身要求的,哪些是我们的设计选择制造的?"
"假设一个资深工程师刚加入项目,看到这套架构,他会问什么问题?"

关键是把 AI 从"优化当前实现"模式切换到"质疑当前假设"模式 。AI 默认会尊重你已有的代码和架构------这通常是对的,但在需要根本性反思时,你必须明确告诉它:退后一步看。

这可能是 AI 协作中最反直觉的一点:AI 最擅长在给定框架内做精细工作,但质疑框架本身这件事,目前还是得由人来发起。

更深一层:陌生领域怎么避免一开始就选错

AI 协作的教训说的是"走偏了怎么纠正"。但还有一个更根本的问题:面对不熟悉的领域,有没有策略能一开始就选对方向?还是说必须得碰过钉子?

答案是:两者都需要,但比例不是你想的那样。

逻辑能覆盖的部分

面对陌生领域,有一个策略确实有用:在动手之前,把"既要又要"列表写出来,然后找矛盾。

这个项目的需求列表:

  • 检测结果要和帧同步
  • 延迟要低
  • 前端要方便提取展示
  • 后端要方便处理

这些单独看都合理。但如果当时追问一句------"同步精度到底要多高?"------答案会是"AI 每秒只出 3-10 个结果,精确到帧没有意义"。这一条一旦松动,SEI 方案的前提就不成立了,后面的复杂度全部不需要。

问题在于:当时我不知道该追问这一句。因为我还不了解 AI 推理的实际吞吐量,也不了解人眼对同步偏差的感知阈值。你需要知道什么才能做出正确决策,这件事本身需要经验。

逻辑覆盖不了的部分

SEI 方案在技术上是成立的------H.264 规范就是支持这么做。如果你去搜"视频流附加自定义数据",SEI 大概率是排名最高的答案。它在概念层面优雅、正确、标准。

你没法靠纯逻辑推导出"这条路走下去工程代价会爆炸"。 因为代价藏在实现细节里------RTP 分包重组、Annex-B 格式、时间戳管理、reorder buffer------这些东西只有做了才知道有多重。你读 H.264 规范的时候不会看到一行字说"注意:如果你用 WebRTC 拉流再改码流推回去,会非常痛苦"。

这就是经验的价值:不是告诉你正确答案,而是告诉你哪些看起来可行的路径走下去会很疼。

一个折中策略:先做最小验证,晚做架构决策

虽然经验无法完全替代,但有一个做法能降低踩坑的代价。

回到这个项目。如果当时不急着选 SEI,而是先花半天时间:

  1. 用 FFmpeg 截几帧图发给 Python,确认推理延迟是 100-300ms
  2. 在前端手动模拟:视频播着,setTimeout 延迟 100ms 画个框,看看效果能不能接受
  3. 能接受 → WebSocket 旁路就够了,不需要碰码流
  4. 不能接受 → 再考虑 SEI

这个验证不需要任何 H.264 知识,不需要 Go,不需要 pion,不需要 NATS。 半天就能做完,而且做完之后对"同步精度到底要多高"这个关键问题就有了体感,后面的技术选型自然会不同。

踩钉子不可避免,但可以控制钉子的大小。先用最小成本验证最大的假设------这条不需要经验,靠逻辑就能做到。

定性需求 vs 定量需求

经验的作用不是直接告诉你选什么方案,而是告诉你在决策之前该去摸清哪些数字。

没经验的时候,你看到"结果要和帧同步",会把它当成一个定性需求,然后去找技术上最"正确"的同步方案------SEI。

有经验的话,你会先把它转成定量问题:同步偏差多少毫秒可以接受?然后去量:推理多快、人眼阈值多少、帧间隔多少。数字一出来,方案自然收窄。

定性需求导向"最正确"的方案,定量需求导向"够用"的方案。 前者容易过度工程化,后者才是工程决策的正常姿势。

相关推荐
翰佰尔生物HiOmics云分析8 天前
老年COVID-19重症化风险预测机器学习在线工具技术解析
机器学习·开发·预测·covid-19
PHP实战开发录8 天前
Nginx上传大文件为什么报413
nginx·php·开发
PHP实战开发录10 天前
PHP Session为什么会拖慢并发请求
系统架构·php·开发
PHP实战开发录11 天前
MySQL连接数为什么会打满
系统架构·php·开发
PHP实战开发录13 天前
PHP接口偶发变慢怎么查
前端·php·开发
PHP实战开发录15 天前
PHP接口怎么防重复提交
系统架构·php·开发
PHP实战开发录17 天前
PHP脚本手动能跑定时任务却失败
开发语言·php·开发
PHP实战开发录18 天前
PHP接口超时后为什么任务还在跑
开发语言·前端·php·开发
PHP实战开发录19 天前
PHP日志写满磁盘后接口为什么异常
开发语言·系统架构·php·开发
PHP实战开发录19 天前
PHP事务异常后为什么数据还写进去了
mysql·php·开发