730 lines
34 KiB
Markdown
730 lines
34 KiB
Markdown
# 🏗️ 跌倒检测视频接入系统 — 线上生产开发方案
|
||||
|
|
|
|||
|
|
> 来源: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(阶段二) | 雷达接入 + 两级判定 | 误报率显著下降,视频常态采集下降 |
|