docs: initialize YoVision requirements and architecture
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# 📊 居家老人跌倒监护系统 — 方案流程说明(客户版)
|
||||
|
||||
> 来源:Notion · https://app.notion.com/p/3af401dc38a681a186a4fa7a8f5930a5
|
||||
> 导出时间:2026-08-01
|
||||
|
||||
> 面向客户的方案说明材料 · 居家老人跌倒监护系统
|
||||
|
||||
# 一句话说明
|
||||
|
||||
**雷达 7×24 守护,AI 二次确认,确认跌倒后 3 分钟内必定有人响应。**
|
||||
|
||||
平时不看、不录、不传——只有在真正发生异常时,系统才会看一眼。
|
||||
|
||||
# 一、业务流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["老人在家中正常生活"] --> B{"雷达持续监测<br/>姿态异常或长时间静止"}
|
||||
B -->|一切正常| A
|
||||
B -->|疑似跌倒| C["唤醒摄像头<br/>回捞事发前 30 秒画面"]
|
||||
C --> D{"AI 姿态分析<br/>二次确认"}
|
||||
D -->|判定为误报| E["静默记录<br/>不打扰家属"]
|
||||
E --> A
|
||||
D -->|确认跌倒| F["生成预警事件<br/>附带视频证据"]
|
||||
F --> G["第一级:家属 App 推送"]
|
||||
G --> H{"30 秒内确认?"}
|
||||
H -->|已确认| M["家属查看视频<br/>前往处理"]
|
||||
H -->|未确认| I["第二级:短信 + 语音电话"]
|
||||
I --> J{"60 秒内确认?"}
|
||||
J -->|已确认| M
|
||||
J -->|未确认| K["第三级:备用联系人<br/>邻居 / 物业 / 护理员"]
|
||||
K --> L{"90 秒内确认?"}
|
||||
L -->|已确认| M
|
||||
L -->|未确认| N["第四级:呼叫中心人工介入"]
|
||||
M --> O["处置结果回填<br/>误报反馈持续优化系统"]
|
||||
N --> O
|
||||
```
|
||||
|
||||
## 关键时间线
|
||||
|
||||
| 时间 | 发生什么 |
|
||||
| --- | --- |
|
||||
| T+0s | 跌倒发生,雷达捕获 |
|
||||
| T+3s | 摄像头唤醒,AI 开始分析 |
|
||||
| T+10s | 确认跌倒,首次通知发出 |
|
||||
| T+40s | 未响应 → 短信与电话 |
|
||||
| T+100s | 未响应 → 备用联系人 |
|
||||
| T+190s | 未响应 → 呼叫中心人工介入 |
|
||||
|
||||
**最坏情况下,三分钟内一定有真人接手。**
|
||||
|
||||
# 二、系统架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph HOME["用户家中"]
|
||||
CAM["IP 摄像头"]
|
||||
RADAR["毫米波雷达<br/>卧室 / 卫生间"]
|
||||
NVR["边缘盒子<br/>录像 + 设备管理"]
|
||||
CAM --> NVR
|
||||
RADAR --> NVR
|
||||
end
|
||||
subgraph CLOUD["云端平台"]
|
||||
SAVANT["AI 分析引擎<br/>姿态识别 + 跌倒判定"]
|
||||
CORE["业务核心服务<br/>事件 / 证据 / 权限"]
|
||||
ALERT["预警调度引擎<br/>升级链 + 多通道"]
|
||||
DB[("事件与审计库")]
|
||||
SAVANT --> CORE
|
||||
CORE --> ALERT
|
||||
CORE --> DB
|
||||
end
|
||||
NVR -->|"仅上传事件片段"| SAVANT
|
||||
ALERT --> FAM["家属 / 护理员 / 呼叫中心"]
|
||||
CORE --> APP["家属 App"]
|
||||
```
|
||||
|
||||
# 三、为什么这样设计
|
||||
|
||||
## 隐私:视频默认不出户
|
||||
|
||||
- 卧室、卫生间**只装雷达,不装摄像头**——雷达只感知人体轮廓与动作,不成像
|
||||
- 客厅摄像头平时**不解码、不上传**,仅在雷达报警时上传那一小段
|
||||
- 录像存在户内盒子,不全量上云
|
||||
- 家属只能看到**自己家、且与告警相关**的片段
|
||||
|
||||
## 可靠:不依赖单一环节
|
||||
|
||||
| 风险 | 应对 |
|
||||
| --- | --- |
|
||||
| 家属没看手机 | 短信 + 语音电话自动升级,可穿透勿扰模式 |
|
||||
| 家属不在本地 | 升级至备用联系人与呼叫中心 |
|
||||
| 家庭断网 | 边缘盒子本地缓存,恢复后补传;断网本身触发运维告警 |
|
||||
| 设备故障 | 每日自动探活,离线超阀值主动联系用户 |
|
||||
| 单户故障 | 一户一盒子,互不影响 |
|
||||
|
||||
## 准确:两级判定降误报
|
||||
|
||||
单靠摄像头或单靠雷达,误报都会让家属很快失去信任。
|
||||
|
||||
- **一级(雷达)**:宁可多报,不能漏报
|
||||
- **二级(AI 视频)**:审核一级的候选,剥离坐下、弯腰、宠物、访客等干扰
|
||||
- **误报反馈**:家属标记的每一次误报都会回流系统,**越用越准**
|
||||
|
||||
## 可追溯:每一条告警都有完整记录
|
||||
|
||||
什么时间触发、基于什么证据、向谁发过、走的什么通道、是否送达、谁在何时确认、处置结果如何——全部不可篡改地留存,可导出备查。
|
||||
|
||||
# 四、技术基座
|
||||
|
||||
核心组件均基于成熟开源项目,**无厂商锁定,数据完全可控**:
|
||||
|
||||
| 环节 | 技术基座 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 边缘录像与设备管理 | Kerberos Agent | MIT 许可,可商用 |
|
||||
| AI 分析流水线 | Savant | 开源,支持故障隔离与弹性扩容 |
|
||||
| 预警调度 | GoAlert | Apache-2.0,由 Target 公司开源并在内部生产使用 |
|
||||
| 边缘硬件 | RK3588 平台 | 无风扇设计,无噪音,低功耗 |
|
||||
| 业务平台与家属 App | 自主开发 | 根据业务需求持续迭代 |
|
||||
|
||||
# 五、实施路径
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P1["试点验证<br/>10 户"] --> P2["小规模运营<br/>50 户"] --> P3["全量部署<br/>200 户"]
|
||||
```
|
||||
|
||||
| 阶段 | 重点 | 产出 |
|
||||
| --- | --- | --- |
|
||||
| 试点 | 验证检测准确率与安装流程 | 真实误报基线、安装规范 |
|
||||
| 小规模 | 验证告警响应链与运维能力 | SLA 实测数据、客服流程 |
|
||||
| 全量 | 规模化与成本优化 | 正式运营 |
|
||||
|
||||
---
|
||||
|
||||
> 本文档为方案概述。具体技术实现、容量规划与风险控制见《线上生产开发方案》。
|
||||
@@ -0,0 +1,183 @@
|
||||
# 🧪 跌倒检测视频接入系统 — 演示与验证开发方案
|
||||
|
||||
> 来源:Notion · https://app.notion.com/p/3af401dc38a6815297c0f1645631e26f
|
||||
> 导出时间:2026-08-01
|
||||
|
||||
> 版本 v1 · 配套文档:《线上生产开发方案》。本文档描述演示与前期验证阶段的做法,与生产架构的关系必须清晰界定。
|
||||
|
||||
# 1. 本阶段的目的(先划清边界)
|
||||
|
||||
演示阶段有两个完全不同的目标,容易被混为一谈,必须分开处理:
|
||||
|
||||
| 目标 | 用什么做 | 产出 |
|
||||
| --- | --- | --- |
|
||||
| A. 摄像头兼容性验证 | MiBeeNvr(现成 NVR) | 采购白名单 |
|
||||
| B. 效果演示给非技术干系人 | MiBeeNvr | 可看的界面 |
|
||||
| C. 架构风险验证 | 200 行自研骨架 + mediamtx | 生产代码 |
|
||||
|
||||
## 关键判断:MiBeeNvr 不是「v0 版本」
|
||||
|
||||
Mi-Bee-Studio/MiBeeNvr 在架构上替代的是 **mediamtx**(它自带 HLS/WebRTC/HTTP-FLV/RTMP/SRT 全套媒体服务),而不是生产方案里要自研的设备管理层。
|
||||
|
||||
```
|
||||
演示版: MiBeeNvr(一个 exe,全包)
|
||||
生产版: mediamtx + 生成的 API 客户端 + 自研骨架 + 对账器 + NAT 隧道
|
||||
↑ 没有一行代码从演示版继承
|
||||
```
|
||||
|
||||
它跑通了,对生产架构零验证。NAT 穿透、path 对账、按需拉流、多租户这些真正会翻车的地方,它一个都不涉及(它假设摄像头与自己在同一局域网)。
|
||||
|
||||
因此:不要把它包装成第一版系统然后计划后续替换,替换是推倒重来而非迭代。
|
||||
|
||||
# 2. 目标 A/B:MiBeeNvr 作为测试台
|
||||
|
||||
## 2.1 项目基本情况
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| 仓库 | Mi-Bee-Studio/MiBeeNvr |
|
||||
| 许可 | MIT |
|
||||
| 技术栈 | Go + Svelte 5 + SQLite,CGO_ENABLED=0 单二进制 |
|
||||
| Web UI | 端口 9090,内置登录、实时预览、录像、系统指标 |
|
||||
| 规模定位 | 树莓派 3B / 1GB 内存可跑(homelab 场景) |
|
||||
|
||||
## 2.2 为什么适合做兼容性测试台
|
||||
|
||||
- ONVIF 摄像头接入局域网后后台自动登记,无需手动扫描
|
||||
- 认证摄像头进入「待激活」状态等待补凭据;未认证的直接开始录制
|
||||
- IP 自愈:摄像头换 IP(Wi-Fi 漫游换 AP)时按序列号自动重定位并重连,使用 unicast 探测,可跨路由子网
|
||||
- 一条 [install.sh](http://install.sh),10 分钟起来,界面上直接看哪个型号能发现、能出流、能 PTZ
|
||||
|
||||
比自己写测试代码快得多,失败信息本身就有价值:某型号它都认不出来,说明该型号 ONVIF 实现有问题,直接从采购清单划掉。
|
||||
|
||||
## 2.3 部署
|
||||
|
||||
```bash
|
||||
# 方式一:一键安装
|
||||
curl -fsSL https://raw.githubusercontent.com/Mi-Bee-Studio/MiBeeNvr/main/install.sh | sudo bash
|
||||
|
||||
# 方式二:预编译二进制
|
||||
wget https://github.com/Mi-Bee-Studio/MiBeeNvr/releases/latest/download/mibee-nvr-amd64
|
||||
chmod +x mibee-nvr-amd64
|
||||
./mibee-nvr-amd64 init --password <yourpassword>
|
||||
./mibee-nvr-amd64 -config mibee-nvr.yaml
|
||||
# 打开 http://localhost:9090
|
||||
```
|
||||
|
||||
## 2.4 使用红线
|
||||
|
||||
| 允许 | 禁止 |
|
||||
| --- | --- |
|
||||
| 实验室环境接自购测试摄像头 | 接入真实住户家中的摄像头 |
|
||||
| 给干系人做效果演示 | 作为生产系统的第一版上线 |
|
||||
| 阅读源码借鉴设计 | 作为长期依赖引入 go.mod |
|
||||
|
||||
理由:项目 84 star / 10 fork / 1 watcher,且功能面异常宽(SRT、WebRTC WHEP、小米 TUTK P2P、WebDAV、FTP、浏览器端 ONNX 推理、时移录制等),一个体量如此的项目在短时间做出这么宽的面,大概率有大量 AI 辅助生成,不能假设它被真实压测过。本项目是养老场景,摄像头对着卧室和卫生间,未经充分验证的组件出安全问题后果不是技术问题。
|
||||
|
||||
## 2.5 值得借鉴的源码
|
||||
|
||||
```
|
||||
internal/onvif/ ONVIF 封装(与 EdgeX device-onvif-camera 对比着读)
|
||||
internal/camera/ 健康监控 + 自动修复状态机
|
||||
internal/store/ SQLite schema 设计
|
||||
```
|
||||
|
||||
重点看两处真实踩坑设计:IP 自愈(序列号而非 IP 作身份)和待激活状态机(批量开通的中间态)。这两点自己从零设计想不到。
|
||||
|
||||
前端 go:embed 打包成单二进制的做法,将来给自研服务加 Web UI 时可直接参考。
|
||||
|
||||
# 3. 目标 C:架构验证骨架(生产代码)
|
||||
|
||||
这部分是真正要写的东西,约 200 行,全部为生产代码而非丢弃品。
|
||||
|
||||
## 3.1 范围
|
||||
|
||||
```
|
||||
mediamtx(配置文件里写死 5 路测试摄像头)
|
||||
+
|
||||
单个 Go 程序:
|
||||
ONVIF 连接 -> GetStreamUri -> 调 mediamtx API 建 path -> 每 30s 探活
|
||||
```
|
||||
|
||||
跑通之后就已经站在生产架构上了,后面是往上加(SQLite、对账、隧道、多租户),不是推倒。
|
||||
|
||||
## 3.2 代码结构
|
||||
|
||||
```
|
||||
cmd/edge-agent/main.go
|
||||
internal/
|
||||
onvif/ 连接、GetProfiles、GetStreamUri、SetSystemDateAndTime
|
||||
store/ 先用 SQLite,schema 与生产 Postgres 保持一致
|
||||
mtx/ oapi-codegen 生成的客户端 + 薄封装
|
||||
reconcile/ 最小对账循环(幂等 + 退避)
|
||||
probe/ 探活 goroutine
|
||||
```
|
||||
|
||||
## 3.3 从第一天就要遵守的约定
|
||||
|
||||
| 约定 | 原因 |
|
||||
| --- | --- |
|
||||
| 设备身份用序列号,不用 IP | DHCP / Wi-Fi 漫游会换 IP |
|
||||
| 建 path 前先查是否存在 | 盲目 add 会踢掉正在推流的 publisher |
|
||||
| 改配置前比对哈希 | 同上;没变就不调 API |
|
||||
| 重试用指数退避 | 无事务,只能靠幂等重试 |
|
||||
| 并发限流(信号量 8) | 避免打爆 mediamtx API |
|
||||
| 设备状态含 pending_activation | 批量开通必然有凭据待补的中间态 |
|
||||
|
||||
## 3.4 mediamtx 侧最小配置
|
||||
|
||||
```yaml
|
||||
api: yes
|
||||
apiAddress: :9997
|
||||
|
||||
pathDefaults:
|
||||
sourceOnDemand: yes
|
||||
sourceOnDemandCloseAfter: 30s
|
||||
rtspTransport: tcp
|
||||
```
|
||||
|
||||
# 4. 摄像头兼容性验证清单(M0 出口标准)
|
||||
|
||||
采购 3-5 个候选型号,这是整个项目第一件要做的事。
|
||||
|
||||
## 4.1 必过项
|
||||
|
||||
| 验证项 | 通过标准 | 不通过的后果 |
|
||||
| --- | --- | --- |
|
||||
| GetProfiles | 返回至少两个 profile(主码流 + 子码流) | 无法自动获取子码流,需人工配置 |
|
||||
| GetStreamUri | 返回可用的 RTSP URL | 无法自动化开通 |
|
||||
| SetSystemDateAndTime | 调用成功且生效 | 事件时间戳不可信 |
|
||||
| 子码流规格 | 可设为 640x360 或 704x576 左右 | 带宽和算力估算失效 |
|
||||
| 编码格式 | 支持 H.264 | H.265 浏览器无法原生预览 |
|
||||
| WS-Security 认证 | digest 或 usernametoken 任一可用 | 认证失败 |
|
||||
| RTSP over TCP | 长时间稳定不断流 | 家庭网络下 UDP 丢包严重 |
|
||||
|
||||
## 4.2 加分项
|
||||
|
||||
- 支持 ONVIF 事件订阅(移动侦测)
|
||||
- 支持修改 GOP 长度(影响回捞缓存的关键帧间隔)
|
||||
- 支持创建独立的只读用户(不必用 admin 凭据接入)
|
||||
|
||||
## 4.3 结论落地
|
||||
|
||||
任何一项必过项失败的型号,直接从采购清单划掉。后期写兼容代码的成本远高于换型号。验证结果整理成表格,作为采购白名单归档。
|
||||
|
||||
# 5. 阶段产出如何流入生产
|
||||
|
||||
| 演示阶段产出 | 流向 |
|
||||
| --- | --- |
|
||||
| 采购白名单 | 生产方案的硬件选型 |
|
||||
| 骨架代码(onvif / store / mtx / reconcile) | 直接成为生产代码基础 |
|
||||
| MiBeeNvr 的设计借鉴(IP 自愈、待激活态) | 写进生产的对账器与状态机 |
|
||||
| MiBeeNvr 的界面截图 | 干系人沟通材料 |
|
||||
| MiBeeNvr 本身 | 不进入生产 |
|
||||
|
||||
# 6. 关于 Web 管理界面
|
||||
|
||||
生产组合(mediamtx + 自研服务)没有现成的管理界面。三个选择:
|
||||
|
||||
1. 前期不做 UI — 用 REST API + 脚本撑过验证期。250 路的开通本来就是批量脚本,不该手点。
|
||||
2. 临时用 go2rtc 的 1984 页面看流状态 — 但它默认无密码,局域网内任何人可访问,必须在配置里关闭或用防火墙隔离。
|
||||
3. 正式做时长在自研服务上 — 因为要展示的是「家庭 -> 摄像头 -> 流状态」的业务视图,第三方面板展示的是「path 列表」,不是一回事。
|
||||
|
||||
> 补充:mediamtx 本身没有管理后台,作者明确表示精力有限、优先保证核心稳定,欢迎社区贡献。社区面板有 mediamtx-dashboard、lgcshy/mediamtx-ui、nabaco/mediamtx-ui、bcanfield/mediamtx-connect 等,均为独立进程通过 API 对接,可按需临时使用。
|
||||
@@ -0,0 +1,729 @@
|
||||
# 🏗️ 跌倒检测视频接入系统 — 线上生产开发方案
|
||||
|
||||
> 来源:Notion · https://app.notion.com/p/3af401dc38a681c29985ffdbd0e9f3d1
|
||||
> 导出时间:2026-08-01
|
||||
|
||||
> 版本 v1 · 面向 200 户 / 250 路 IP 摄像头的跌倒检测视频接入系统。本文档描述**生产环境**架构与开发规范。演示与验证阶段见配套文档《演示与验证开发方案》。
|
||||
|
||||
# 1. 目标与约束
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| 接入规模 | 约 200 个家庭,250 路 IP 摄像头 |
|
||||
| 业务目标 | 视频流分析实现摔倒动作检测 |
|
||||
| 关键约束 | 摄像头位于家庭路由器 NAT 之后,中心机房无法主动访问 |
|
||||
| 未来扩展 | 毫米波雷达等异构传感器(架构必须预留) |
|
||||
| 技术栈 | Go 为主,媒体层复用成熟开源组件 |
|
||||
|
||||
# 2. 架构总览
|
||||
|
||||
系统分为三个平面,接缝要尽可能少。
|
||||
|
||||
| 平面 | 组件 | 职责 |
|
||||
| --- | --- | --- |
|
||||
| 流控制面 | mediamtx + 自研 Go 编排服务 | path 生命周期、按需拉流、录制、对账 |
|
||||
| 设备业务面 | 自研 Go 服务(单 exe)+ Postgres | 家庭/摄像头台账、凭据、在线状态、开通与停用 |
|
||||
| 感知面 | 自研推理服务 | 抽帧、姿态模型、跌倒判定 |
|
||||
|
||||
## 2.1 控制面与数据面分离(核心决策)
|
||||
|
||||
这是解决 NAT 问题的关键:
|
||||
|
||||
- **控制面走隧道**:每户边缘盒子跑 WireGuard,中心服务通过隧道访问摄像头的 ONVIF 端口。ONVIF 控制流量极小,只在配置和探活时产生。
|
||||
- **数据面走推流**:视频不走隧道。边缘盒子主动把流推到中心 mediamtx(RTSP announce / SRT / RTMP)。
|
||||
|
||||
### IP 规划
|
||||
|
||||
每户分配一个不冲突的内网段,摄像头地址确定化:
|
||||
|
||||
```
|
||||
10.<户号高位>.<户号低位>.0/24
|
||||
摄像头固定从 .10 起分配
|
||||
```
|
||||
|
||||
这样设备地址可预测,不依赖自动发现,也便于对账。
|
||||
|
||||
# 3. 组件选型
|
||||
|
||||
| 组件 | 仓库 | 角色 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| mediamtx | bluenviron/mediamtx | 媒体路由 | MIT,Go,单二进制。协议互转、按需拉流、录制、鉴权回调、Prometheus |
|
||||
| gortsplib | bluenviron/gortsplib | RTSP 库 | mediamtx 作者同款。用于自研 worker 拉流并做帧级丢弃 |
|
||||
| ONVIF 库 | IOTechSystems/onvif 或 use-go/onvif | 设备控制 | 前者是 EdgeX device-onvif-camera 内部使用的增强版 |
|
||||
| API 客户端 | 自行用 oapi-codegen 从 mediamtx OpenAPI 生成 | mediamtx 控制 | 见 3.1 |
|
||||
| 边缘推流 | mediamtx 或 go2rtc | 边缘盒子 | 杂牌摄像头兼容性差时用 go2rtc 兜底 |
|
||||
|
||||
## 3.1 关于 mediamtx-sdk-go
|
||||
|
||||
zbiljic/mediamtx-sdk-go 是个人项目,用 ogen 从官方 OpenAPI 规范生成。**建议自行生成客户端**,避免依赖第三方更新节奏:
|
||||
|
||||
```bash
|
||||
# 从 mediamtx 仓库取 apidocs/openapi.yaml
|
||||
oapi-codegen -package mtxapi -generate types,client openapi.yaml > mtxapi/client.go
|
||||
```
|
||||
|
||||
好处是版本跟随实际部署的 mediamtx,不会因上游停更而受阻。
|
||||
|
||||
# 4. 关键设计决策
|
||||
|
||||
## 4.1 mediamtx 只管流,不管设备
|
||||
|
||||
mediamtx 的数据模型里没有 camera 实体,只有 path(名字 + source URL + 配置)。它不知道 URL 背后是哪个品牌、是否同一台设备的两条码流、这户是否欠费。
|
||||
|
||||
必须自建设备业务面。三条硬约束:
|
||||
|
||||
1. **API 改的配置不落盘** — mediamtx 重启后通过 API 添加的 path 全部消失。设备台账必须在自己的数据库,服务启动时 replay。
|
||||
2. **改配置会踢掉当前发布者** — 任何通过 API 的 path 配置变更都会触发 path reload,断开正在推流的 source,即使改动本身与它无关。因此改配置前必须先判断「真的变了吗」,没变绝对不调 API。
|
||||
3. **没有 enable/disable 开关** — 只能删除 path,不能停用。
|
||||
|
||||
## 4.2 按需拉流(sourceOnDemand)
|
||||
|
||||
```yaml
|
||||
pathDefaults:
|
||||
sourceOnDemand: yes
|
||||
sourceOnDemandStartTimeout: 10s
|
||||
sourceOnDemandCloseAfter: 30s
|
||||
```
|
||||
|
||||
语义:只有 reader 连上来时才去连摄像头,没人看就断开。对本系统等价于「AI worker 订阅 = 开始分析,取消订阅 = 自动停止拉流」,是最省带宽和算力的开关方式。
|
||||
|
||||
## 4.3 停用某路摄像头的四种粒度
|
||||
|
||||
按副作用从轻到重:
|
||||
|
||||
| 需求 | 做法 | 效果 |
|
||||
| --- | --- | --- |
|
||||
| 暂停分析,流保留 | worker 取消订阅 | 配合 sourceOnDemand 自动停止拉流,随时恢复 |
|
||||
| 立即断开,允许重连 | `POST /v3/rtspsessions/kick/{id}`(RTMP/SRT/WebRTC 各有对应接口) | 断一次,客户端会重连 |
|
||||
| 永久拒绝 | 鉴权服务对该 path 返回 401 + kick | 推不上来,且无需改 mediamtx 配置 |
|
||||
| 彻底移除 | `DELETE /v3/config/paths/delete/{name}` | path 消失,重启后靠自有库恢复 |
|
||||
|
||||
标准组合:先 kick 断开、再让鉴权拒绝 = 立刻生效且不会重连。
|
||||
|
||||
## 4.4 鉴权用 HTTP 回调
|
||||
|
||||
```yaml
|
||||
authMethod: http
|
||||
authHTTPAddress: http://control-plane:8080/mtx-auth
|
||||
paths:
|
||||
"~^home-.*$":
|
||||
source: publisher
|
||||
```
|
||||
|
||||
每次边缘端推流,mediamtx POST 到自有鉴权服务,带 user/password/path/IP/action。返回 200 或 401 即控制了「哪些摄像头允许接入」。注销、欠费、隐私模式全部在自有业务库判断,mediamtx 侧零配置变更。
|
||||
|
||||
## 4.5 为雷达预留
|
||||
|
||||
未来接入毫米波雷达后,架构会发生一次反转:雷达 7x24 常开做一级触发,视频做二级确认。
|
||||
|
||||
- 算力:常态 GPU 占用接近零,只在事件时唤醒推理。250 路常态推理需 2-4 张卡,改为触发式后一张即可。
|
||||
- 隐私:卫生间、卧室只放雷达不放摄像头;客厅摄像头平时不解码不存储。这是养老场景能否落地的分水岭。
|
||||
- 误报:纯视觉对遮挡/逆光/坐下误报高,纯雷达对快速坐下和弯腰误判,两级串联是行业标准做法。
|
||||
|
||||
当前阶段的设计要求:设备表结构不要写死为「摄像头表」,预留 device_type;编排服务的触发入口要独立成一个 handler,将来接雷达告警时不用改动流控制逻辑。
|
||||
|
||||
# 5. 数据模型
|
||||
|
||||
```sql
|
||||
-- 家庭
|
||||
CREATE TABLE households (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
subnet CIDR NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- 设备(不要命名为 cameras,为雷达预留)
|
||||
CREATE TABLE devices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
household_id BIGINT NOT NULL REFERENCES households(id),
|
||||
device_type TEXT NOT NULL,
|
||||
serial TEXT UNIQUE NOT NULL,
|
||||
vendor TEXT,
|
||||
model TEXT,
|
||||
onvif_addr TEXT,
|
||||
onvif_user TEXT,
|
||||
onvif_secret TEXT,
|
||||
state TEXT NOT NULL,
|
||||
last_seen_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 流绑定
|
||||
CREATE TABLE stream_bindings (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
device_id BIGINT NOT NULL REFERENCES devices(id),
|
||||
mtx_instance TEXT NOT NULL,
|
||||
path_name TEXT NOT NULL,
|
||||
rtsp_url TEXT,
|
||||
profile_token TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
UNIQUE (mtx_instance, path_name)
|
||||
);
|
||||
|
||||
-- 对账状态
|
||||
CREATE TABLE sync_state (
|
||||
binding_id BIGINT PRIMARY KEY REFERENCES stream_bindings(id),
|
||||
desired_hash TEXT,
|
||||
applied_hash TEXT,
|
||||
last_error TEXT,
|
||||
retry_after TIMESTAMPTZ
|
||||
);
|
||||
```
|
||||
|
||||
字段说明:device_type 取 camera / radar / button;state 取 pending_activation / active / disabled;onvif_secret 需加密存储;rtsp_url 存 ONVIF GetStreamUri 返回的子码流地址。
|
||||
|
||||
**设计要点**:以序列号而非 IP 作为设备身份。家庭 Wi-Fi 漫游或 DHCP 续租会导致 IP 变化,按序列号重定位是唯一可靠的做法。
|
||||
|
||||
# 6. 对账器(Reconciler)
|
||||
|
||||
系统最容易出竞态的地方,也是自研工作量的核心。
|
||||
|
||||
## 6.1 职责
|
||||
|
||||
数据库是唯一真相源,**mediamtx 和 Savant 都是被驱动的从属状态**。对账器持续把三者拉齐。
|
||||
|
||||
关键原则:**水平触发(level-triggered)而非边沿触发(edge-triggered)**。对账器不处理「事件」,只不断比对期望状态与实际状态并向期望收敛。这是处理部分失败的唯一可行方案(见 6.5)。
|
||||
|
||||
```go
|
||||
func (r *Reconciler) Sync(ctx context.Context) error {
|
||||
desired := r.db.EnabledBindings() // 真相源
|
||||
actual := r.mtx.ListPaths(ctx) // 生成的 API 客户端
|
||||
|
||||
add, del, update := diff(desired, actual)
|
||||
|
||||
sem := semaphore.NewWeighted(8) // 限流,别打爆 API
|
||||
for _, b := range add {
|
||||
// 幂等:已存在则跳过,绝不盲目 add(会踢掉 publisher)
|
||||
// 重试:指数退避,失败写回 sync_state.retry_after
|
||||
}
|
||||
for _, b := range update {
|
||||
// 关键:先比对 desired_hash 与 applied_hash
|
||||
// 没变就绝对不调 API
|
||||
}
|
||||
// del 同理
|
||||
}
|
||||
```
|
||||
|
||||
## 6.2 必须处理的边界情况
|
||||
|
||||
| 情况 | 处理 |
|
||||
| --- | --- |
|
||||
| mediamtx 重启 | path 数量骤降触发全量重建;也要有定时全量对账兜底 |
|
||||
| 配置变更踢 publisher | 变更前比对哈希;确需变更时先标记为维护窗口 |
|
||||
| API 批量失败 | 无事务。第 137 个失败时前 136 个已生效,必须幂等重试而非回滚 |
|
||||
| 并发打满 | 信号量限流到 8 并发 |
|
||||
| path 漂移 | 定时全量 diff,处理人工在 mediamtx 侧的手改 |
|
||||
| 设备离线 vs 无人订阅 | 两者在 mediamtx 侧表现相同,靠自有探活区分 |
|
||||
|
||||
## 6.3 探活
|
||||
|
||||
独立 goroutine,对每个 active 设备定期做 ONVIF 轻量调用(如 GetSystemDateAndTime),指数退避,更新 devices.last_seen_at。**不要用 path 在线状态代替设备在线状态**。
|
||||
|
||||
## 6.4 双系统对账:mediamtx + Savant
|
||||
|
||||
引入 Savant(第 9 节)后,一个设备的完整开通需要在**两个独立系统**上各做一次操作,而两者之间**没有分布式事务**。
|
||||
|
||||
### 数据模型调整
|
||||
|
||||
第 5 节的 `sync_state` 拆成每系统一行:
|
||||
|
||||
```sql
|
||||
DROP TABLE sync_state;
|
||||
|
||||
CREATE TABLE sync_state (
|
||||
binding_id BIGINT NOT NULL REFERENCES stream_bindings(id),
|
||||
system TEXT NOT NULL, -- 'mediamtx' | 'savant'
|
||||
desired_hash TEXT,
|
||||
applied_hash TEXT, -- NULL = 尚未生效
|
||||
last_error TEXT,
|
||||
attempt INT NOT NULL DEFAULT 0,
|
||||
retry_after TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (binding_id, system)
|
||||
);
|
||||
|
||||
-- 便于运维发现中间态
|
||||
CREATE VIEW binding_health AS
|
||||
SELECT b.id, b.path_name,
|
||||
bool_and(s.desired_hash = s.applied_hash) AS converged,
|
||||
max(s.attempt) AS max_attempt
|
||||
FROM stream_bindings b JOIN sync_state s ON s.binding_id = b.id
|
||||
WHERE b.enabled
|
||||
GROUP BY b.id, b.path_name;
|
||||
```
|
||||
|
||||
一个 binding 只有当**两行都收敛**时才算真正开通。`converged = false` 的就是中间态,必须在 Grafana 上可见。
|
||||
|
||||
### 操作顺序(不对称,必须遵守)
|
||||
|
||||
| 动作 | 顺序 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| **开通** | mediamtx 先 → Savant 后 | 流必须先存在,Savant 才能挂上去;反之 Savant 会反复重试连不存在的源 |
|
||||
| **停用** | Savant 先 → mediamtx 后 | 先停消费再断流;反之 Savant 会看到源断开并报错告警 |
|
||||
|
||||
建立和拆除是逆序的,这一点要写进代码注释,很容易被后来人改错。
|
||||
|
||||
### ⚠️ 关键耦合:Savant source = mediamtx reader
|
||||
|
||||
`sourceOnDemand: yes` 的语义是「有 reader 才拉流」。**Savant 挂上 source 就是一个 reader**,也就是说:
|
||||
|
||||
- 挂载 Savant source → mediamtx 立即开始拉流 → 产生带宽和连接开销
|
||||
- 卸载 Savant source → 30s 后(`sourceOnDemandCloseAfter`)mediamtx 自动断流
|
||||
|
||||
因此:**「暂停分析」必须通过卸载 Savant source 实现,不能只在 Python 插件里 `return`**。后者会让 mediamtx 持续拉流、带宽白烧,而监控上看起来一切正常。
|
||||
|
||||
这也意味着第 4.3 节「停用粒度」表格里的第一行(暂停分析)具体实现就是卸载 Savant source。
|
||||
|
||||
## 6.5 部分失败:不回滚,只收敛
|
||||
|
||||
典型场景:mediamtx path 建好了,Savant source 挂载失败。
|
||||
|
||||
**错误做法**:回滚删掉 mediamtx path。回滚本身也会失败,那时候你需要回滚的回滚,无限递归。
|
||||
|
||||
**正确做法**:保留已成功的部分,把失败的那行 `sync_state` 标为未收敛 + 退避重试。下一轮对账会看到 mediamtx 已收敛(跳过)、Savant 未收敛(重试)。
|
||||
|
||||
代价:**会有一个无人消费的 mediamtx path 短暂存在**。因为 `sourceOnDemand` 的存在,这个 path 没有 reader 就不会拉流,**成本接近零**。这是 `sourceOnDemand` 给双系统对账带来的额外好处——中间态是安全的。
|
||||
|
||||
### 重试预算
|
||||
|
||||
| 参数 | 值 |
|
||||
| --- | --- |
|
||||
| 初始退避 | 5s |
|
||||
| 退避倍率 | 2x,上限 5min |
|
||||
| 告警阈值 | `attempt > 5` 且持续 > 15min → 告警 |
|
||||
| 放弃阈值 | 不放弃。持续重试,靠告警引入人工 |
|
||||
|
||||
不设放弃阈值是故意的:对账器放弃了,设备就静默地不工作了,而这是养老监护场景不可接受的失败模式。
|
||||
|
||||
## 6.6 孤儿资源清理
|
||||
|
||||
对账不只是「把缺的补上」,还要「把多的删掉」。三类孤儿:
|
||||
|
||||
| 类型 | 成因 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| mediamtx 有 path,DB 无 | 人工手改、旧版本残留 | 删除(但先日志,不静默) |
|
||||
| Savant 有 source,DB 无 | 同上 | 卸载 |
|
||||
| DB 有 binding,两边都无且长期未收敛 | 开通失败后无人处理 | 告警,不自动删 DB |
|
||||
|
||||
**清理动作必须有安全闸**:单轮对账删除量超过总量的 10%(如 25 路)时**停止并告警**,不执行。这道闸是防数据库连接异常或查询 bug 导致的「期望集合意外为空」——那会把 250 路全部拆掉。
|
||||
|
||||
## 6.7 完整循环
|
||||
|
||||
```go
|
||||
func (r *Reconciler) Sync(ctx context.Context) error {
|
||||
desired := r.db.EnabledBindings() // 真相源
|
||||
if len(desired) == 0 && r.lastDesiredCount > 0 {
|
||||
return ErrSuspiciousEmptyDesired // 6.6 安全闸
|
||||
}
|
||||
|
||||
mtxActual, err1 := r.mtx.ListPaths(ctx) // 分片内所有实例
|
||||
svActual, err2 := r.savant.ListSources(ctx)
|
||||
if err1 != nil || err2 != nil {
|
||||
return errors.Join(err1, err2) // 拿不到实际状态就不动作
|
||||
}
|
||||
|
||||
plan := buildPlan(desired, mtxActual, svActual)
|
||||
// plan 内部已按 6.4 的顺序约束排序:
|
||||
// teardown: savant 先 → mtx 后
|
||||
// setup: mtx 先 → savant 后
|
||||
|
||||
for _, step := range plan {
|
||||
if !step.DueForRetry(now) { continue } // 退避窗口未到
|
||||
if step.DesiredHash == step.AppliedHash { continue } // 没变,绝不调 API
|
||||
|
||||
sem.Acquire(ctx, 1) // 每系统独立限流
|
||||
go func() {
|
||||
defer sem.Release(1)
|
||||
if err := step.Apply(ctx); err != nil {
|
||||
r.db.MarkFailed(step, err) // 退避,不回滚
|
||||
return
|
||||
}
|
||||
r.db.MarkApplied(step) // applied_hash = desired_hash
|
||||
}()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**两个独立的限流预算**:mediamtx API 和 Savant API 各自 8 并发,不共享信号量。否则一个系统变慢会饿死另一个。
|
||||
|
||||
**循环周期**:正常 30s;有未收敛项时降到 5s;另有每 10min 一次的全量对账(含孤儿扫描)。
|
||||
|
||||
## 6.8 必须暴露的指标
|
||||
|
||||
| 指标 | 用途 |
|
||||
| --- | --- |
|
||||
| `reconciler_unconverged_bindings{system}` | 核心健康指标,持续 > 0 即异常 |
|
||||
| `reconciler_apply_duration_seconds{system,action}` | 发现 API 变慢 |
|
||||
| `reconciler_orphans_detected{type}` | 有人手改了生产系统 |
|
||||
| `reconciler_safety_brake_triggered` | 安全闸触发,**应该直接呼叫**人 |
|
||||
| `reconciler_loop_duration_seconds` | 循环超时 = 规模到顶,该分片了 |
|
||||
|
||||
# 7. mediamtx 配置要点
|
||||
|
||||
```yaml
|
||||
api: yes
|
||||
apiAddress: :9997
|
||||
|
||||
metrics: yes
|
||||
metricsAddress: :9998
|
||||
|
||||
authMethod: http
|
||||
authHTTPAddress: http://control-plane:8080/mtx-auth
|
||||
|
||||
pathDefaults:
|
||||
sourceOnDemand: yes
|
||||
sourceOnDemandCloseAfter: 30s
|
||||
rtspTransport: tcp # 家庭网络下 UDP 丢包严重
|
||||
record: no
|
||||
recordDeleteAfter: 7d
|
||||
runOnOnline: curl -s -X POST http://control-plane:8080/hooks/online?path=$MTX_PATH
|
||||
runOnOffline: curl -s -X POST http://control-plane:8080/hooks/offline?path=$MTX_PATH
|
||||
|
||||
paths:
|
||||
"~^home-(\\d+)-cam(\\d+)$":
|
||||
source: publisher
|
||||
```
|
||||
|
||||
用一条正则 path 兜底,不要写 250 条静态配置。
|
||||
|
||||
> 注意:v1.19.x 的钩子命名为 runOnAvailable / runOnUnavailable / runOnOnline / runOnOffline,与旧版 runOnReady / runOnNotReady 不同。部署前核对所用版本的配置参考。
|
||||
|
||||
# 8. 容量与分片
|
||||
|
||||
## 8.1 带宽
|
||||
|
||||
| 项 | 估算 |
|
||||
| --- | --- |
|
||||
| 250 路子码流 640x360 @ 512 kbps | 约 125 Mbps 常态入向 |
|
||||
| 250 路主码流 | 1-1.5 Gbps(不可行,必须用子码流做检测) |
|
||||
|
||||
## 8.2 算力
|
||||
|
||||
- 抽帧到 5 fps:250 x 5 = 1250 帧/秒解码
|
||||
- CPU 软解不可行,需 NVDEC 或边缘解码
|
||||
- 用 gortsplib 在 Go 侧做帧级丢弃(只保留关键帧/按抽帧间隔),比起用 ffmpeg 子进程可省约 70% 解码开销
|
||||
|
||||
## 8.3 分片
|
||||
|
||||
- 单 mediamtx 实例建议承载 60-80 路,250 路分 3-4 个实例
|
||||
- 分片键用家庭 ID,映射存在 stream_bindings.mtx_instance
|
||||
- 不要单实例硬扛:进程挂掉影响面过大,且默认 writeQueueSize: 512 下 250 路的内存与 GC 压力不小
|
||||
|
||||
# 9. 感知面:Savant 推理流水线
|
||||
|
||||
> 本节取代第 2 节表格中「感知面 = 自研推理服务」的占位描述。
|
||||
|
||||
## 9.1 为什么用框架而非完全自研
|
||||
|
||||
开源社区没有「批量摄像头摔倒检测」的成品(算法是差异化,不会有人开源),但有成熟的**多路视频分析流水线框架**。算法是自己的,工程化用现成的。
|
||||
|
||||
选定 **Savant**(insight-platform/Savant),基于 NVIDIA DeepStream 的高层框架,声明式 YAML + Python 插件函数。三个决定性理由:
|
||||
|
||||
1. **故障隔离** — 原生 DeepStream 中数据流与 pipeline 紧耦合,任一源崩溃则整个 pipeline 崩溃,重启加载模型要数秒且期间丢数据。Savant 的 adapter 通过 ZeroMQ 与框架通信,adapter 挂掉不影响 pipeline。200 户家庭网络掉线是常态,**这是 250 路规模的生死线**。
|
||||
2. **可配置降级策略** — 可设为实时模式(算力不足时主动丢数据)或高吞吐模式(保证处理全部数据)。本系统选**实时模式**,丢帧优于积压崩溃。
|
||||
3. **Replay(按需与非线性处理)** — REST 控制的录制/回放/转推服务,直接对应阶段二的雷达触发式二级确认。
|
||||
|
||||
同一套代码可跑在 Jetson(Orin Nano/NX/AGX)和数据中心 GPU 上,与本方案的边缘/中心混合部署契合。
|
||||
|
||||
**代价**:必须 NVIDIA 硬件(Turing/Ampere/Ada/Hopper/Blackwell 或 Jetson),与 DeepStream 版本强绑定(需查对照表),学习曲线陡,无企业支持。
|
||||
|
||||
**备选**:Pipeless(Rust,不绑 NVIDIA,支持 ONNX Runtime/OpenVINO/TensorRT,HTTP 动态增删流)——若硬件选型变更或规模缩小时启用。
|
||||
|
||||
## 9.2 模型选型与许可警告
|
||||
|
||||
> ⚠️ **Ultralytics YOLO 是 AGPL-3.0**。YOLOv8/v11 系列仓库默认对所有用户采用 AGPL-3.0;要集成进商业产品和服务而不遵守其开源要求,必须购买 Enterprise License。本项目是面向付费用户的网络服务,落在 AGPL 第 13 条覆盖范围内,严格执行则需开源整个服务端。**必须在 M3 前做出决定:买授权,或换模型。**
|
||||
|
||||
推荐替代:**RTMPose / RTMO**(MMPose 项目,Apache-2.0)
|
||||
|
||||
- RTMPose-m:COCO 75.8% AP,GTX 1660 Ti + TensorRT 下 430+ FPS,Intel i7-11700 CPU + ONNXRuntime 下 90+ FPS
|
||||
- 支持 ONNXRuntime、TensorRT、ncnn、OpenVINO、RKNN 多种部署后端(通过 MMDeploy)
|
||||
- 关键点定义同为 COCO 17 点,**现有判定算法基本不用改**
|
||||
- 430 FPS 的单卡吞吐意味着第 8 节估算的 1250 fps 需求可用 2-3 张中端卡满足
|
||||
|
||||
## 9.3 Pipeline 架构
|
||||
|
||||
```
|
||||
边缘推流 → mediamtx(250 路汇聚,按需拉流)
|
||||
↓ RTSP
|
||||
Savant RTSP source adapter(多路复用 + 故障隔离)
|
||||
↓ ZeroMQ / Protobuf
|
||||
Buffer Adapter(磁盘持久化缓冲,抗突发)
|
||||
↓
|
||||
Savant module
|
||||
├─ RTMPose / TensorRT(姿态关键点)
|
||||
└─ Python 插件:时序窗口 + 跌倒判定(**自研部分**)
|
||||
↓ ZeroMQ / Protobuf
|
||||
Go 控制面(告警、去重、防抖、通知)
|
||||
```
|
||||
|
||||
需要自己写的只有中间那个 Python 插件,前后两段是现成的。
|
||||
|
||||
## 9.4 与 Go 控制面的对接契约
|
||||
|
||||
一条 pipeline 是自给自足的服务,通过高性能流式 API 与外界通信,视频和元数据统一使用 Protocol Buffers 协议。
|
||||
|
||||
| 需求 | 通道 | 语言无关 |
|
||||
| --- | --- | --- |
|
||||
| 接收检测结果 | 直接订阅 sink 的 ZeroMQ 端点,解 protobuf | ✅ |
|
||||
| 动态增删流 | 动态挂载/卸载 source 与 sink,无需重载 pipeline | ✅ |
|
||||
| 回放/回捞 | Replay 服务的 REST 接口 | ✅ |
|
||||
| 程序化灌数据/调试 | Client SDK(**仅 Python**,集成 OpenTelemetry) | ❌ |
|
||||
|
||||
**设计约束:优先走 ZeroMQ 和 REST 两条语言无关通道,Go 服务不得依赖 Python 中间层。** Client SDK 仅用于算法调试和测试。
|
||||
|
||||
因为两边都是 API 驱动,**第 6 节的对账器可以同时驱动 mediamtx 和 Savant**:开通 = 建 path + 挂 source;停用 = kick + 卸 source。逻辑写在同一个 reconciler 里。
|
||||
|
||||
## 9.5 关键配置决策
|
||||
|
||||
**必须开启 Buffer Adapter**。它实现了 adapter 与 module 之间的持久化事务性磁盘缓冲,用于支撑资源消耗不可预测的 pipeline、抗住流量突发,并把自身条目数和大小导出到 Prometheus。
|
||||
|
||||
> 本场景天然有突发:夜里安静无事,早上起床时段几十户同时有活动。**一开始就加上,别等出问题再补。**
|
||||
|
||||
**AO-RTSP Sink 仅用于调试**。它把处理后、带元数据标注的视频通过 RTSP 和 LL-HLS 播出去,用来直观确认某一路的处理效果。
|
||||
|
||||
> ⚠️ **硬件限制**:GeForce 系列显卡同时硬件编码的流数上限为 **3**。若给多路挂 AO-RTSP 可视化,消费级卡直接卡在 3 路,需 Quadro/Tesla/L4 类专业卡。**不要做成常态功能。**
|
||||
|
||||
**开发期开 Development Server**:支持改代码动态重载、不重启 pipeline,还能从 IDE 远程开发跑在 Jetson 或服务器上的代码。原生 DeepStream 改一行要重启加载模型等几十秒,这个工具能省大量时间。
|
||||
|
||||
## 9.6 可观测
|
||||
|
||||
Savant 没有管理界面(与 mediamtx 同构),但可观测完整:
|
||||
|
||||
- **Prometheus + Grafana**:pipeline 和 buffer adapter 导出运行指标,开发者可声明自定义指标与系统指标一起导出。**把每路的人体检出率、跌倒候选数、推理延迟声明成自定义指标** → Grafana 上就是现成运营看板
|
||||
- **OpenTelemetry → Jaeger**:排查单帧处理延迟
|
||||
- mediamtx 也出 Prometheus 指标,**一个 Grafana 全收**,前期不用写一行前端代码
|
||||
|
||||
## 9.7 算法分层
|
||||
|
||||
| 层 | 内容 | 位置 |
|
||||
| --- | --- | --- |
|
||||
| 姿态提取 | RTMPose,COCO 17 关键点 | Savant 推理单元 |
|
||||
| 单帧启发式 | 包围框宽高比、重心高度、躯干角度 | Python 插件(现有算法) |
|
||||
| 时序判定 | 30 帧滑窗,静态位置 + 动态速度特征 | Python 插件(待建) |
|
||||
| 事件后处理 | 去重、防抖、多源融合 | Go 控制面 |
|
||||
|
||||
参考实现:
|
||||
|
||||
- `GajuuzZ/Human-Falling-Detect-Tracks` — ST-GCN 对每个人轨迹的每 30 帧预测动作,支持站立/行走/坐下/躺下/起身/坐下动作/跌倒共 7 类。**七分类而非二分类是降误报的关键**
|
||||
- `kelsonbatista/fall-detection-ai-project` — YOLO-pose 逐帧抽 128 维特征(静态位置 + 动态速度),30 帧窗口喂 LSTM。注意 GPL-3.0
|
||||
|
||||
**负面经验**:`Y-B-Class-Projects/Human-Fall-Detection` 用约 500 张网络图片训 LSTM 完全学不会,最后退回 if-else 规则。**小数据量下时序模型学不动,先解决数据再上模型。**
|
||||
|
||||
## 9.8 本节风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| --- | --- |
|
||||
| Ultralytics AGPL-3.0 污染商业代码 | M3 前决策:买授权或换 RTMPose |
|
||||
| 公开数据集与真实场景脱节(Le2i/UR Fall 均为演员摆拍的表演性跌倒,真实老人常为缓慢滑落、部分遮挡) | M3 首要产出是**真实误报案例库**,不是模型指标 |
|
||||
| Savant 学习曲线陡 | M1 阶段先用 5 路跑通一条 pipeline,测出单路真实吞吐再反推卡数 |
|
||||
| 绑定 NVIDIA + DeepStream 版本 | 预留 Pipeless 作为备选;推理代码与框架解耦 |
|
||||
| 常态全量推理成本 | **Savant 不降低算力需求**,只解决「怎么高效跑 250 路」。雷达一级触发仍是唯一的数量级优化,与 Replay 配套使用 |
|
||||
|
||||
**无雷达时的过渡方案**:参考 Frigate 的「先用运动侦测过滤、有变化才跑推理」思路,与雷达触发是同一逻辑。
|
||||
|
||||
# 10. 预警信息管理系统
|
||||
|
||||
> 接住第 9 节的输出:检测出来之后怎么办。**本节描述的是产品本身,不是辅助设施。**
|
||||
|
||||
## 10.1 两类告警必须彻底分开
|
||||
|
||||
| | 运维告警 | 业务预警 |
|
||||
| --- | --- | --- |
|
||||
| 触发源 | 设备离线、binding 未收敛、GPU 过载、缓冲积压 | 跌倒判定 |
|
||||
| 收件人 | 运维团队 | 家属、护理员、呼叫中心 |
|
||||
| 延迟容忍 | 分钟级 | **秒级** |
|
||||
| 漏报后果 | 服务降级 | 人身伤害 + 法律责任 |
|
||||
| 方案 | Prometheus + Alertmanager(现成) | **自研状态机 + 复用投递通道** |
|
||||
|
||||
运维那侧接 Alertmanager 就完事,一两天工作量,本节不再展开。**两套系统不得共用投递通道和值班配置**,避免运维噪声淡化业务预警。
|
||||
|
||||
## 10.2 告警生命周期状态机
|
||||
|
||||
```
|
||||
[触发] ─→ [投递中] ─→ [已送达] ─→ [已确认 ack] ─→ [处置中] ─→ [关闭]
|
||||
│ │ │ │
|
||||
│ │ └─超时未 ack─→ [升级] ─→ 下一级投递 │
|
||||
│ └─投递失败─→ [升级] │
|
||||
└─防抖命中─→ [抑制] [标记误报] ←─┘
|
||||
```
|
||||
|
||||
**没有 ack 环节的告警系统等于没有。** 发出去和被人看到是两回事,整个设计围绕「确认或升级」这一对概念展开。
|
||||
|
||||
## 10.3 分级升级链
|
||||
|
||||
```
|
||||
家属 App 推送
|
||||
│ 30s 无 ack
|
||||
▼
|
||||
家属短信 + 语音呼叫
|
||||
│ 60s 无 ack
|
||||
▼
|
||||
备用联系人(邻居/物业/护理员)
|
||||
│ 90s 无 ack
|
||||
▼
|
||||
呼叫中心人工介入
|
||||
```
|
||||
|
||||
每户的联系人链、时段策略(白天找子女、夜间找同住者)、优先级都要可配。升级超时值需支持按户覆盖——独居老人和有同住家属的超时策略应该不同。
|
||||
|
||||
## 10.4 数据模型
|
||||
|
||||
```sql
|
||||
CREATE TABLE alerts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
household_id BIGINT NOT NULL REFERENCES households(id),
|
||||
device_id BIGINT REFERENCES devices(id),
|
||||
kind TEXT NOT NULL, -- 'fall' | 'no_motion' | 'device_offline_business'
|
||||
confidence REAL,
|
||||
triggered_at TIMESTAMPTZ NOT NULL,
|
||||
state TEXT NOT NULL, -- triggered/delivering/delivered/acked/handling/closed/suppressed
|
||||
acked_by BIGINT,
|
||||
acked_at TIMESTAMPTZ,
|
||||
closed_at TIMESTAMPTZ,
|
||||
outcome TEXT, -- true_positive / false_positive / unknown
|
||||
evidence_uri TEXT, -- 回捞的视频片段 + 姿态序列
|
||||
escalation_level INT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE contact_chains (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
household_id BIGINT NOT NULL REFERENCES households(id),
|
||||
level INT NOT NULL, -- 0,1,2,3
|
||||
contact_name TEXT NOT NULL,
|
||||
channels JSONB NOT NULL, -- [{type:'push',token:...},{type:'sms',no:...}]
|
||||
active_hours TEXT, -- '08:00-22:00' 或 NULL 表示全天
|
||||
timeout_sec INT NOT NULL DEFAULT 30,
|
||||
UNIQUE (household_id, level)
|
||||
);
|
||||
|
||||
CREATE TABLE alert_deliveries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
alert_id BIGINT NOT NULL REFERENCES alerts(id),
|
||||
level INT NOT NULL,
|
||||
channel TEXT NOT NULL, -- push / sms / voice / webhook
|
||||
provider TEXT NOT NULL, -- 便于多供应商冗余
|
||||
sent_at TIMESTAMPTZ,
|
||||
receipt_at TIMESTAMPTZ, -- 回执:NULL = 未确认送达
|
||||
failed_reason TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX ON alerts (state) WHERE state NOT IN ('closed','suppressed');
|
||||
```
|
||||
|
||||
关键设计:**投递是独立实体而非 alerts 的字段**。一次告警会产生多条投递记录(多级×多通道),每条各自有回执状态。
|
||||
|
||||
## 10.5 投递回执:三个不同的事实
|
||||
|
||||
| 事实 | 含义 | 字段 |
|
||||
| --- | --- | --- |
|
||||
| 发出去了 | 调用了上游 API | `sent_at` |
|
||||
| 送达了 | 上游回执确认到达终端 | `receipt_at` |
|
||||
| 被看到了 | 用户主动 ack | `alerts.acked_at` |
|
||||
|
||||
**没有回执就当失败并升级**,不要乐观假设。推送可能被系统杀掉、短信可能延迟、电话可能占线。
|
||||
|
||||
## 10.6 通道选型与冗余
|
||||
|
||||
**必须至少两条独立路径,且电话通道要能绕过互联网。** 推送依赖 APNs/FCM,短信依赖运营商,任何单一通道都可能整体故障。
|
||||
|
||||
| 层 | 选型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 状态机、升级链、ack、审计 | **自研**(Go) | 业务定制度高,通用工具都要大改 |
|
||||
| 多通道投递 | Novu / Apprise / ntfy,或直接对接云厂商 | 只解决「怎么发」 |
|
||||
| 短信 + 语音呼叫 | 云厂商(双供应商) | `provider` 字段就是为切换准备的 |
|
||||
|
||||
> ⚠️ **不要用 Grafana OnCall**。OSS 版已于 2026-03-24 归档,仓库只读,仅修 CVSS 7.0+ 漏洞。更关键的是移动推送、短信和电话在归档当日停止工作(那些通道依赖 Grafana Cloud 中转)。调度和升级链还能跑,但**送不出去的告警系统等于没有**。
|
||||
|
||||
若阶段二引入 ThingsBoard(接雷达时),其告警子系统 + 规则链可覆盖状态机的一大半,届时自研部分可收缩到只做升级链和电话对接。
|
||||
|
||||
## 10.7 去重与防抖
|
||||
|
||||
| 规则 | 参数 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 同设备冷却期 | 5min 内不重复告警 | 避免同一事件多次触发 |
|
||||
| 同家庭聚合 | 多设备同时触发合并为一条 | 客厅两个机位拍到同一次跌倒 |
|
||||
| 已处置抑制 | 已 ack 的告警期间不再新建 | 护理员已到现场 |
|
||||
| 维护窗口 | 住户主动静默(如家庭聚会) | **需限时且到期自动恢复** |
|
||||
|
||||
静默功能必须强制限时(建议最长 4h)并在到期前提醒。永久静默是这类系统最常见的事故成因。
|
||||
|
||||
## 10.8 误报反馈闭环(产品飞轮)
|
||||
|
||||
家属点「误报」这个动作,要自动把 `evidence_uri` 对应的视频片段和姿态序列推进标注队列。
|
||||
|
||||
这条链路直接解决 9.8 节的数据集问题:公开数据集全是演员摆拍,而**真实误报案例是你最稀缺的训练数据**。没有这个闭环,模型不会随运营时间变好。
|
||||
|
||||
同理,`outcome = true_positive` 的样本是正例金矿。**从 M3 第一天就要存,不要等到想训模型时才发现没数据。**
|
||||
|
||||
## 10.9 审计与合规
|
||||
|
||||
每条告警的完整生命周期需**不可篡改地**留存:
|
||||
|
||||
- 什么时间触发、基于什么证据
|
||||
- 向谁发过、走的什么通道、是否送达
|
||||
- 谁在什么时间确认、处置结果
|
||||
- 升级到第几级、每级耗时
|
||||
|
||||
一旦出事,「系统有没有报、几点报的、谁接的、多久响应」是法律问题而非技术问题。建议 `alerts` 和 `alert_deliveries` **只追加不修改**(状态变更写事件表),并独立备份。
|
||||
|
||||
另需确认的合规项(开工前法务确认):视频证据的保存期限、家属查看权限范围、被监护者本人的知情同意。
|
||||
|
||||
## 10.10 SLA 目标(待 M3 实测校准)
|
||||
|
||||
| 指标 | 目标 |
|
||||
| --- | --- |
|
||||
| 跌倒发生 → 首次投递 | < 10s |
|
||||
| 首次投递 → 送达回执 | < 5s(推送)/ < 30s(短信) |
|
||||
| 全链路无人响应 → 呼叫中心 | < 3min |
|
||||
| 告警丢失率 | 0(持久化后才确认触发) |
|
||||
|
||||
最后一行是硬约束:**告警先落库再投递**。进程崩溃后重启,未完成的升级链要能从数据库恢复继续——这跟第 6 节对账器的「水平触发」是同一个思路,启动时扫描所有未终态告警并接续处理。
|
||||
|
||||
# 11. EdgeX 升级路径(可选,阶段二)
|
||||
|
||||
当前阶段不引入 EdgeX。理由:它白送的能力(设备注册表、探活、命令抽象)约值一两周开发量,但代价是长期多背 4 个服务、跨服务追日志,且它假设设备在同一局域网、与 NAT 场景冲突。
|
||||
|
||||
## 何时值得切换
|
||||
|
||||
当确定要接入毫米波雷达、门磁、紧急按钮等异构设备时。EdgeX 的 device-mqtt / device-modbus / device-snmp 能复用同一套设备模型,这是它唯一压倒性的优势。
|
||||
|
||||
## 切换成本低的原因
|
||||
|
||||
ONVIF 调用那部分代码两边几乎一样(EdgeX 的 device-onvif-camera 内部就是用 IOTechSystems/onvif),对账器也不用改。
|
||||
|
||||
## 若切换,最小服务集
|
||||
|
||||
```
|
||||
core-metadata 设备注册表
|
||||
core-command 命令下发入口
|
||||
device-onvif-camera ONVIF <-> REST
|
||||
redis 消息总线
|
||||
(consul 可用 -cp=none 省掉)
|
||||
```
|
||||
|
||||
砍掉 core-data、app-service-*、security 全套。注意 discovery 在本场景基本不可用:netscan 对 200 个 /24 段是 5 万次探测,不现实;应使用预定义 deviceList 或 REST 批量录入,但保留 EnableStatusCheck / CheckStatusInterval 做探活。
|
||||
|
||||
另注意:EdgeX 3.x 起 API 路径是 /api/v3,网上大量 2.x 文档写的是 /api/v2。
|
||||
|
||||
# 12. 风险清单
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
| --- | --- | --- |
|
||||
| 摄像头 ONVIF 实现差异 | 部分型号无法接入 | 采购前用真机验证,见演示方案文档 |
|
||||
| 摄像头时钟漂移 | 事件时间戳全乱 | 开通时调 SetSystemDateAndTime,定期校时 |
|
||||
| NAT 隧道不稳 | 控制面失联 | 控制面失联不影响视频推流(已解耦);告警但不阻断 |
|
||||
| path reload 踢流 | 用户看到卡顿 | 配置变更前比对哈希;变更集中在维护窗口 |
|
||||
| H.265 兼容性 | 浏览器无法预览 | 摄像头统一设为 H.264 |
|
||||
| 隐私合规 | 项目无法落地 | 尽早引入雷达做一级触发,减少视频常态采集 |
|
||||
|
||||
# 13. 里程碑
|
||||
|
||||
| 阶段 | 内容 | 出口标准 |
|
||||
| --- | --- | --- |
|
||||
| M0 | 摄像头兼容性验证 | 3-5 个候选型号跑通 GetProfiles / GetStreamUri / SetSystemDateAndTime |
|
||||
| M1 | 单 exe 骨架 + mediamtx | 5 路摄像头自动建 path、探活、断线重建 |
|
||||
| M2 | 对账器 + Postgres + 隧道 | 10 户试点,含开通/停用全流程 |
|
||||
| M3 | 抽帧 worker + 推理接入 | 端到端跌倒告警跑通,拿到误报率基线 |
|
||||
| M4 | 分片 + 灰度扩容 | 250 路稳定运行 |
|
||||
| M5(阶段二) | 雷达接入 + 两级判定 | 误报率显著下降,视频常态采集下降 |
|
||||
Reference in New Issue
Block a user