GO_REWRITE_REQUIREMENTS.md 34 KB

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(推流任务)

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(输入类型枚举)

type InputType string

const (
    InputRTSP InputType = "RTSP"
    InputFILE InputType = "FILE"
    InputSRT  InputType = "SRT"
    InputRTMP InputType = "RTMP"
)

2.3 StreamStatus(任务状态枚举)

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(节点信息)

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(摄像头信息)

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

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

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

type CameraScannerGateway interface {
    ScanCameras(subnet string) ([]Camera, error)
    ScanLocalNetwork() ([]Camera, error)
}

3.4 CameraDeviceAdapter

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(推流指令):

{
  "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 云台指令):

{
  "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(摄像头指令):

{
  "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(巡航指令):

{
  "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 通道发布结果:

{
  "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

{
  "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 请求体:

{
  "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 请求体:

{
  "taskId": "task_001",
  "force": false
}

统一响应格式:

{
  "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 请求体:

{
  "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 请求体:

{
  "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 请求体:

{
  "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 示例:

<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 示例:

<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 请求体:

<PTZData>
  <pan>-50</pan>   <!-- -100~100, 负值=左, 正值=右 -->
  <tilt>0</tilt>   <!-- -100~100, 负值=下, 正值=上 -->
  <zoom>0</zoom>   <!-- -100~100, 负值=缩小, 正值=放大 -->
</PTZData>

设置预置位 XML 请求体:

<PTZPreset>
  <id>1</id>
  <presetName>Entrance</presetName>
</PTZPreset>

五、功能模块详细说明

5.1 FFmpeg 推流管理

5.1.1 命令构建

如果 StreamTask.FFmpegCommand 非空,直接使用 RCS 预构建的命令。否则本地构建。

RTSP 模式命令模板:

{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 模式额外参数:

-re -stream_loop -1 -i {sourceUrl}

SRT 模式额外参数:

-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 结构

type ProcessInfo struct {
    PID       int64
    Process   *os.Process
    StartTime time.Time
    LogBuffer string // 最近 2000 字符的日志
}

5.2 PTZ 云台控制

5.2.1 协议选择逻辑

func selectAdapter(vendorType string) string {
    switch strings.ToUpper(vendorType) {
    case "HIKVISION", "HIK":
        return "ISAPI"
    default:
        return "ONVIF" // 其他厂商默认使用 ONVIF
    }
}

5.2.2 PTZ Action 枚举

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 数据结构

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 结构

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. 停止所有定时器

七、错误码定义

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
)

八、配置项

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,功能包括:

  • 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 日志

使用结构化日志(推荐 slogzerolog),关键操作需记录:

  • 推流任务启动/停止/错误
  • 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
    → 巡航完成,清理会话