|
|
@@ -34,11 +34,11 @@
|
|
|
|
|
|
采用领域驱动设计 (DDD) 分层:
|
|
|
|
|
|
-| 层 | 职责 |
|
|
|
-|---|------|
|
|
|
-| **adapter** | REST API 控制器,对外暴露 HTTP 接口 |
|
|
|
-| **application** | 业务编排服务,协调领域对象与基础设施 |
|
|
|
-| **domain** | 核心领域模型、枚举、Gateway 接口定义 |
|
|
|
+| 层 | 职责 |
|
|
|
+| ------------------ | --------------------------------------------------------- |
|
|
|
+| **adapter** | REST API 控制器,对外暴露 HTTP 接口 |
|
|
|
+| **application** | 业务编排服务,协调领域对象与基础设施 |
|
|
|
+| **domain** | 核心领域模型、枚举、Gateway 接口定义 |
|
|
|
| **infrastructure** | 外部集成实现:FFmpeg、Ably、ONVIF、ISAPI、RCS HTTP 客户端 |
|
|
|
|
|
|
---
|
|
|
@@ -117,6 +117,7 @@ const (
|
|
|
```
|
|
|
|
|
|
辅助方法:
|
|
|
+
|
|
|
- `IsActive()` — STARTING / STREAMING / RECONNECTING 返回 true
|
|
|
- `IsTerminal()` — STOPPED / ERROR 返回 true
|
|
|
|
|
|
@@ -229,12 +230,12 @@ LSS 通过 Ably WebSocket 接收 RCS 下发的指令,并通过同一通道返
|
|
|
|
|
|
#### Ably 通道列表
|
|
|
|
|
|
-| 通道名 | 方向 | 用途 |
|
|
|
-|--------|------|------|
|
|
|
-| `lss:registry` | LSS → RCS | Presence 注册/在线状态 |
|
|
|
-| `lss:{nodeId}:commands` | RCS → LSS | 接收指令 |
|
|
|
-| `lss:{nodeId}:status` | LSS → RCS | 发布状态和指令执行结果 |
|
|
|
-| `task:{taskId}:events` | LSS → RCS | 任务级别事件 |
|
|
|
+| 通道名 | 方向 | 用途 |
|
|
|
+| ----------------------- | --------- | ---------------------- |
|
|
|
+| `lss:registry` | LSS → RCS | Presence 注册/在线状态 |
|
|
|
+| `lss:{nodeId}:commands` | RCS → LSS | 接收指令 |
|
|
|
+| `lss:{nodeId}:status` | LSS → RCS | 发布状态和指令执行结果 |
|
|
|
+| `task:{taskId}:events` | LSS → RCS | 任务级别事件 |
|
|
|
|
|
|
#### 指令消息格式
|
|
|
|
|
|
@@ -271,6 +272,7 @@ LSS 通过 Ably WebSocket 接收 RCS 下发的指令,并通过同一通道返
|
|
|
```
|
|
|
|
|
|
其他推流指令:
|
|
|
+
|
|
|
- `STOP_STREAM` — payload: `{ "taskId": "task_001", "force": false }`
|
|
|
- `QUERY_STATUS` — payload: `{ "taskId": "task_001" }`
|
|
|
|
|
|
@@ -288,6 +290,7 @@ LSS 通过 Ably WebSocket 接收 RCS 下发的指令,并通过同一通道返
|
|
|
```
|
|
|
|
|
|
PTZ 指令列表:
|
|
|
+
|
|
|
- `PTZ_CONTROL` — payload: `{ cameraId, action, speed }`
|
|
|
- `PTZ_STOP` — payload: `{ cameraId }`
|
|
|
- `PTZ_PRESET_GOTO` — payload: `{ cameraId, presetId }`
|
|
|
@@ -310,6 +313,7 @@ PTZ 指令列表:
|
|
|
```
|
|
|
|
|
|
摄像头指令列表:
|
|
|
+
|
|
|
- `CAMERA_CHECK` — 连通性测试
|
|
|
- `CAMERA_SNAPSHOT` — 抓取截图,返回 Base64 编码的 JPEG
|
|
|
- `CAMERA_PRESET_LIST` — 获取预置位列表
|
|
|
@@ -358,6 +362,7 @@ PTZ 指令列表:
|
|
|
```
|
|
|
|
|
|
巡航指令列表:
|
|
|
+
|
|
|
- `PATROL_START` — 启动巡航
|
|
|
- `PATROL_STOP` — 停止巡航
|
|
|
- `PATROL_PAUSE` — 暂停巡航
|
|
|
@@ -411,15 +416,15 @@ LSS 启动后加入 `lss:registry` 通道的 Presence,数据包含节点基本
|
|
|
|
|
|
基础路径:`http://{nodeIP}:10060/api`
|
|
|
|
|
|
-| 方法 | 路径 | 说明 |
|
|
|
-|------|------|------|
|
|
|
-| POST | `/stream/start` | 启动推流任务 |
|
|
|
-| POST | `/stream/stop` | 停止推流任务 |
|
|
|
-| GET | `/stream/task/{taskId}` | 查询单个任务状态 |
|
|
|
-| GET | `/stream/tasks/active` | 查询所有活跃任务 |
|
|
|
-| GET | `/stream/tasks` | 查询所有任务 |
|
|
|
-| GET | `/stream/node` | 获取节点信息 |
|
|
|
-| GET | `/stream/health` | 健康检查 |
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
+| ---- | ----------------------- | ---------------- |
|
|
|
+| POST | `/stream/start` | 启动推流任务 |
|
|
|
+| POST | `/stream/stop` | 停止推流任务 |
|
|
|
+| GET | `/stream/task/{taskId}` | 查询单个任务状态 |
|
|
|
+| GET | `/stream/tasks/active` | 查询所有活跃任务 |
|
|
|
+| GET | `/stream/tasks` | 查询所有任务 |
|
|
|
+| GET | `/stream/node` | 获取节点信息 |
|
|
|
+| GET | `/stream/health` | 健康检查 |
|
|
|
|
|
|
**POST /stream/start 请求体:**
|
|
|
|
|
|
@@ -459,11 +464,11 @@ LSS 启动后加入 `lss:registry` 通道的 Presence,数据包含节点基本
|
|
|
|
|
|
#### 4.2.2 LSS 调用 RCS 的 HTTP 接口
|
|
|
|
|
|
-| 方法 | 路径 | 说明 |
|
|
|
-|------|------|------|
|
|
|
-| POST | `{RCS_BASE_URL}/lss/register` | 节点注册 |
|
|
|
-| POST | `{RCS_BASE_URL}/lss/heartbeat` | HTTP 心跳(5 秒间隔) |
|
|
|
-| POST | `{RCS_BASE_URL}/lss/task/callback` | 任务状态回调 |
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
+| ---- | ---------------------------------- | --------------------- |
|
|
|
+| POST | `{RCS_BASE_URL}/lss/register` | 节点注册 |
|
|
|
+| POST | `{RCS_BASE_URL}/lss/heartbeat` | HTTP 心跳(5 秒间隔) |
|
|
|
+| POST | `{RCS_BASE_URL}/lss/task/callback` | 任务状态回调 |
|
|
|
|
|
|
**POST /lss/register 请求体:**
|
|
|
|
|
|
@@ -570,25 +575,25 @@ SOAP Header 示例:
|
|
|
|
|
|
**设备服务** — `http://{ip}:{port}/onvif/device_service`
|
|
|
|
|
|
-| 操作 | 用途 | 返回值 |
|
|
|
-|------|------|--------|
|
|
|
+| 操作 | 用途 | 返回值 |
|
|
|
+| ---------------------- | ------------ | -------------------------------------------------------------- |
|
|
|
| `GetDeviceInformation` | 获取设备信息 | manufacturer, model, firmwareVersion, serialNumber, hardwareId |
|
|
|
-| `GetCapabilities` | 获取设备能力 | media/PTZ/device 服务地址 |
|
|
|
+| `GetCapabilities` | 获取设备能力 | media/PTZ/device 服务地址 |
|
|
|
|
|
|
**媒体服务** — `http://{ip}:{port}/onvif/media_service`
|
|
|
|
|
|
-| 操作 | 用途 | 返回值 |
|
|
|
-|------|------|--------|
|
|
|
+| 操作 | 用途 | 返回值 |
|
|
|
+| ------------- | --------------------- | ----------------------------- |
|
|
|
| `GetProfiles` | 获取媒体 Profile 列表 | Profile Token(PTZ 操作必需) |
|
|
|
|
|
|
**PTZ 服务** — `http://{ip}:{port}/onvif/ptz_service`
|
|
|
|
|
|
-| 操作 | 参数 | 用途 |
|
|
|
-|------|------|------|
|
|
|
-| `ContinuousMove` | profileToken, panSpeed(-1.0~1.0), tiltSpeed(-1.0~1.0), zoomSpeed(-1.0~1.0) | 持续移动 |
|
|
|
-| `Stop` | profileToken | 停止移动 |
|
|
|
-| `GotoPreset` | profileToken, presetToken | 转到预置位 |
|
|
|
-| `SetPreset` | profileToken, presetName | 设置预置位 |
|
|
|
+| 操作 | 参数 | 用途 |
|
|
|
+| ---------------- | -------------------------------------------------------------------------- | ---------- |
|
|
|
+| `ContinuousMove` | profileToken, panSpeed(-1.0~1.0), tiltSpeed(-1.0~1.0), zoomSpeed(-1.0~1.0) | 持续移动 |
|
|
|
+| `Stop` | profileToken | 停止移动 |
|
|
|
+| `GotoPreset` | profileToken, presetToken | 转到预置位 |
|
|
|
+| `SetPreset` | profileToken, presetName | 设置预置位 |
|
|
|
|
|
|
**ContinuousMove SOAP Body 示例:**
|
|
|
|
|
|
@@ -618,18 +623,18 @@ ONVIF 每次 PTZ 操作都需要 Profile Token。首次调用时通过 `GetProfi
|
|
|
|
|
|
#### ISAPI 端点列表
|
|
|
|
|
|
-| 方法 | 路径 | 用途 |
|
|
|
-|------|------|------|
|
|
|
-| PUT | `/ISAPI/PTZCtrl/channels/{ch}/continuous` | 持续云台移动 |
|
|
|
-| PUT | `/ISAPI/PTZCtrl/channels/{ch}/momentary` | 瞬时云台移动 |
|
|
|
-| PUT | `/ISAPI/PTZCtrl/channels/{ch}/presets/{id}/goto` | 跳转到预置位 |
|
|
|
-| PUT | `/ISAPI/PTZCtrl/channels/{ch}/presets/{id}` | 设置预置位 |
|
|
|
-| DELETE | `/ISAPI/PTZCtrl/channels/{ch}/presets/{id}` | 删除预置位 |
|
|
|
-| GET | `/ISAPI/PTZCtrl/channels/{ch}/presets` | 获取预置位列表 |
|
|
|
-| PUT | `/ISAPI/PTZCtrl/channels/{ch}/patrols/{id}/start` | 启动巡航 |
|
|
|
-| PUT | `/ISAPI/PTZCtrl/channels/{ch}/patrols/{id}/stop` | 停止巡航 |
|
|
|
-| GET | `/ISAPI/System/deviceInfo` | 获取设备信息 |
|
|
|
-| GET | `/ISAPI/Streaming/channels/{ch}/picture` | 抓取截图 (JPEG) |
|
|
|
+| 方法 | 路径 | 用途 |
|
|
|
+| ------ | ------------------------------------------------- | --------------- |
|
|
|
+| PUT | `/ISAPI/PTZCtrl/channels/{ch}/continuous` | 持续云台移动 |
|
|
|
+| PUT | `/ISAPI/PTZCtrl/channels/{ch}/momentary` | 瞬时云台移动 |
|
|
|
+| PUT | `/ISAPI/PTZCtrl/channels/{ch}/presets/{id}/goto` | 跳转到预置位 |
|
|
|
+| PUT | `/ISAPI/PTZCtrl/channels/{ch}/presets/{id}` | 设置预置位 |
|
|
|
+| DELETE | `/ISAPI/PTZCtrl/channels/{ch}/presets/{id}` | 删除预置位 |
|
|
|
+| GET | `/ISAPI/PTZCtrl/channels/{ch}/presets` | 获取预置位列表 |
|
|
|
+| PUT | `/ISAPI/PTZCtrl/channels/{ch}/patrols/{id}/start` | 启动巡航 |
|
|
|
+| PUT | `/ISAPI/PTZCtrl/channels/{ch}/patrols/{id}/stop` | 停止巡航 |
|
|
|
+| GET | `/ISAPI/System/deviceInfo` | 获取设备信息 |
|
|
|
+| GET | `/ISAPI/Streaming/channels/{ch}/picture` | 抓取截图 (JPEG) |
|
|
|
|
|
|
`{ch}` 默认为 `1`(通道号)。
|
|
|
|
|
|
@@ -757,22 +762,22 @@ const (
|
|
|
|
|
|
#### 5.2.3 速度映射
|
|
|
|
|
|
-| 参数 | 海康 ISAPI | ONVIF |
|
|
|
-|------|-----------|-------|
|
|
|
+| 参数 | 海康 ISAPI | ONVIF |
|
|
|
+| -------- | ------------------ | -------------------- |
|
|
|
| 方向速度 | -100 ~ 100(整数) | -1.0 ~ 1.0(浮点数) |
|
|
|
-| 转换公式 | 直接使用 | `speed / 100.0` |
|
|
|
+| 转换公式 | 直接使用 | `speed / 100.0` |
|
|
|
|
|
|
Action 到方向的映射:
|
|
|
|
|
|
-| Action | pan | tilt | zoom |
|
|
|
-|--------|-----|------|------|
|
|
|
-| PAN_LEFT | -speed | 0 | 0 |
|
|
|
-| PAN_RIGHT | +speed | 0 | 0 |
|
|
|
-| TILT_UP | 0 | +speed | 0 |
|
|
|
-| TILT_DOWN | 0 | -speed | 0 |
|
|
|
-| ZOOM_IN | 0 | 0 | +speed |
|
|
|
-| ZOOM_OUT | 0 | 0 | -speed |
|
|
|
-| STOP | 0 | 0 | 0 |
|
|
|
+| Action | pan | tilt | zoom |
|
|
|
+| --------- | ------ | ------ | ------ |
|
|
|
+| PAN_LEFT | -speed | 0 | 0 |
|
|
|
+| PAN_RIGHT | +speed | 0 | 0 |
|
|
|
+| TILT_UP | 0 | +speed | 0 |
|
|
|
+| TILT_DOWN | 0 | -speed | 0 |
|
|
|
+| ZOOM_IN | 0 | 0 | +speed |
|
|
|
+| ZOOM_OUT | 0 | 0 | -speed |
|
|
|
+| STOP | 0 | 0 | 0 |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -844,6 +849,7 @@ const (
|
|
|
#### 5.4.2 设备探测 (Probe)
|
|
|
|
|
|
探测流程:
|
|
|
+
|
|
|
1. 尝试 ONVIF `GetDeviceInformation` + `GetCapabilities` + `GetProfiles`
|
|
|
2. 如果 ONVIF 失败,尝试海康 ISAPI `GET /ISAPI/System/deviceInfo`
|
|
|
3. 返回设备信息和能力集
|
|
|
@@ -902,12 +908,12 @@ type PTZCapabilities struct {
|
|
|
|
|
|
处理 Ably `camera_command` 事件:
|
|
|
|
|
|
-| 指令 | 处理逻辑 |
|
|
|
-|------|----------|
|
|
|
-| `CAMERA_CHECK` | HTTP GET 设备信息接口,成功则在线 |
|
|
|
-| `CAMERA_SNAPSHOT` | GET `/ISAPI/Streaming/channels/1/picture`,如需要则缩放图片,Base64 编码后返回 |
|
|
|
-| `CAMERA_PRESET_LIST` | GET `/ISAPI/PTZCtrl/channels/1/presets`,解析 XML 返回 |
|
|
|
-| `CAMERA_DEVICE_INFO` | 调用探测服务获取完整设备信息 |
|
|
|
+| 指令 | 处理逻辑 |
|
|
|
+| -------------------- | ------------------------------------------------------------------------------ |
|
|
|
+| `CAMERA_CHECK` | HTTP GET 设备信息接口,成功则在线 |
|
|
|
+| `CAMERA_SNAPSHOT` | GET `/ISAPI/Streaming/channels/1/picture`,如需要则缩放图片,Base64 编码后返回 |
|
|
|
+| `CAMERA_PRESET_LIST` | GET `/ISAPI/PTZCtrl/channels/1/presets`,解析 XML 返回 |
|
|
|
+| `CAMERA_DEVICE_INFO` | 调用探测服务获取完整设备信息 |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -931,10 +937,10 @@ type PTZCapabilities struct {
|
|
|
|
|
|
### 6.2 心跳机制(双通道)
|
|
|
|
|
|
-| 通道 | 间隔 | 传输方式 | 内容 |
|
|
|
-|------|------|---------|------|
|
|
|
-| HTTP | 5 秒 | POST /lss/heartbeat | nodeId, activeTaskCount, cpuUsage, memoryUsage, cameras |
|
|
|
-| Ably | 10 秒 | Ably Message | 同上 + activeTasks 详情 |
|
|
|
+| 通道 | 间隔 | 传输方式 | 内容 |
|
|
|
+| ---- | ----- | ------------------- | ------------------------------------------------------- |
|
|
|
+| HTTP | 5 秒 | POST /lss/heartbeat | nodeId, activeTaskCount, cpuUsage, memoryUsage, cameras |
|
|
|
+| Ably | 10 秒 | Ably Message | 同上 + activeTasks 详情 |
|
|
|
|
|
|
### 6.3 关闭流程
|
|
|
|
|
|
@@ -1019,46 +1025,46 @@ const (
|
|
|
|
|
|
```yaml
|
|
|
server:
|
|
|
- port: 10060 # HTTP 服务端口
|
|
|
+ port: 10060 # HTTP 服务端口
|
|
|
|
|
|
ably:
|
|
|
- api_key: "" # Ably API Key(必填)
|
|
|
- client_id: "" # 自动生成: lss-{machineId}
|
|
|
- heartbeat_interval: 10s # Ably 心跳间隔
|
|
|
+ api_key: '' # Ably API Key(必填)
|
|
|
+ client_id: '' # 自动生成: lss-{machineId}
|
|
|
+ heartbeat_interval: 10s # Ably 心跳间隔
|
|
|
|
|
|
lss:
|
|
|
node:
|
|
|
- name: "LSS-Node-1" # 节点名称
|
|
|
- machine_id: "" # 机器标识(必填)
|
|
|
- max_tasks: 4 # 最大并发推流数
|
|
|
+ name: 'LSS-Node-1' # 节点名称
|
|
|
+ machine_id: '' # 机器标识(必填)
|
|
|
+ max_tasks: 4 # 最大并发推流数
|
|
|
|
|
|
ffmpeg:
|
|
|
- path: "/usr/local/ffmpeg-whip/bin/ffmpeg" # FFmpeg 路径
|
|
|
- stop_timeout: 10s # 停止超时
|
|
|
+ path: '/usr/local/ffmpeg-whip/bin/ffmpeg' # FFmpeg 路径
|
|
|
+ stop_timeout: 10s # 停止超时
|
|
|
|
|
|
rcs:
|
|
|
- base_url: "" # RCS 服务地址(必填)
|
|
|
- connect_timeout: 10s # HTTP 连接超时
|
|
|
- read_timeout: 30s # HTTP 读取超时
|
|
|
- heartbeat_interval: 5s # HTTP 心跳间隔
|
|
|
+ base_url: '' # RCS 服务地址(必填)
|
|
|
+ connect_timeout: 10s # HTTP 连接超时
|
|
|
+ read_timeout: 30s # HTTP 读取超时
|
|
|
+ heartbeat_interval: 5s # HTTP 心跳间隔
|
|
|
|
|
|
camera:
|
|
|
- scan_enabled: true # 是否启用摄像头扫描
|
|
|
- scan_subnet: "192.168.0.0/24" # 扫描子网
|
|
|
- scan_timeout: 30s # 扫描超时
|
|
|
- rescan_interval: 60s # 重扫描间隔
|
|
|
+ scan_enabled: true # 是否启用摄像头扫描
|
|
|
+ scan_subnet: '192.168.0.0/24' # 扫描子网
|
|
|
+ scan_timeout: 30s # 扫描超时
|
|
|
+ rescan_interval: 60s # 重扫描间隔
|
|
|
```
|
|
|
|
|
|
环境变量:
|
|
|
|
|
|
-| 变量 | 必填 | 说明 |
|
|
|
-|------|------|------|
|
|
|
-| `ABLY_API_KEY` | 是 | Ably API Key |
|
|
|
-| `LSS_MACHINE_ID` | 是 | 机器标识 |
|
|
|
-| `RCS_BASE_URL` | 是 | RCS 服务地址 |
|
|
|
-| `LSS_NODE_NAME` | 否 | 节点名称,默认 "LSS-Node-1" |
|
|
|
-| `LSS_MAX_TASKS` | 否 | 最大并发数,默认 4 |
|
|
|
-| `FFMPEG_PATH` | 否 | FFmpeg 路径 |
|
|
|
+| 变量 | 必填 | 说明 |
|
|
|
+| ---------------- | ---- | --------------------------- |
|
|
|
+| `ABLY_API_KEY` | 是 | Ably API Key |
|
|
|
+| `LSS_MACHINE_ID` | 是 | 机器标识 |
|
|
|
+| `RCS_BASE_URL` | 是 | RCS 服务地址 |
|
|
|
+| `LSS_NODE_NAME` | 否 | 节点名称,默认 "LSS-Node-1" |
|
|
|
+| `LSS_MAX_TASKS` | 否 | 最大并发数,默认 4 |
|
|
|
+| `FFMPEG_PATH` | 否 | FFmpeg 路径 |
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -1074,6 +1080,7 @@ lss:
|
|
|
### 9.2 Ably SDK
|
|
|
|
|
|
Go 实现需使用 [Ably Go SDK](https://github.com/ably/ably-go),功能包括:
|
|
|
+
|
|
|
- Realtime 连接
|
|
|
- Channel 订阅(subscribe message)
|
|
|
- Presence enter/leave
|
|
|
@@ -1082,6 +1089,7 @@ Go 实现需使用 [Ably Go SDK](https://github.com/ably/ably-go),功能包括
|
|
|
### 9.3 ONVIF 实现
|
|
|
|
|
|
不需要完整的 ONVIF SDK,自行构建 SOAP XML 请求即可:
|
|
|
+
|
|
|
- 使用 `net/http` 发送 POST 请求
|
|
|
- 手动拼装 SOAP Envelope(参见第 4.3 节)
|
|
|
- 使用正则或 `encoding/xml` 解析 XML 响应
|
|
|
@@ -1101,6 +1109,7 @@ Go 实现需使用 [Ably Go SDK](https://github.com/ably/ably-go),功能包括
|
|
|
### 9.6 日志
|
|
|
|
|
|
使用结构化日志(推荐 `slog` 或 `zerolog`),关键操作需记录:
|
|
|
+
|
|
|
- 推流任务启动/停止/错误
|
|
|
- PTZ 控制指令
|
|
|
- Ably 连接状态变化
|