|
|
@@ -0,0 +1,1152 @@
|
|
|
+# TG Live LSS — Go 重写需求文档
|
|
|
+
|
|
|
+> LSS (Local Sender Service) 是一个部署在本地网络的边缘流媒体代理服务,负责通过 FFmpeg 将 IP 摄像头的 RTSP 流推送到 Cloudflare WHIP 端点,同时提供摄像头 PTZ 云台控制能力。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、系统架构总览
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────┐
|
|
|
+│ RCS (Remote Camera Service) - 云端 │
|
|
|
+│ - 中央管理服务 │
|
|
|
+│ - 通过 Ably 下发指令 │
|
|
|
+│ - 通过 HTTP 接收心跳/回调 │
|
|
|
+└──────────────┬──────────────────────────┘
|
|
|
+ │ Ably Realtime (WebSocket)
|
|
|
+ │ Channel: lss:{nodeId}:commands
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────┐
|
|
|
+│ LSS (Local Sender Service) - 本地边缘 │
|
|
|
+│ - FFmpeg 进程管理 (RTSP → WHIP) │
|
|
|
+│ - 摄像头 PTZ 控制 (ONVIF / ISAPI) │
|
|
|
+│ - 摄像头发现与探测 │
|
|
|
+│ - 巡航路线执行 │
|
|
|
+└──────────────┬──────────────────────────┘
|
|
|
+ │ FFmpeg -f whip
|
|
|
+ ▼
|
|
|
+┌─────────────────────────────────────────┐
|
|
|
+│ Cloudflare Stream (WebRTC) │
|
|
|
+└─────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 架构分层
|
|
|
+
|
|
|
+采用领域驱动设计 (DDD) 分层:
|
|
|
+
|
|
|
+| 层 | 职责 |
|
|
|
+|---|------|
|
|
|
+| **adapter** | REST API 控制器,对外暴露 HTTP 接口 |
|
|
|
+| **application** | 业务编排服务,协调领域对象与基础设施 |
|
|
|
+| **domain** | 核心领域模型、枚举、Gateway 接口定义 |
|
|
|
+| **infrastructure** | 外部集成实现:FFmpeg、Ably、ONVIF、ISAPI、RCS HTTP 客户端 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、核心领域模型
|
|
|
+
|
|
|
+### 2.1 StreamTask(推流任务)
|
|
|
+
|
|
|
+```go
|
|
|
+type StreamTask struct {
|
|
|
+ StreamSn string // 任务唯一标识(由 RCS 分配)
|
|
|
+ CameraID string // 摄像头标识
|
|
|
+ InputType InputType // 输入类型:RTSP / FILE / SRT / RTMP
|
|
|
+ SourceURL string // 输入源地址,如 rtsp://admin:pass@192.168.0.64/...
|
|
|
+ Loop bool // FILE 模式下是否循环播放
|
|
|
+ WhipURL string // Cloudflare WHIP 推流地址
|
|
|
+ PlaybackURL string // WHEP 播放地址
|
|
|
+ Status StreamStatus // 任务状态
|
|
|
+ ProcessID int64 // FFmpeg 进程 PID
|
|
|
+ ErrorMessage string // 错误信息
|
|
|
+ RetryCount int // 已重试次数,默认 0
|
|
|
+ MaxRetries int // 最大重试次数,默认 3
|
|
|
+ CreatedAt time.Time
|
|
|
+ StartedAt time.Time
|
|
|
+ StoppedAt time.Time
|
|
|
+ LastHeartbeat time.Time
|
|
|
+ VideoConfig VideoConfig // 视频编码配置
|
|
|
+ AudioConfig AudioConfig // 音频编码配置
|
|
|
+ FFmpegCommand string // RCS 预构建的完整 FFmpeg 命令(优先使用)
|
|
|
+}
|
|
|
+
|
|
|
+type VideoConfig struct {
|
|
|
+ Codec string // 默认 "libx264"
|
|
|
+ Bitrate string // 默认 "2500k"
|
|
|
+ FPS int // 默认 30
|
|
|
+ Width int // 可选,不设则不缩放
|
|
|
+ Height int // 可选
|
|
|
+ Preset string // 默认 "veryfast"
|
|
|
+ KeyframeInterval int // 默认 30 (等于 FPS)
|
|
|
+}
|
|
|
+
|
|
|
+type AudioConfig struct {
|
|
|
+ Codec string // 默认 "opus"
|
|
|
+ Bitrate string // 默认 "128k"
|
|
|
+ SampleRate int // 默认 48000
|
|
|
+ Channels int // 默认 2
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 InputType(输入类型枚举)
|
|
|
+
|
|
|
+```go
|
|
|
+type InputType string
|
|
|
+
|
|
|
+const (
|
|
|
+ InputRTSP InputType = "RTSP"
|
|
|
+ InputFILE InputType = "FILE"
|
|
|
+ InputSRT InputType = "SRT"
|
|
|
+ InputRTMP InputType = "RTMP"
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 StreamStatus(任务状态枚举)
|
|
|
+
|
|
|
+```go
|
|
|
+type StreamStatus string
|
|
|
+
|
|
|
+const (
|
|
|
+ StatusIdle StreamStatus = "IDLE" // 空闲
|
|
|
+ StatusStarting StreamStatus = "STARTING" // 启动中
|
|
|
+ StatusStreaming StreamStatus = "STREAMING" // 推流中
|
|
|
+ StatusReconnecting StreamStatus = "RECONNECTING" // 重连中
|
|
|
+ StatusStopped StreamStatus = "STOPPED" // 已停止
|
|
|
+ StatusError StreamStatus = "ERROR" // 错误
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+辅助方法:
|
|
|
+- `IsActive()` — STARTING / STREAMING / RECONNECTING 返回 true
|
|
|
+- `IsTerminal()` — STOPPED / ERROR 返回 true
|
|
|
+
|
|
|
+### 2.4 LssNode(节点信息)
|
|
|
+
|
|
|
+```go
|
|
|
+type LssNode struct {
|
|
|
+ NodeID string // 格式: "lss-{machineId}"
|
|
|
+ NodeName string // 人类可读名称
|
|
|
+ MachineID string // 物理机器标识
|
|
|
+ AblyClientID string // 与 NodeID 相同
|
|
|
+ NodeIP string // 节点 IP
|
|
|
+ NodePort int // 服务端口,默认 10060
|
|
|
+ Status NodeStatus // ONLINE / OFFLINE / BUSY / MAINTENANCE
|
|
|
+ ActiveTaskCount int // 当前活跃推流任务数
|
|
|
+ MaxTasks int // 最大并发任务数,默认 4
|
|
|
+ RegisteredAt time.Time
|
|
|
+ LastHeartbeat time.Time
|
|
|
+ FFmpegPath string // 默认 "/usr/local/ffmpeg-whip/bin/ffmpeg"
|
|
|
+ SystemInfo SystemInfo
|
|
|
+ Cameras []Camera // 已发现的摄像头列表
|
|
|
+}
|
|
|
+
|
|
|
+type SystemInfo struct {
|
|
|
+ OS string
|
|
|
+ CPUCores int
|
|
|
+ TotalMemory int64 // 字节
|
|
|
+ AvailableMemory int64
|
|
|
+ FFmpegVersion string
|
|
|
+}
|
|
|
+
|
|
|
+type NodeStatus string
|
|
|
+
|
|
|
+const (
|
|
|
+ NodeOnline NodeStatus = "ONLINE"
|
|
|
+ NodeOffline NodeStatus = "OFFLINE"
|
|
|
+ NodeBusy NodeStatus = "BUSY"
|
|
|
+ NodeMaintenance NodeStatus = "MAINTENANCE"
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+### 2.5 Camera(摄像头信息)
|
|
|
+
|
|
|
+```go
|
|
|
+type Camera struct {
|
|
|
+ IP string // 摄像头 IP
|
|
|
+ Port int // 端口
|
|
|
+ ONVIF bool // 是否支持 ONVIF
|
|
|
+ RTSPURL string // RTSP 流地址
|
|
|
+ Vendor string // "HIKVISION" / "DAHUA" / "UNKNOWN"
|
|
|
+ Model string // 型号
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、Gateway 接口定义
|
|
|
+
|
|
|
+Go 实现需要实现以下接口:
|
|
|
+
|
|
|
+### 3.1 FFmpegGateway
|
|
|
+
|
|
|
+```go
|
|
|
+type FFmpegGateway interface {
|
|
|
+ StartProcess(task *StreamTask) (pid int64, err error)
|
|
|
+ StopProcess(processID int64) error
|
|
|
+ IsAlive(processID int64) bool
|
|
|
+ KillProcess(processID int64)
|
|
|
+ GetProcessLog(processID int64) string
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 RcsGateway
|
|
|
+
|
|
|
+```go
|
|
|
+type RcsGateway interface {
|
|
|
+ Register(node *LssNode) error
|
|
|
+ HeartbeatWithCameras(node *LssNode) error
|
|
|
+ TaskStartedCallback(taskID string, processID int64, hlsPlaybackURL string) error
|
|
|
+ TaskStoppedCallback(taskID string) error
|
|
|
+ TaskErrorCallback(taskID string, errorMessage string) error
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.3 CameraScannerGateway
|
|
|
+
|
|
|
+```go
|
|
|
+type CameraScannerGateway interface {
|
|
|
+ ScanCameras(subnet string) ([]Camera, error)
|
|
|
+ ScanLocalNetwork() ([]Camera, error)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 3.4 CameraDeviceAdapter
|
|
|
+
|
|
|
+```go
|
|
|
+type CameraDeviceAdapter interface {
|
|
|
+ ProbeDeviceInfo(cmd *ProbeCameraCommand) (*CameraDeviceInfo, error)
|
|
|
+ ExecutePTZControl(cmd *PTZControlCommand, ip string, port int, username, password string) error
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、通信协议详细说明
|
|
|
+
|
|
|
+### 4.1 Ably Realtime 消息(核心指令通道)
|
|
|
+
|
|
|
+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 | 任务级别事件 |
|
|
|
+
|
|
|
+#### 指令消息格式
|
|
|
+
|
|
|
+所有 Ably 消息使用 JSON 格式,通过 `Message.name` 区分事件类型:
|
|
|
+
|
|
|
+**事件类型 `command`(推流指令):**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "command": "START_STREAM",
|
|
|
+ "payload": {
|
|
|
+ "taskId": "task_001",
|
|
|
+ "cameraId": "cam_001",
|
|
|
+ "sourceUrl": "rtsp://admin:pass@192.168.0.64/Streaming/Channels/101",
|
|
|
+ "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/whip",
|
|
|
+ "playbackUrl": "https://customer-xxx.cloudflarestream.com/xxx/whep",
|
|
|
+ "inputType": "RTSP",
|
|
|
+ "loop": false,
|
|
|
+ "ffmpegCommand": "完整 FFmpeg 命令字符串(可选,优先使用)",
|
|
|
+ "videoConfig": {
|
|
|
+ "codec": "libx264",
|
|
|
+ "bitrate": "2500k",
|
|
|
+ "fps": 30,
|
|
|
+ "preset": "veryfast"
|
|
|
+ },
|
|
|
+ "audioConfig": {
|
|
|
+ "codec": "opus",
|
|
|
+ "bitrate": "128k",
|
|
|
+ "sampleRate": 48000,
|
|
|
+ "channels": 2
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+其他推流指令:
|
|
|
+- `STOP_STREAM` — payload: `{ "taskId": "task_001", "force": false }`
|
|
|
+- `QUERY_STATUS` — payload: `{ "taskId": "task_001" }`
|
|
|
+
|
|
|
+**事件类型 `ptz_command`(PTZ 云台指令):**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "command": "PTZ_CONTROL",
|
|
|
+ "payload": {
|
|
|
+ "cameraId": "cam_001",
|
|
|
+ "action": "PAN_LEFT",
|
|
|
+ "speed": 50
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+PTZ 指令列表:
|
|
|
+- `PTZ_CONTROL` — payload: `{ cameraId, action, speed }`
|
|
|
+- `PTZ_STOP` — payload: `{ cameraId }`
|
|
|
+- `PTZ_PRESET_GOTO` — payload: `{ cameraId, presetId }`
|
|
|
+- `PTZ_PRESET_SET` — payload: `{ cameraId, presetId, presetName }`
|
|
|
+- `PTZ_CONTINUOUS_MOVE` — payload: `{ cameraId, action, speed, duration }`
|
|
|
+
|
|
|
+**事件类型 `camera_command`(摄像头指令):**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "command": "CAMERA_CHECK",
|
|
|
+ "payload": {
|
|
|
+ "cameraId": "cam_001",
|
|
|
+ "ip": "192.168.0.64",
|
|
|
+ "port": 80,
|
|
|
+ "username": "admin",
|
|
|
+ "password": "xxx"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+摄像头指令列表:
|
|
|
+- `CAMERA_CHECK` — 连通性测试
|
|
|
+- `CAMERA_SNAPSHOT` — 抓取截图,返回 Base64 编码的 JPEG
|
|
|
+- `CAMERA_PRESET_LIST` — 获取预置位列表
|
|
|
+- `CAMERA_DEVICE_INFO` — 获取设备信息(厂商、型号、序列号)
|
|
|
+
|
|
|
+**事件类型 `onvif_command`(ONVIF 指令):**
|
|
|
+
|
|
|
+- `ONVIF_PROBE` — 探测 ONVIF 设备能力
|
|
|
+
|
|
|
+**事件类型 `patrol_command`(巡航指令):**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "command": "PATROL_START",
|
|
|
+ "payload": {
|
|
|
+ "cameraId": "cam_001",
|
|
|
+ "tourId": "tour_001",
|
|
|
+ "ip": "192.168.0.64",
|
|
|
+ "port": 80,
|
|
|
+ "username": "admin",
|
|
|
+ "password": "xxx",
|
|
|
+ "vendorType": "HIKVISION",
|
|
|
+ "loopCount": 0,
|
|
|
+ "waypoints": [
|
|
|
+ {
|
|
|
+ "action": "LEFT",
|
|
|
+ "speed": 50,
|
|
|
+ "duration": 3000,
|
|
|
+ "startTime": 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "action": "STOP",
|
|
|
+ "speed": 0,
|
|
|
+ "duration": 2000,
|
|
|
+ "startTime": 3000
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "action": "RIGHT",
|
|
|
+ "speed": 50,
|
|
|
+ "duration": 3000,
|
|
|
+ "startTime": 5000
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+巡航指令列表:
|
|
|
+- `PATROL_START` — 启动巡航
|
|
|
+- `PATROL_STOP` — 停止巡航
|
|
|
+- `PATROL_PAUSE` — 暂停巡航
|
|
|
+- `PATROL_RESUME` — 恢复巡航
|
|
|
+- `PATROL_STATUS` — 查询巡航状态
|
|
|
+
|
|
|
+#### 指令执行结果回复
|
|
|
+
|
|
|
+LSS 执行完指令后,通过 `lss:{nodeId}:status` 通道发布结果:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "COMMAND_RESULT",
|
|
|
+ "command": "START_STREAM",
|
|
|
+ "taskId": "task_001",
|
|
|
+ "success": true,
|
|
|
+ "data": { ... },
|
|
|
+ "errorMessage": null,
|
|
|
+ "timestamp": "2026-01-30T10:00:00Z"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Ably 心跳(10 秒间隔)
|
|
|
+
|
|
|
+LSS 每 10 秒通过 Ably 发送心跳消息到 `lss:{nodeId}:status`:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "type": "HEARTBEAT",
|
|
|
+ "nodeId": "lss-machine_007",
|
|
|
+ "status": "ONLINE",
|
|
|
+ "activeTaskCount": 2,
|
|
|
+ "maxTasks": 4,
|
|
|
+ "activeTasks": {
|
|
|
+ "task_001": 1706601600000,
|
|
|
+ "task_002": 1706601650000
|
|
|
+ },
|
|
|
+ "timestamp": "2026-01-30T10:00:00Z"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### Ably Presence
|
|
|
+
|
|
|
+LSS 启动后加入 `lss:registry` 通道的 Presence,数据包含节点基本信息。RCS 通过 Presence 感知 LSS 上线/下线。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 4.2 HTTP REST API
|
|
|
+
|
|
|
+#### 4.2.1 LSS 暴露的 HTTP 接口(供 RCS 或调试使用)
|
|
|
+
|
|
|
+基础路径:`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 请求体:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "taskId": "task_001",
|
|
|
+ "cameraId": "cam_001",
|
|
|
+ "sourceUrl": "rtsp://admin:pass@192.168.0.64/Streaming/Channels/101",
|
|
|
+ "whipUrl": "https://customer-xxx.cloudflarestream.com/xxx/whip",
|
|
|
+ "playbackUrl": "https://...",
|
|
|
+ "inputType": "RTSP",
|
|
|
+ "loop": false,
|
|
|
+ "ffmpegCommand": "...",
|
|
|
+ "videoConfig": { "codec": "libx264", "bitrate": "2500k", "fps": 30, "preset": "veryfast" },
|
|
|
+ "audioConfig": { "codec": "opus", "bitrate": "128k", "sampleRate": 48000, "channels": 2 }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**POST /stream/stop 请求体:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "taskId": "task_001",
|
|
|
+ "force": false
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**统一响应格式:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": { ... }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 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 /lss/register 请求体:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "nodeId": "lss-machine_007",
|
|
|
+ "nodeName": "LSS-Node-1",
|
|
|
+ "nodeIp": "192.168.0.100",
|
|
|
+ "nodePort": 10060,
|
|
|
+ "machineId": "machine_007",
|
|
|
+ "ablyClientId": "lss-machine_007",
|
|
|
+ "maxTasks": 4,
|
|
|
+ "ffmpegPath": "/usr/local/ffmpeg-whip/bin/ffmpeg",
|
|
|
+ "systemInfo": {
|
|
|
+ "os": "Linux 5.15.0",
|
|
|
+ "cpuCores": 8,
|
|
|
+ "totalMemory": 16000000000,
|
|
|
+ "availableMemory": 12000000000,
|
|
|
+ "ffmpegVersion": "6.0-whip"
|
|
|
+ },
|
|
|
+ "cameras": [
|
|
|
+ {
|
|
|
+ "ip": "192.168.0.64",
|
|
|
+ "port": 554,
|
|
|
+ "onvif": true,
|
|
|
+ "rtspUrl": "rtsp://192.168.0.64:554/Streaming/Channels/101",
|
|
|
+ "vendor": "HIKVISION",
|
|
|
+ "model": "DS-2CD2T45FWD"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**POST /lss/heartbeat 请求体:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "nodeId": "lss-machine_007",
|
|
|
+ "activeTaskCount": 2,
|
|
|
+ "cpuUsage": 45.5,
|
|
|
+ "memoryUsage": 62.3,
|
|
|
+ "cameras": [
|
|
|
+ {
|
|
|
+ "ip": "192.168.0.64",
|
|
|
+ "port": 554,
|
|
|
+ "online": true,
|
|
|
+ "rtspUrl": "rtsp://...",
|
|
|
+ "vendor": "HIKVISION",
|
|
|
+ "model": "DS-2CD2T45FWD"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**POST /lss/task/callback 请求体:**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "taskId": "task_001",
|
|
|
+ "nodeId": "lss-machine_007",
|
|
|
+ "callbackType": "STARTED",
|
|
|
+ "status": "STREAMING",
|
|
|
+ "processId": 12345,
|
|
|
+ "hlsPlaybackUrl": "...",
|
|
|
+ "errorMessage": null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+`callbackType` 枚举值:`STARTED` / `STOPPED` / `ERROR` / `STATUS_UPDATE`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 4.3 ONVIF 协议(SOAP/HTTP)
|
|
|
+
|
|
|
+LSS 自行实现了轻量级 ONVIF SOAP 客户端,不依赖 WS 框架。
|
|
|
+
|
|
|
+#### 认证方式:WS-Security UsernameToken + PasswordDigest
|
|
|
+
|
|
|
+```
|
|
|
+PasswordDigest = Base64(SHA-1(nonce + created + password))
|
|
|
+```
|
|
|
+
|
|
|
+SOAP Header 示例:
|
|
|
+
|
|
|
+```xml
|
|
|
+<s:Header>
|
|
|
+ <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
|
|
|
+ xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
|
|
|
+ <wsse:UsernameToken>
|
|
|
+ <wsse:Username>admin</wsse:Username>
|
|
|
+ <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">
|
|
|
+ base64_digest_value
|
|
|
+ </wsse:Password>
|
|
|
+ <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">
|
|
|
+ base64_nonce_value
|
|
|
+ </wsse:Nonce>
|
|
|
+ <wsu:Created>2026-01-30T10:00:00.000Z</wsu:Created>
|
|
|
+ </wsse:UsernameToken>
|
|
|
+ </wsse:Security>
|
|
|
+</s:Header>
|
|
|
+```
|
|
|
+
|
|
|
+#### ONVIF 服务端点与操作
|
|
|
+
|
|
|
+**设备服务** — `http://{ip}:{port}/onvif/device_service`
|
|
|
+
|
|
|
+| 操作 | 用途 | 返回值 |
|
|
|
+|------|------|--------|
|
|
|
+| `GetDeviceInformation` | 获取设备信息 | manufacturer, model, firmwareVersion, serialNumber, hardwareId |
|
|
|
+| `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 SOAP Body 示例:**
|
|
|
+
|
|
|
+```xml
|
|
|
+<s:Body>
|
|
|
+ <tptz:ContinuousMove xmlns:tptz="http://www.onvif.org/ver20/ptz/wsdl">
|
|
|
+ <tptz:ProfileToken>Profile_1</tptz:ProfileToken>
|
|
|
+ <tptz:Velocity>
|
|
|
+ <tt:PanTilt xmlns:tt="http://www.onvif.org/ver10/schema" x="-0.50" y="0.00"/>
|
|
|
+ <tt:Zoom xmlns:tt="http://www.onvif.org/ver10/schema" x="0.00"/>
|
|
|
+ </tptz:Velocity>
|
|
|
+ </tptz:ContinuousMove>
|
|
|
+</s:Body>
|
|
|
+```
|
|
|
+
|
|
|
+#### Profile Token 缓存
|
|
|
+
|
|
|
+ONVIF 每次 PTZ 操作都需要 Profile Token。首次调用时通过 `GetProfiles` 获取,之后按 `ip:port` 缓存复用。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 4.4 海康威视 ISAPI 协议(HTTP + Digest Auth)
|
|
|
+
|
|
|
+#### 认证方式:HTTP Digest Authentication (RFC 7616)
|
|
|
+
|
|
|
+需要先发送无认证请求获取 `WWW-Authenticate` challenge,然后用 Digest 算法计算 response。支持 nonce 复用和 nonce count 递增。
|
|
|
+
|
|
|
+#### 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) |
|
|
|
+
|
|
|
+`{ch}` 默认为 `1`(通道号)。
|
|
|
+
|
|
|
+**持续移动 XML 请求体:**
|
|
|
+
|
|
|
+```xml
|
|
|
+<PTZData>
|
|
|
+ <pan>-50</pan> <!-- -100~100, 负值=左, 正值=右 -->
|
|
|
+ <tilt>0</tilt> <!-- -100~100, 负值=下, 正值=上 -->
|
|
|
+ <zoom>0</zoom> <!-- -100~100, 负值=缩小, 正值=放大 -->
|
|
|
+</PTZData>
|
|
|
+```
|
|
|
+
|
|
|
+**设置预置位 XML 请求体:**
|
|
|
+
|
|
|
+```xml
|
|
|
+<PTZPreset>
|
|
|
+ <id>1</id>
|
|
|
+ <presetName>Entrance</presetName>
|
|
|
+</PTZPreset>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、功能模块详细说明
|
|
|
+
|
|
|
+### 5.1 FFmpeg 推流管理
|
|
|
+
|
|
|
+#### 5.1.1 命令构建
|
|
|
+
|
|
|
+如果 `StreamTask.FFmpegCommand` 非空,直接使用 RCS 预构建的命令。否则本地构建。
|
|
|
+
|
|
|
+**RTSP 模式命令模板:**
|
|
|
+
|
|
|
+```bash
|
|
|
+{ffmpegPath} \
|
|
|
+ -rtsp_transport tcp \
|
|
|
+ -analyzeduration 1000000 \
|
|
|
+ -probesize 1000000 \
|
|
|
+ -reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 3 \
|
|
|
+ -i {sourceUrl} \
|
|
|
+ -map 0:v:0 -map "0:a?" \
|
|
|
+ -c:v libx264 -preset veryfast -tune zerolatency \
|
|
|
+ -profile:v baseline -level 3.1 -pix_fmt yuv420p \
|
|
|
+ -g {keyframeInterval} -keyint_min {keyframeInterval} \
|
|
|
+ -b:v {videoBitrate} -maxrate {videoBitrate} -bufsize {2x videoBitrate} \
|
|
|
+ -c:a opus -strict -2 -ar {sampleRate} -ac {channels} -b:a {audioBitrate} \
|
|
|
+ -f whip {whipUrl}
|
|
|
+```
|
|
|
+
|
|
|
+**FILE 模式额外参数:**
|
|
|
+
|
|
|
+```bash
|
|
|
+-re -stream_loop -1 -i {sourceUrl}
|
|
|
+```
|
|
|
+
|
|
|
+**SRT 模式额外参数:**
|
|
|
+
|
|
|
+```bash
|
|
|
+-i "srt://{sourceUrl}"
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.1.2 进程管理
|
|
|
+
|
|
|
+- **启动**:通过 `os/exec` 启动 FFmpeg 子进程,记录 PID
|
|
|
+- **日志收集**:独立 goroutine 读取 stdout/stderr,缓冲最近 2000 字符
|
|
|
+- **启动检查**:启动后等待 2 秒检查进程是否存活
|
|
|
+- **优雅停止**:先发 SIGTERM,等待 10 秒超时后发 SIGKILL
|
|
|
+- **进程注册表**:`map[int64]*ProcessInfo`,线程安全(需用 sync.RWMutex)
|
|
|
+- **命令解析**:支持多行命令(反斜杠 `\` 续行),需清理后拆分为参数列表
|
|
|
+
|
|
|
+#### 5.1.3 ProcessInfo 结构
|
|
|
+
|
|
|
+```go
|
|
|
+type ProcessInfo struct {
|
|
|
+ PID int64
|
|
|
+ Process *os.Process
|
|
|
+ StartTime time.Time
|
|
|
+ LogBuffer string // 最近 2000 字符的日志
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.2 PTZ 云台控制
|
|
|
+
|
|
|
+#### 5.2.1 协议选择逻辑
|
|
|
+
|
|
|
+```go
|
|
|
+func selectAdapter(vendorType string) string {
|
|
|
+ switch strings.ToUpper(vendorType) {
|
|
|
+ case "HIKVISION", "HIK":
|
|
|
+ return "ISAPI"
|
|
|
+ default:
|
|
|
+ return "ONVIF" // 其他厂商默认使用 ONVIF
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.2.2 PTZ Action 枚举
|
|
|
+
|
|
|
+```go
|
|
|
+type PTZAction string
|
|
|
+
|
|
|
+const (
|
|
|
+ PanLeft PTZAction = "PAN_LEFT"
|
|
|
+ PanRight PTZAction = "PAN_RIGHT"
|
|
|
+ TiltUp PTZAction = "TILT_UP"
|
|
|
+ TiltDown PTZAction = "TILT_DOWN"
|
|
|
+ ZoomIn PTZAction = "ZOOM_IN"
|
|
|
+ ZoomOut PTZAction = "ZOOM_OUT"
|
|
|
+ FocusNear PTZAction = "FOCUS_NEAR"
|
|
|
+ FocusFar PTZAction = "FOCUS_FAR"
|
|
|
+ FocusAuto PTZAction = "FOCUS_AUTO"
|
|
|
+ IrisOpen PTZAction = "IRIS_OPEN"
|
|
|
+ IrisClose PTZAction = "IRIS_CLOSE"
|
|
|
+ Stop PTZAction = "STOP"
|
|
|
+ SetPreset PTZAction = "SET_PRESET"
|
|
|
+ GotoPreset PTZAction = "GOTO_PRESET"
|
|
|
+ DeletePreset PTZAction = "DELETE_PRESET"
|
|
|
+ StartPatrol PTZAction = "START_PATROL"
|
|
|
+ StopPatrol PTZAction = "STOP_PATROL"
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.2.3 速度映射
|
|
|
+
|
|
|
+| 参数 | 海康 ISAPI | ONVIF |
|
|
|
+|------|-----------|-------|
|
|
|
+| 方向速度 | -100 ~ 100(整数) | -1.0 ~ 1.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 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.3 巡航路线执行器 (PatrolTourExecutor)
|
|
|
+
|
|
|
+#### 5.3.1 核心概念
|
|
|
+
|
|
|
+巡航路线由一组 **航点 (Waypoint)** 组成,每个航点定义一个 PTZ 动作、速度、开始时间和持续时间。执行器按时序执行这些航点,支持循环、暂停/恢复。
|
|
|
+
|
|
|
+#### 5.3.2 数据结构
|
|
|
+
|
|
|
+```go
|
|
|
+type WaypointInfo struct {
|
|
|
+ Action string // LEFT, RIGHT, UP, DOWN, ZOOM_IN, ZOOM_OUT, STOP, ...
|
|
|
+ Speed int // 0-100
|
|
|
+ Duration int // 持续时间,毫秒
|
|
|
+ StartTime int // 相对于巡航开始的时间偏移,毫秒
|
|
|
+}
|
|
|
+
|
|
|
+type PatrolSession struct {
|
|
|
+ TourID string
|
|
|
+ CameraID string
|
|
|
+ State PatrolState // RUNNING / PAUSED / STOPPED
|
|
|
+ CurrentWaypoint int // 0-based 索引
|
|
|
+ CurrentLoop int // 0-based 循环计数
|
|
|
+ PauseLock sync.Mutex // 暂停/恢复同步
|
|
|
+ PauseCond *sync.Cond // 用于暂停等待
|
|
|
+}
|
|
|
+
|
|
|
+type PatrolState string
|
|
|
+
|
|
|
+const (
|
|
|
+ PatrolRunning PatrolState = "RUNNING"
|
|
|
+ PatrolPaused PatrolState = "PAUSED"
|
|
|
+ PatrolStopped PatrolState = "STOPPED"
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.3.3 执行流程
|
|
|
+
|
|
|
+```
|
|
|
+对每个循环 (loopCount, 0=无限循环):
|
|
|
+ 对每个航点:
|
|
|
+ 1. 等待 (waypoint.startTime - 上一个航点结束时间)
|
|
|
+ 2. 发送 PTZ ContinuousMove(action, speed)
|
|
|
+ 3. 等待 duration(每 500ms 检查中断标志)
|
|
|
+ 4. 发送 PTZ Stop
|
|
|
+ 5. 如果状态为 PAUSED,阻塞等待恢复
|
|
|
+ 6. 如果状态为 STOPPED,退出
|
|
|
+```
|
|
|
+
|
|
|
+#### 5.3.4 约束
|
|
|
+
|
|
|
+- **每个摄像头同一时刻只能有一个巡航任务**
|
|
|
+- 会话通过 `map[string]*PatrolSession`(key = cameraId)管理
|
|
|
+- 启动新巡航前必须停止已有巡航
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.4 摄像头发现与探测
|
|
|
+
|
|
|
+#### 5.4.1 网络扫描
|
|
|
+
|
|
|
+- 扫描指定子网(如 `192.168.0.0/24`)
|
|
|
+- 对每个 IP 尝试连接 RTSP (554)、HTTP (80)、ONVIF (80) 端口
|
|
|
+- 返回可用摄像头列表
|
|
|
+- 支持定时重扫描(默认 60 秒间隔)
|
|
|
+
|
|
|
+#### 5.4.2 设备探测 (Probe)
|
|
|
+
|
|
|
+探测流程:
|
|
|
+1. 尝试 ONVIF `GetDeviceInformation` + `GetCapabilities` + `GetProfiles`
|
|
|
+2. 如果 ONVIF 失败,尝试海康 ISAPI `GET /ISAPI/System/deviceInfo`
|
|
|
+3. 返回设备信息和能力集
|
|
|
+
|
|
|
+#### 5.4.3 CameraDeviceInfo 结构
|
|
|
+
|
|
|
+```go
|
|
|
+type CameraDeviceInfo struct {
|
|
|
+ CameraID string
|
|
|
+ IP string
|
|
|
+ Port int
|
|
|
+ Manufacturer string // "Hikvision", "Dahua", etc.
|
|
|
+ Model string
|
|
|
+ SerialNumber string
|
|
|
+ FirmwareVersion string
|
|
|
+ HardwareID string
|
|
|
+ MacAddress string
|
|
|
+ DeviceName string
|
|
|
+ ProbeMethod string // "ONVIF" / "ISAPI"
|
|
|
+ ProtocolSupport ProtocolSupport
|
|
|
+ Capabilities *CameraCapabilities
|
|
|
+ ProbeTime time.Time
|
|
|
+}
|
|
|
+
|
|
|
+type ProtocolSupport struct {
|
|
|
+ ONVIF bool
|
|
|
+ ISAPI bool
|
|
|
+ RTSP bool
|
|
|
+ ONVIFVersion string
|
|
|
+}
|
|
|
+
|
|
|
+type CameraCapabilities struct {
|
|
|
+ PTZ PTZCapabilities
|
|
|
+ Video VideoCapabilities
|
|
|
+ Audio AudioCapabilities
|
|
|
+}
|
|
|
+
|
|
|
+type PTZCapabilities struct {
|
|
|
+ Supported bool
|
|
|
+ ContinuousMove bool
|
|
|
+ AbsoluteMove bool
|
|
|
+ RelativeMove bool
|
|
|
+ Preset bool
|
|
|
+ MaxPresets int
|
|
|
+ Patrol bool
|
|
|
+ MaxPatrols int
|
|
|
+ Zoom bool
|
|
|
+ Focus bool
|
|
|
+ Iris bool
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.5 摄像头控制服务 (CameraControlService)
|
|
|
+
|
|
|
+处理 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` | 调用探测服务获取完整设备信息 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、节点生命周期管理
|
|
|
+
|
|
|
+### 6.1 启动流程
|
|
|
+
|
|
|
+```
|
|
|
+1. 收集系统信息(OS、CPU、内存、FFmpeg 版本)
|
|
|
+2. 生成 NodeID: "lss-{machineId}"
|
|
|
+3. 获取本机 IP
|
|
|
+4. 获取公网 IP(可选,GET https://api.ipify.org)
|
|
|
+5. 扫描本地网络摄像头
|
|
|
+6. POST /lss/register 向 RCS 注册
|
|
|
+7. 连接 Ably,加入 Presence
|
|
|
+8. 订阅指令通道 lss:{nodeId}:commands
|
|
|
+9. 启动 HTTP 心跳定时器(5 秒间隔)
|
|
|
+10. 启动 Ably 心跳定时器(10 秒间隔)
|
|
|
+11. 启动摄像头定时扫描(60 秒间隔)
|
|
|
+```
|
|
|
+
|
|
|
+### 6.2 心跳机制(双通道)
|
|
|
+
|
|
|
+| 通道 | 间隔 | 传输方式 | 内容 |
|
|
|
+|------|------|---------|------|
|
|
|
+| HTTP | 5 秒 | POST /lss/heartbeat | nodeId, activeTaskCount, cpuUsage, memoryUsage, cameras |
|
|
|
+| Ably | 10 秒 | Ably Message | 同上 + activeTasks 详情 |
|
|
|
+
|
|
|
+### 6.3 关闭流程
|
|
|
+
|
|
|
+```
|
|
|
+1. 停止所有活跃推流任务(逐个 SIGTERM → SIGKILL)
|
|
|
+2. 离开 Ably Presence
|
|
|
+3. 关闭 Ably 连接
|
|
|
+4. 停止所有定时器
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、错误码定义
|
|
|
+
|
|
|
+```go
|
|
|
+type ErrorCode int
|
|
|
+
|
|
|
+const (
|
|
|
+ Success ErrorCode = 0
|
|
|
+
|
|
|
+ // 1xxxx: 认证错误
|
|
|
+ Unauthorized ErrorCode = 10001
|
|
|
+ AccessDenied ErrorCode = 10002
|
|
|
+
|
|
|
+ // 2xxxx: 参数错误
|
|
|
+ BadRequest ErrorCode = 20001
|
|
|
+ ParamMissing ErrorCode = 20002
|
|
|
+ ParamInvalid ErrorCode = 20003
|
|
|
+ NotFound ErrorCode = 20004
|
|
|
+ ValidationFailed ErrorCode = 20005
|
|
|
+
|
|
|
+ // 301xx: 任务相关
|
|
|
+ TaskNotFound ErrorCode = 30101
|
|
|
+ TaskAlreadyExist ErrorCode = 30102
|
|
|
+ TaskLimitReached ErrorCode = 30103
|
|
|
+ TaskStartFailed ErrorCode = 30104
|
|
|
+ TaskStopFailed ErrorCode = 30105
|
|
|
+ TaskInvalidState ErrorCode = 30106
|
|
|
+
|
|
|
+ // 302xx: RTSP 相关
|
|
|
+ RTSPConnFailed ErrorCode = 30201
|
|
|
+ RTSPAuthFailed ErrorCode = 30202
|
|
|
+ RTSPStreamError ErrorCode = 30203
|
|
|
+
|
|
|
+ // 303xx: 节点相关
|
|
|
+ NodeNotReady ErrorCode = 30301
|
|
|
+ NodeBusy ErrorCode = 30302
|
|
|
+ NodeOffline ErrorCode = 30303
|
|
|
+
|
|
|
+ // 401xx: FFmpeg 相关
|
|
|
+ FFmpegNotFound ErrorCode = 40101
|
|
|
+ FFmpegStartFail ErrorCode = 40102
|
|
|
+ FFmpegCrash ErrorCode = 40103
|
|
|
+ FFmpegTimeout ErrorCode = 40104
|
|
|
+
|
|
|
+ // 402xx: WHIP/Cloudflare
|
|
|
+ WHIPConnFailed ErrorCode = 40201
|
|
|
+ WHIPAuthFailed ErrorCode = 40202
|
|
|
+ WHIPStreamError ErrorCode = 40203
|
|
|
+
|
|
|
+ // 403xx: Ably
|
|
|
+ AblyConnFailed ErrorCode = 40301
|
|
|
+ AblyAuthFailed ErrorCode = 40302
|
|
|
+ AblyPublishFail ErrorCode = 40303
|
|
|
+
|
|
|
+ // 404xx: RCS
|
|
|
+ RCSConnFailed ErrorCode = 40401
|
|
|
+ RCSAuthFailed ErrorCode = 40402
|
|
|
+ RCSCallbackFail ErrorCode = 40403
|
|
|
+
|
|
|
+ // 5xxxx: 内部错误
|
|
|
+ InternalError ErrorCode = 50001
|
|
|
+ DatabaseError ErrorCode = 50002
|
|
|
+ Unavailable ErrorCode = 50003
|
|
|
+ UnknownError ErrorCode = 59999
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、配置项
|
|
|
+
|
|
|
+```yaml
|
|
|
+server:
|
|
|
+ port: 10060 # HTTP 服务端口
|
|
|
+
|
|
|
+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 # 最大并发推流数
|
|
|
+
|
|
|
+ ffmpeg:
|
|
|
+ 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 心跳间隔
|
|
|
+
|
|
|
+ camera:
|
|
|
+ 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 路径 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 九、关键实现注意事项
|
|
|
+
|
|
|
+### 9.1 并发安全
|
|
|
+
|
|
|
+- 推流任务存储在内存中(`sync.RWMutex` 保护的 map),无数据库
|
|
|
+- FFmpeg 进程注册表需要线程安全
|
|
|
+- 巡航会话 map 需要加锁
|
|
|
+- ONVIF Profile Token 缓存需要线程安全
|
|
|
+
|
|
|
+### 9.2 Ably SDK
|
|
|
+
|
|
|
+Go 实现需使用 [Ably Go SDK](https://github.com/ably/ably-go),功能包括:
|
|
|
+- Realtime 连接
|
|
|
+- Channel 订阅(subscribe message)
|
|
|
+- Presence enter/leave
|
|
|
+- Message publish
|
|
|
+
|
|
|
+### 9.3 ONVIF 实现
|
|
|
+
|
|
|
+不需要完整的 ONVIF SDK,自行构建 SOAP XML 请求即可:
|
|
|
+- 使用 `net/http` 发送 POST 请求
|
|
|
+- 手动拼装 SOAP Envelope(参见第 4.3 节)
|
|
|
+- 使用正则或 `encoding/xml` 解析 XML 响应
|
|
|
+- HTTP 超时:5 秒
|
|
|
+
|
|
|
+### 9.4 海康 ISAPI 实现
|
|
|
+
|
|
|
+- 使用标准 `net/http` 客户端
|
|
|
+- 需实现 HTTP Digest Authentication(Go 标准库不自带,需自行实现或使用第三方库)
|
|
|
+- 支持 nonce 缓存和 nonce count 递增
|
|
|
+- XML 请求/响应处理
|
|
|
+
|
|
|
+### 9.5 无数据库
|
|
|
+
|
|
|
+所有状态均保存在内存中。服务重启后状态丢失,依赖 RCS 重新下发指令恢复。
|
|
|
+
|
|
|
+### 9.6 日志
|
|
|
+
|
|
|
+使用结构化日志(推荐 `slog` 或 `zerolog`),关键操作需记录:
|
|
|
+- 推流任务启动/停止/错误
|
|
|
+- PTZ 控制指令
|
|
|
+- Ably 连接状态变化
|
|
|
+- HTTP 心跳发送
|
|
|
+- 摄像头发现结果
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十、数据流示例
|
|
|
+
|
|
|
+### 10.1 启动推流
|
|
|
+
|
|
|
+```
|
|
|
+RCS → Ably(lss:{nodeId}:commands) → [command: START_STREAM]
|
|
|
+ → LSS 接收消息
|
|
|
+ → StreamTaskService.StartStream()
|
|
|
+ → 创建 StreamTask
|
|
|
+ → FFmpegGateway.StartProcess() → 启动 FFmpeg 子进程
|
|
|
+ → 等待 2 秒确认进程存活
|
|
|
+ → RcsGateway.TaskStartedCallback(taskId, pid, playbackUrl)
|
|
|
+ → Ably 发布结果到 lss:{nodeId}:status
|
|
|
+```
|
|
|
+
|
|
|
+### 10.2 PTZ 控制(向左转)
|
|
|
+
|
|
|
+```
|
|
|
+RCS → Ably(lss:{nodeId}:commands) → [ptz_command: PTZ_CONTROL, action: PAN_LEFT, speed: 50]
|
|
|
+ → LSS 接收消息
|
|
|
+ → PTZControlService.HandlePTZControl()
|
|
|
+ → 获取缓存的凭证
|
|
|
+ → 判断厂商类型
|
|
|
+ → 海康: PUT /ISAPI/PTZCtrl/channels/1/continuous → <PTZData><pan>-50</pan>...</PTZData>
|
|
|
+ → 或 ONVIF: POST /onvif/ptz_service → ContinuousMove(x=-0.5, y=0)
|
|
|
+ → Ably 发布结果
|
|
|
+```
|
|
|
+
|
|
|
+### 10.3 巡航执行
|
|
|
+
|
|
|
+```
|
|
|
+RCS → Ably → [patrol_command: PATROL_START, waypoints: [...], loopCount: 3]
|
|
|
+ → PatrolTourExecutor.StartPatrol()
|
|
|
+ → 创建 PatrolSession
|
|
|
+ → 启动 goroutine 执行巡航
|
|
|
+ → 循环 3 次:
|
|
|
+ → 航点 1: 等待 → ContinuousMove(LEFT, 50) → 等 3s → Stop
|
|
|
+ → 航点 2: 等待 → Stop → 等 2s
|
|
|
+ → 航点 3: 等待 → ContinuousMove(RIGHT, 50) → 等 3s → Stop
|
|
|
+ → 巡航完成,清理会话
|
|
|
+```
|