# GB28181Client **Repository Path**: AndroidCoderPeng/GB28181Client ## Basic Information - **Project Name**: GB28181Client - **Description**: CLion平台集成Qt框架实现GB28181推流例子,包含Sip信令交互+OpenCV采集相机预览数据+FFmpeg编码+PS封装+RTP发送PS流到国标平台(ZLMediaKit) - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2026-06-14 - **Last Updated**: 2026-08-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # GB28181Client 基于 **Qt5 + OpenCV + FFmpeg + eXosip2** 实现的 **GB/T 28181-2016 设备侧(IPC)客户端**: SIP 信令交互 + 相机采集预览 + 视频/音频编码 + MPEG-2 PS 封装 + RTP 推流到国标平台(已适配 ZLMediaKit),并支持平台发起的 **语音广播(对讲)**。 --- ## 1. 功能特性 | 能力 | 状态 | 说明 | |----------------------------------|------|-----------------------------------------------| | SIP 注册 / 注销 / 鉴权 | ✅ | REGISTER + 401/407 MD5 摘要认证,expires=7200 | | SIP 心跳保活 | ✅ | 独立线程,30s 一次 MESSAGE(Keepalive) | | 目录查询 Catalog | ✅ | 响应 1 路通道(设备自身) | | 设备信息 DeviceInfo | ✅ | 响应设备名称 / 厂商 / 型号等 | | 设备状态 DeviceStatus | ✅ | 响应 ONLINE / ON DUTY | | 实时点播(Invite → PS/RTP 推流) | ✅ | 支持 UDP / TCP 两种传输 | | 语音广播(对讲,平台 → 设备) | ✅ | TCP 接收 G.711 帧并解码播放 | | 云台控制 DeviceControl | ❌ | 未实现,收到后仅告警 | | 配置查询 ConfigDownload | ❌ | 未实现 | | 录像查询 / 回放 RecordInfo | ❌ | 未实现 | | 报警上报 Alarm | ❌ | 未实现 | --- ## 2. 技术栈 | 类别 | 依赖 | |-------------------|-----------------------------------------------------------------| | 语言 / 构建 | C++14、CMake 3.10+(`AUTOMOC` / `AUTOUIC` / `AUTORCC`) | | UI / 音频采集播放 | Qt5 `Core` `Gui` `Widgets` `Multimedia` | | SIP 信令 | eXosip2 / osip2(`pkg-config libeXosip2`) | | 视频编码 | FFmpeg `libavcodec` `libavutil` `libswscale`(及其他 4 个模块) | | 视频采集 | OpenCV `VideoCapture`(V4L2 后端) | | XML | pugixml 1.15(内置于 `3rdparty/`) | --- ## 3. 目录结构 ``` GB28181Client/ ├── main.cpp / ClientApplication.* # 程序入口:QApplication、全局 QSS、主窗口 ├── GlobalDefinition.hpp # 全局常量、NALU / SdpStruct / SipParameter、注册状态枚举 ├── Logger.* # 带边框的自绘日志(d/i/w/e、dFmt、dBox 流式 API) ├── RtpSender.* # RTP 封装与 UDP / TCP 发送(单例) ├── sip/ │ ├── SipManager.* # 门面:组装各子模块 + SIP 事件循环线程 │ ├── SipContext.* # eXosip 上下文生命周期、参数、URI 推导、线程锁 │ ├── RegisterManager.* # 注册状态机、鉴权、注销 │ ├── HeartbeatManager.* # 心跳线程 │ ├── StreamManager.* # 点播推流会话 + 语音对讲会话管理 │ ├── EventDispatcher.* # 事件类型 → Handler 注册表分发 │ ├── handlers/ # RegistrationHandler / MessageHandler / CallHandler / DefaultHandler │ └── utils/ # ResponseSender / SdpParser / StateCode / WorkerThread / XmlBuilder ├── video/ │ ├── FrameCapture.* # OpenCV 相机采集(独立 QThread + QTimer 40ms) │ ├── PreviewWorker.* # 缩放 + BGR→RGB + QImage 本地预览(独立 QThread) │ ├── FrameEncoder.* # FFmpeg H.264 软编码(独立 QThread) │ └── H264Splitter.* # AnnexB / AVCC 格式检测与 NALU 拆分 ├── audio/ │ ├── AudioCapture.* # Qt QAudioInput 采集 PCM(独立 QThread) │ ├── AudioReceiver.* # TCP 接收平台对讲音频(独立 QThread + 环形缓冲) │ ├── AudioProcessor.* # G.711 A-law / μ-law 与 PCM16 互转 │ └── RingBuffer.* # 无锁读写位置的环形缓冲 ├── muxer/ │ ├── PsMuxer.* # PES / PS 封装、SPS/PPS 缓存、分片(单例) │ └── HeaderBuilder.* # Pack Header / System Header / PSM / PES Header 构造 ├── ui/MainWindow.* # 主窗口:参数输入、注册/注销、预览、对讲播放 ├── 3rdparty/pugixml.* # XML 解析/序列化 ├── Resources.qrc / style.qss # Qt 资源与全局样式 └── sip_command.md # 平台下发信令 XML 样例 ``` --- ## 4. 编译与运行 ```bash # 依赖(Debian / Ubuntu) sudo apt install -y build-essential cmake pkg-config \ libexosip2-dev \ libavformat-dev libavcodec-dev libavutil-dev libavfilter-dev \ libswscale-dev libswresample-dev \ libopencv-dev \ qtbase5-dev qtmultimedia5-dev # 构建 mkdir -p build && cd build cmake .. make -j"$(nproc)" ./GB28181Client ``` > 注意:`SipContext` 固定以 **TCP 监听本地 5060 端口**接收 SIP 信令,若被占用会初始化失败。 ### 界面参数 | 控件 | 说明 | 默认值 | |----------------|---------------------------------------------------------|------------------------| | 协议版本 | 只读展示,未参与逻辑 | `2016` | | SIP 传输协议 | 下拉框(`TCP` / `UDP`),当前**未参与逻辑**(见 §12.9) | `TCP` | | SIP 服务器地址 | 平台 IP | `111.198.10.15` | | 服务器端口 | 平台 SIP 端口 | `22117` | | SIP 服务器域 | 平台域编码 | `1101080000` | | SIP 服务器 ID | 平台设备编码 | `11010800002000000002` | | 设备名称 | 同时作为 SIP 认证用户名与 User-Agent 后缀 | `L1300011118` | | 设备编号 | 本端设备编码(20 位) | `11010800001300011118` | | SIP 认证密码 | MD5 摘要认证用 | `1234qwer` | | 注册 / 注销 | 按钮,切换注册状态 | — | --- ## 5. 整体数据流 ``` 【上行 · 推流】 Camera(V4L2/MJPG) └─OpenCV─> FrameCapture ──frameCaptured(shared_ptr)──┬─> PreviewWorker ─onPreviewReady(QImage)─> QLabel └─> FrameEncoder ─onFrameEncode(vector)─> 主线程 Mic ─Qt Multimedia─> AudioCapture ──pcmCaptured(QByteArray)───────────────────────────> 主线程 主线程(_isStartStream == true 时才落盘) ├─ PsMuxer::writeVideoFrame(H.264, len, pts_90k) └─ PsMuxer::writeAudioFrame(PCM16, size, pts_90k) // 内部 PCM → G.711μ │ ▼ PsMuxerCallback(由 RtpSender 注入) RtpSender::sendDataPacket → [RTP头(12B)] + [PS包] ──TCP(0x24 framing) / UDP sendto──> 国标平台 【下行 · 语音对讲】 平台 TCP ─> AudioReceiver(QThread) ─> RingBuffer(256KB) ─ 按魔数 03 2C 80 88 定位帧 └─> audioFrameReadySignal(QByteArray 160B G.711) └─> StreamManager lambda ─> G711Callback ─> SipManager::g711DataSignal └─> MainWindow::onG711DataReceived ─ alaw/ulawToPcm ─> RingBuffer(256KB) ─> QAudioOutput 播放 ``` --- ## 6. SIP 信令流程 ### 6.1 初始化与注册 1. `SipContext::initialize()`:`eXosip_malloc` → `eXosip_init` → `eXosip_listen_addr(TCP, 5060)`。 2. URI 由 `SipParameter` 现算,始终与参数一致: - `From` = `sip:{设备编号}@{服务器域}` - `To` = `sip:{服务器ID}@{服务器域}` - `Proxy`= `sip:{服务器地址}:{端口}` - `User-Agent` = `GB28181-Device/1.0 {设备名称}` 3. `RegisterManager::startRegistration()` 发送 `REGISTER`(`expires = REGISTER_EXPIRED_TIME = 7200`)。 4. 收到 **401 / 407** → `handleAuthentication()`: `eXosip_add_authentication_info(用户名=设备名称, 认证ID=设备编号, 密码, "MD5")` → 重建 REGISTER 并发送。 5. 状态机:`IDLE → SENT_INITIAL → SENT_AUTH → SUCCESS / FAILED`;成功后回调 `onLoginSuccess()` → 启动心跳 + 抛出 `1000`。 ### 6.2 注销 `logout()` → `REGISTER(expires = 0)` → 收到 2xx → `onLogoutSuccess()`: 1. 停止心跳; 2. `StreamManager::reset()`(静默释放 RTP 与音频接收器、复位会话); 3. `RegisterManager::clearRegistration()` 移除 eXosip 注册记录; 4. 抛出 `201`。 ### 6.3 心跳 `HeartbeatManager`(继承 `WorkerThread` 的独立 `std::thread`)每 `HEARTBEAT_INTERVAL = 30s` 触发一次 `ResponseSender::sendHeartbeat()`,构造 `Keepalive` XML(`XmlBuilder::buildHeartbeat`,SN 自增)并以 `MESSAGE`(`Content-Type: Application/MANSCDP+xml`)上报;`rid <= 0` 时跳过。 ### 6.4 MESSAGE 处理(`MessageHandler`) 注册事件:`EXOSIP_MESSAGE_NEW` / `EXOSIP_MESSAGE_ANSWERED` / `EXOSIP_MESSAGE_REQUESTFAILURE` 处理顺序: 1. `Content-Type` 必须为 `Application/MANSCDP+xml`,否则回 **415**; 2. 消息体为空回 **400**,XML 解析失败回 **400**; 3. **先回 200 OK 再处理业务**(避免平台超时重传); 4. 按根节点分发: - `` → `Catalog` / `DeviceInfo` / `DeviceStatus` 由 `XmlBuilder` 构造 ``,再通过 `ResponseSender::sendMessage()` 主动 MESSAGE 上报;其余(`ConfigDownload` 等)仅告警; - `` → `CmdType = Broadcast` 时解析 `SourceID`(平台侧)/ `TargetID`(设备侧), 触发语音对讲流程。 ### 6.5 实时点播(Invite → 推流) `CallHandler` 覆盖 `EXOSIP_CALL_INVITE / ANSWERED / ACK / CLOSED / RELEASED / NOANSWER / CANCELLED / *FAILURE` 等全部呼叫事件。 ``` 平台 ──INVITE(SDP Offer)──> CallHandler ─> StreamManager::handleVideoInvite() │ SdpParser::parse() 解析 c=/m=/a=setup/a=rtpmap/y= │ transport == "udp" ? RtpSender::initUdpSocket : initTcpSocket │ SdpParser::buildUpstreamSdp() 构造 Answer │ 平台 <──200 OK(SDP Answer)─────────────────────┘ 记录 cid/did,回调 2100(开始推流) 平台 ──ACK──> 开始收流 平台 ──BYE──> CallHandler::handleCallClosed → StreamManager::stopPushStream() (发 BYE + RtpSender::stop(),回调 2101) ``` - 解析失败的兜底:消息体为空 / SDP IP 端口无效 → 回 **488**;RTP 通道建立失败 → 回 **500** + 业务码 `2109`。 - 上行 SDP Answer(`buildUpstreamSdp`): ``` v=0 o=<设备编号> 0 0 IN IP4 <本地IP> s=Play c=IN IP4 <本地IP> t=0 0 m=video 9 TCP/RTP/AVP 96 a=sendonly a=rtpmap:96 PS/90000 a=connection:new y= ``` > 上行只有一条 `m=video` 行:视频 H.264 与音频 G.711 都已被 PS 流复用,一个 PS 通道即可承载。 --- ## 7. 媒体上行:采集 → 编码 → PS 封装 → RTP ### 7.1 视频采集(`FrameCapture`) - `cv::VideoCapture(0, cv::CAP_V4L2)`,强制 **MJPG** 编码格式(高分辨率下 YUV 会因 USB 带宽不足掉帧, OpenCV 内部用 libjpeg 解码回 BGR,不影响后续编码); - 请求 `1920×1080 @25fps`,并 **回读实际值**(V4L2 常静默降级); - `QTimer(40ms)` 定时取帧,`emit frameCaptured(std::shared_ptr)`(智能指针避免跨线程深拷贝)。 ### 7.2 本地预览(`PreviewWorker`) 3 帧环形缓冲(生产者/消费者,`QMutex` 保护)→ `cv::resize` 到 **640×360** → BGR→RGB → `emit onPreviewReady(QImage)` → 主线程 `QLabel::setPixmap`。 ### 7.3 H.264 编码(`FrameEncoder`) | 参数 | 值 | |-------------|----------------------------------------------------------------| | 编码器 | `avcodec_find_encoder(AV_CODEC_ID_H264)`(libx264) | | 分辨率 | `1920×1080` | | 像素格式 | `AV_PIX_FMT_YUV420P`(用 `sws_scale` 从 OpenCV 的 BGR24 转换) | | 时基 / 码率 | `1/25` / `4500000` | | GOP | `25`(1 秒一个 IDR) | | B 帧 | `0`(实时流不引入编解码延迟) | | x264 预设 | `preset=ultrafast`、`tune=zerolatency` | 编码结果( **AVCC 或 AnnexB 均可能**)通过 `emit onFrameEncode(std::vector)` 交给主线程。 > ⚠️ 关键点:无论哪种格式,`PsMuxer` 都要求最终是带起始码(`00 00 00 01` / `00 00 01`)的 AnnexB, > 否则 NALU 无法拆分、PS 封装失败、平台无画面。`H264Splitter` 提供 `detectFormat()` 与 `avccToAnnexB()`。 ### 7.4 PS 封装(`PsMuxer` / `HeaderBuilder`) **时间戳**:GB/T 28181-2016 要求 90kHz 时基,由主线程按帧号计算: ```cpp // 视频:每帧间隔 = 90000 / 25 = 3600 const quint32 pts_90k = _videoFrameCount * (TIMESTAMP_BASE / VIDEO_FPS); // 音频:8kHz 采样,按样本序号换算 const double ts_sec = static_cast(_audioFrameCount) / 8000; const quint32 pts_90k = static_cast(ts_sec * 90000); ``` **视频帧处理流程**: 1. `H264Splitter::splitFrame()` 按起始码拆分为 NALU 列表; 2. 分类:`7=SPS`(缓存)、`8=PPS`(缓存)、`5=IDR`、`1=非IDR(P)`、`6=SEI`(丢弃); 3. **首帧门控**:`_isWaitingIdr` 为 true 时丢弃所有帧直到第一个 IDR 到达(保证平台先拿到 SPS/PPS); 4. IDR 帧组帧:`[0001][SPS][0001][PPS][0001][IDR...]` → PES(`stream_id = 0xE0`)→ PS 包( **带 System Header + PSM**); 5. 非 IDR 帧组帧:`[0001][P...]` → PES(`0xE0`)→ PS 包( **不带** System Header / PSM); 6. 缺 SPS/PPS 时直接丢弃该 IDR 帧。 **音频帧处理流程**: 1. PCM16(8kHz/单声道/16bit)→ G.711μ(`AudioProcessor::pcmToUlaw`,另有 `pcmToAlaw` 可切换); 2. 直接作为 PES 负载(`stream_id = 0xBD`、`stream_type = 0x91`)→ PS 包; 3. **无需起始码、无需 System Header / PSM**; 4. 门控:`_isIdrSent == false`(首个 IDR 未发出)前丢弃所有音频,避免"只听得到声音没画面"。 **PES 分片**: ``` PS 包 = [Pack Header(14B)] + [System Header + PSM(仅关键帧)] + [PES 包] ``` - PES 负载 ≤ `MAX_PES_PAYLOAD_PER_PACKET = 1300` → 单包发送; - \> 1300 → 按 1300 字节切分, **每一片都重新构建 PES 头并携带完整 PS 头**; - 关键帧标志贯穿所有分片(每片都带 System Header / PSM); - 仅 **最后一片**的 `is_last = true`,用于置位 RTP marker。 ### 7.5 RTP 发送(`RtpSender`) - 12 字节固定头:`V=2, P=0, X=0, CC=0`,`PT = 96`(PS 流),`M = is_last ? 1 : 0`; - `seq` 随机初值后逐包递增(发送成功才自增),`SSRC` 取自平台 SDP 的 `y=` 字段( **十进制解析**),异常时回退随机值; - `MAX_RTP_PAYLOAD = 1400` 越界包直接丢弃; - **UDP**:非阻塞 `sendto`,发送缓冲 512KB; - **TCP**:按 RTSP interleaved 方式加 4 字节前缀 `$` + `channel(0)` + 2 字节大端长度, `send_all()` 内部用 `poll(POLLOUT)` 处理 `EAGAIN`; - socket 关闭统一加互斥锁并先置 `-1` 再 `close`,避免 double-close / fd 复用竞态。 > ⚠️ 关键点:传输协议与地址端口 **必须严格按平台 SDP 协商结果执行**,否则平台会直接丢弃收到的包。 --- ## 8. 语音对讲(下行 · Broadcast) ``` 平台 ──MESSAGE/Notify(CmdType=Broadcast, SourceID, TargetID)──> MessageHandler 设备 ──200 OK──> 平台 设备:AudioReceiver 绑定本地随机 TCP 端口(bind Any:0,读缓冲 256KB) 设备 ──INVITE(SDP, a=setup:active, m=audio TCP/RTP/AVP 0 96, PCMU/8000, recvonly)──> 平台 平台 ──200 OK(SDP Answer)──> CallHandler → StreamManager::handleAudioAnswer() 设备:校验 cid → 解析平台 IP/端口 → connectPlatform(5s 超时) → sendAck → 回调 2200 平台 ──TCP G.711 流──> AudioReceiver(QThread + RingBuffer 256KB) ─> 160B 帧 └─> G711Callback ─> SipManager::g711DataSignal ─> 解码 PCM ─> RingBuffer ─> QAudioOutput(8k/单声道/16bit) 平台 ──BYE──> stopReceiveAudio() → 回调 2201 → stopAudioTalk() ``` ### 为什么用 TCP 设备通常在内网/局域网,平台在公网。UDP 需要自行实现 NAT 穿透或打洞; 而 TCP 由 **设备主动向平台发起连接** 建立链路后,平台即可复用该连接把对讲数据下发,无需公网 IP。 ### 帧边界识别 平台下发为 TCP interleaved 数据:`[03 2C] + [RTP 头 12B] + [G.711 数据(多个 160 字节帧)]`。 `AudioReceiver` 在环形缓冲中搜索魔数 `03 2C 80 88`,跳过 14 字节头后按 `G711_FRAME_SIZE = 160` 字节(20ms @ 8kHz 8bit)切帧并上抛。 ### 为什么必须"独立线程 + 环形缓冲" 平台音频回调频率极高(每 20ms 一帧)。若在 SIP 事件循环或 GUI 线程内直接解析播放, 会阻塞信令处理并造成卡顿,因此采用 `AudioReceiver` 独占 `QThread` + `RingBuffer(256KB)` 的方案。 ### 解码类型 由平台 SDP 的 `a=rtpmap` 决定(`handleAudioAnswer` 中判定): | Payload | 编码 | 解码函数 | |---------|--------------------|-----------------------------| | `8` | PCMA (G.711 A-law) | `AudioProcessor::alawToPcm` | | `0` | PCMU (G.711 μ-law) | `AudioProcessor::ulawToPcm` | > ⚠️ G.711 数据回调类型是 **ByteArray**(每样本 1 字节),解码后的 PCM 裸流是 **ShortArray**(每样本 2 字节), > 单位与字节数不同,取长度时不要混用。 --- ## 9. 线程模型 | 线程 | 载体 | 职责 | |--------------|---------------------------------|------------------------------------------------------------| | GUI 主线程 | Qt | 参数输入、注册/注销、预览渲染、PS 封装调用、对讲播放 | | 相机采集 | `QThread` + `QTimer(40ms)` | OpenCV 抓帧 | | 预览渲染 | `QThread` | 缩放 / 色彩转换 / QImage | | 视频编码 | `QThread` | FFmpeg H.264 编码 | | 音频采集 | `QThread` + `QTimer(10ms)` | Qt `QAudioInput` 读取 PCM | | SIP 事件循环 | `WorkerThread`(`std::thread`) | `eXosip_event_wait(100ms)` + `eXosip_execute()` + 事件分发 | | 心跳 | `WorkerThread`(`std::thread`) | 每 30s 发送 Keepalive | | 对讲音频接收 | `QThread` | TCP 收流 + 环形缓冲 + 切帧 | - 跨线程通信统一走 Qt **QueuedConnection**,并在 `ClientApplication` 中 `qRegisterMetaType>()`; - 空闲时每 30s 打印一次事件循环心跳日志(含空闲循环数与当前注册状态),便于判断卡死; - `eXosip_event_free(event)` 必须在分发后调用,否则内存泄漏。 --- ## 10. 状态码(`StateCode::toString`) | 区间 | 含义 | 常用码 | |-------------|-----------------|----------------------------------------------------------------------------------------| | 1xx ~ 6xx | 标准 SIP 响应码 | `200` 成功、`401` 未授权、`488` 此处不可接受 | | 1000 ~ 1080 | eXosip 事件映射 | **`1000` 注册成功**、`1001` 注册失败 | | 1100 ~ 1122 | osip2 返回码 | `1107` API 未初始化 | | 2000 ~ 2099 | 注册相关错误 | `2001` 发送注册失败、`2003` 发送认证注册失败 | | 2100 ~ 2199 | 媒体流 | **`2100` 开始推流**、**`2101` 停止推流**、`2103` SDP 协商失败、`2109` RTP 端口分配失败 | | 2200 ~ 2299 | 语音对讲 | **`2200` 开始接收音频**、**`2201` 停止接收音频**、`2203` SDP 协商失败 | | 2300 ~ 2399 | 消息处理 | `2300` XML 解析失败、`2303` 不支持的控制命令 | | 3000 ~ 3005 | 系统 / 网络 | `3003` 线程创建失败 | `MainWindow::onSipStateChanged()` 只消费关键码:`1000`(注册成功)、`201`(注销成功)、 `2100`/`2101`(启停推流)、`2200`/`2201`(启停对讲播放)。 --- ## 11. 关键参数(`GlobalDefinition.hpp`) | 常量 | 值 | 说明 | |--------------------------------------|-----------------|----------------------------| | `VIDEO_WIDTH` / `VIDEO_HEIGHT` | `1920` / `1080` | 采集与编码分辨率 | | `VIDEO_FPS` | `25` | 帧率 | | `VIDEO_BIT_RATE` | `4500000` | H.264 码率 | | `TIMESTAMP_BASE` | `90000` | PS/RTP 时间戳基准(90kHz) | | `G711_FRAME_SIZE` | `160` | 20ms @ 8kHz 8bit | | `RING_BUFFER_SIZE` | `256 * 1024` | 对讲接收环形缓冲 | | `REGISTER_EXPIRED_TIME` | `7200` | 注册有效期(秒) | | `HEARTBEAT_INTERVAL` | `30` | 心跳间隔(秒) | | `VIDEO_STREAM_TYPE` / `ID` | `0x1B` / `0xE0` | PS 中视频流类型 / 流 ID | | `AUDIO_STREAM_TYPE` / `ID` | `0x91` / `0xBD` | G.711 私有流类型 / 流 ID | | `MAX_PES_PAYLOAD_PER_PACKET` | `1300` | PES 负载分片阈值 | | `MAX_RTP_PAYLOAD` / `MAX_RTP_PACKET` | `1400` / `1412` | RTP 负载与整包上限 | --- ## 12. 已知问题 > 以下问题在阅读代码时确认存在,改动前请留意。 1. **本地 IP 硬编码**:`MainWindow::doRegister()` 中 `param.localHost = "192.168.3.131"`, `longitude/latitude` 与空的 `serialNumber` 亦硬编码,未开放到界面。 2. **eXosip 监听端口固定 5060/TCP**,被占用即无法启动,未在界面或日志中给出可配置入口。 3. **相机设备号写死 0**:`FrameCapture::start()` 内 `_cap.open(0, cv::CAP_V4L2)`,构造参数 `index` 未生效; 预览缩放尺寸也在 `PreviewWorker` 内硬编码为 `640×360`。 4. **未实现的国标能力**:`ConfigDownload`、`DeviceControl`(云台)、`RecordInfo`(录像查询/回放)、`Alarm`(报警上报)。 5. **对讲帧同步依赖魔数** `03 2C 80 88`:当平台一次写入跨多个 RTP 包或出现粘包/半包时, 当前仅做"堆积超过 2048 字节丢弃前 1024 字节"的粗粒度恢复,鲁棒性有限。 6. **界面"SIP 传输协议"下拉框未接线**:`MainWindow::doRegister()` 未读取 `ui->comboBox`, SIP 信令始终走 `SipContext` 中固定的 TCP 5060;媒体传输协议则完全由平台 SDP 决定。 7. **编码线程常驻**:`FrameCapture` / `FrameEncoder` 在窗口构造时即启动并持续编码, `_isStartStream` 只控制"是否写入 PS/RTP",未注册或未点播时仍占用 CPU。 --- ## 13. 参考 - `sip_command.md`:平台下发的 Query / Control / Notify XML 样例(DeviceInfo、Catalog、ConfigDownload、 DeviceStatus、DeviceControl/PTZCmd、Broadcast)。 - 媒体流结构速记: ``` H.264 帧: [00 00 00 01 SPS][00 00 00 01 PPS][00 00 00 01 IDR][00 00 01 P][00 00 01 P]... PES 包: [起始码 00 00 01 BA/...][PES Header][负载] PS 包: [Pack Header][System Header + PSM(仅关键帧)][PES 包] RTP 包: [RTP Header 12B][PS 包 ≤1400B] TCP 传输: [0x24][channel][len_hi][len_lo][RTP 包] ```