我用 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。

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

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

相关推荐
医疗信息化王工5 天前
钉钉小程序开发实战:投诉管理系统
小程序·钉钉·开发·投诉管理
杨浦老苏11 天前
开源的AI编程工作站HolyClaude
人工智能·docker·ai·编辑器·开发·群晖
做cv的小昊11 天前
【conda】打包已有conda环境并在其他服务器上搭建
运维·服务器·python·conda·运维开发·pip·开发
杨浦老苏14 天前
可视化Docker Compose构建器VCompose
运维·docker·开发·可视化·群晖
七夜zippoe21 天前
区块链开发:从智能合约到DApp
python·区块链·智能合约·开发·dapp
那我掉的头发算什么1 个月前
【博客系统】基于Spring全家桶的博客系统(下)
java·后端·spring·mybatis·开发
UXbot1 个月前
APP原型生成工具测评
android·前端·人工智能·低代码·ios·开发·app原型
轩情吖1 个月前
MySQL之表的约束
android·数据库·c++·后端·mysql·开发·约束
轩情吖1 个月前
MySQL内置函数
android·数据库·c++·后端·mysql·开发·函数