# 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
→ 巡航完成,清理会话
```