# 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 admin base64_digest_value base64_nonce_value 2026-01-30T10:00:00.000Z ``` #### 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 Profile_1 ``` #### 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 -50 0 0 ``` **设置预置位 XML 请求体:** ```xml 1 Entrance ``` --- ## 五、功能模块详细说明 ### 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 → -50... → 或 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 → 巡航完成,清理会话 ```