yolo-video:流式扩 展
本案例剖析基于市场 v2.7.6(扩展原名 yolo-video-v2,现更名 yolo-video;上游 main 尚未同步新目录名,深链在合并前会 404)。行号以 audit 时点为准,以仓库实际代码为准。2026-09 起版本推进至 2.7.8(src/lib.rs 约 3700 行、备份文件已清理),行号相应偏移。
阅读提示:全篇约 950 行,涵盖 案例背景 → 架构总览 → 核心实现剖析 → 关键设计决策 → 与 NeoMind 主体的集成 → 测试与验证策略 → 部署运维与排障;时间有限可先读 案例背景 与 关键设计决策。
案例背景
yolo-video 是 NeoMind 生态中最复杂的流式扩展——它把 Ultralytics YOLOv11 目标检测模型挂到实时视频流 上,支持三类数据来源(RTSP/RTMP/HLS 网络流、本地摄像头、前端 base64 帧推送),在 Push 模式下持续把带检测框的 JPEG 帧和结构化检测 JSON 推回前端。
业务能力方面,附带 ROI 区域计数、越线计数、智能抓拍规则(阈值/出现/消失触发)等业务能力。
audit 时点版本 2.7.6,核心代码约 2829 行 Rust(src/lib.rs)+ 721 行(src/detector.rs)+ 387 行(src/video_source.rs),是本系列单 crate 代码量最大的扩展,也是最完整地使用 SDK StreamCapability + StreamMode::Push + send_push_output FFI 链路的扩展(stream-player、voice-assistant 等也使用该链路,但以本案例用法最完整)。
它解决了什么问题? NeoMind 的同步能力桥(参考 案例 #2)适合「事件驱动 + 单帧推理」——设备图像更新时跑一次 YOLO。
但视频分析场景是连续帧流:一个 RTSP 摄像头每秒产出 25~30 帧,每帧都需要推理、统计、可视化。如果用同步能力桥轮询,每秒要发起 30 次跨进程调用,延迟和开销都不可接受。
yolo-video 用 Push 模式解决了这个问题:
- 扩展在
init_session时拉起一条专用的 OS 线程跑帧循环 - 每帧通过
send_push_outputFFI 直接把结果灌入 SDK 的输出通道 - 再由
UnifiedExtensionService中转到前端 WebSocket - 全程不阻塞 runtime 主线程
与 yolo-device-inference 的关键区别(这是理解本案例最重要的对比维度):
| 维度 | yolo-device-inference (2) | yolo-video (3) |
|---|---|---|
| 数据来源 | 订阅已绑定 设备的 image metric(event-driven pull) | RTSP 网络流 / camera:// 本地摄像头 / base64 帧推送(后两者共用 base64 通道) |
| 调用模式 | configure + bind_device 后常驻 | start_stream / stop_stream 显式会话生命周期 |
| 流模式 | 同步能力桥(invoke_capability_sync) | StreamCapability + StreamMode::Push + send_push_output |
| 帧率 | 设备图像更新频率(通常 < 1 FPS) | 视频原生帧率(25~30 FPS) |
| 线程模型 | runtime 主线程 + block_in_place | 专用 OS 线程跑帧循环,与 tokio runtime 完全解耦 |
目标读者:
- 视觉工程师——要在 NeoMind 上跑实时视频分析,你会看到完整的 RTSP 取流 + YOLO 推理 + JPEG 编码 + Push 推送链路
- SDK 开发者——想理解 Push 流模式,本案例是 SDK
StreamCapability接口唯一的「完整生产级」参考实现
你将学到:
- Push 模式语义——为什么视频流必须用 Push 而非 Pull,
StreamMode::Push在 SDK 层到底做了什么 - 会话生命周期——
init_session→start_push→ 帧循环 →stop_stream的完整状态机和清理逻辑 - 多后端视频源——为什么网络流用 ffmpeg-next、本地摄像头(
camera://)与 base64 推流共用process_session_chunk通道,以及Cargo.toml中声明却未被src/引用的 nokhwa - 跨平台 ONNX Runtime dylib 治理——从
libonnxruntime.so.N版本化符号链接到 Windows DLL 路径再到 macOSDYLD_LIBRARY_PATH - 源码卫生反例——为什么
detector.rs.backup这类备份文件不应该提交到仓库
架构总览
yolo-video 采用五层架构:NeoMind Runtime(WebSocket relay)→ Extension(StreamProcessor + ActiveStream map)→ Detector(YoloDetector 懒加载 usls YOLO)→ Video Source(ffmpeg-next / base64 channel)→ Frontend(YoloVideoDisplay React 组件)。
下图展示数据流向和关键状态机。
流式会话状态机
一个 stream 从创建到销毁经历四个阶段,每个阶段对应 SDK 的一个回调:
| 状态 | 触发方 | 回调 | 内部动作 |
|---|---|---|---|
Created | 前端 init WebSocket | — | ActiveStream 结构体构造,未启动帧循环 |
Initializing | SDK | init_session | 解析 source_url 决定走 ffmpeg(网络流)或 base64 通道(camera:// 本地摄像头 / 前端推送);插入 registry |
Streaming | SDK | start_push | 专用 OS 线程跑帧循环:decode → detect → ROI/line → JPEG → send_push_output |
Stopped | 前端 stop_stream 或断连 | stop_stream | running = false,registry 移除,线程自然退出 |
init_session 的关键判断逻辑在 src/lib.rs L1302-L1308:通过 source_url 的协议前缀(rtsp:// / http:// / camera:// 等)决定走网络流(ffmpeg)还是本地摄像头 / base64 通道(process_session_chunk)。
// lib.rs L1302-L1308
let is_network_stream = source_url.starts_with("rtsp://")
|| source_url.starts_with("rtmp://")
|| source_url.starts_with("hls://")
|| source_url.contains(".m3u8")
|| source_url.starts_with("http://")
|| source_url.starts_with("https://")
|| source_url.starts_with("file://");