yb 10 часов назад
Родитель
Сommit
3cb130ae93
1 измененных файлов с 1152 добавлено и 0 удалено
  1. 1152 0
      docs/GO_REWRITE_REQUIREMENTS.md

+ 1152 - 0
docs/GO_REWRITE_REQUIREMENTS.md

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