# RtspPusherClient **Repository Path**: AndroidCoderPeng/RtspPusherClient ## Basic Information - **Project Name**: RtspPusherClient - **Description**: 基于 Qt5 + OpenCV + FFmpeg 的 RTSP 推流桌面客户端: USB 相机采集 → 本地预览 → H.264 软编码 → FFmpeg RTSP muxer 推流到外部流媒体服务器(如 ZLMediaKit), 支持画面水印与实时帧率显示。 - **Primary Language**: C++ - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-05 - **Last Updated**: 2026-09-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RtspPusherClient 基于 Qt5 + OpenCV + FFmpeg 的 RTSP 推流桌面客户端: USB 相机采集 → 本地预览 → H.264 软编码 → FFmpeg RTSP muxer 推流到外部流媒体服务器(如 ZLMediaKit), 支持画面水印与实时帧率显示。 ## 特性 - **采集**:OpenCV `VideoCapture`(`cv::CAP_V4L2`,默认 `/dev/video0`),强制 `MJPG` 四字符码, 1920×1080 @ 30 fps;每秒统计相机实际帧率并在画面右上角显示,首帧时打印相机真实输出格式 / 分辨率 / 帧率 - **预览**:独立预览线程,环形缓冲(容量 3)+ `QImage` 显示,缩放到 854×480(480P),不阻塞采集与编码; 发送给 UI 前 `img.copy()`,避免跨线程数据竞争 - **编码**:FFmpeg `AV_CODEC_ID_H264`(libx264),`preset=ultrafast` + `tune=zerolatency`,无 B 帧, GOP = `VIDEO_FPS / 2`;`AV_CODEC_FLAG_GLOBAL_HEADER` 让 `extradata` 携带 SPS/PPS - **按需编码**:未开始推流时 `FrameEncoder::frameReceived()` 直接丢帧,不进缓冲、不编码,CPU 占用不随预览上涨 - **推流**:FFmpeg RTSP muxer(`avformat_write_header` / `av_interleaved_write_frame`), `rtsp_transport=tcp` + `stimeout=5000000`(5 秒超时),避免 UDP 丢包花屏 - **格式自适应**:`H264Splitter` 自动探测 AnnexB / AVCC,AVCC 先转 AnnexB 再送出 - **IDR 对齐**:启动后丢弃 P 帧直到第一个 IDR;关键帧前重新拼接 `[SPS][PPS][IDR]`,保证对端首帧可独立解码 - **水印**:`WatermarkRenderer` 统一绘制(顶部半透明底条 + 白字,多行机构信息 + 实时时间戳),预览与编码两路共用, 实时开关;Qt 5.14+ 走 `Format_BGR888` 零拷贝,5.12 走 RGB 转换回退;字号按帧高缩放(12~54 px) - **日志**:`Logger` 分级 ANSI 彩色边框日志,支持多行 `BoxBuilder` 流式 API - **线程隔离**:采集 / 预览 / 编码 / 推流各自独立线程,帧通路不经 GUI 主线程 ## 目录结构 ``` RtspPusherClient ├── main.cpp 入口(QApplication::exec) ├── PusherApplication.* 应用启动(Fusion 风格、跨线程元类型注册、窗口居中) ├── CMakeLists.txt Qt5 / OpenCV / FFmpeg 依赖与链接配置(C++14) ├── ui/ │ ├── MainWindow.* 主窗口(线程装配、URL 输入、推流按钮、水印开关) │ └── MainWindow.ui ├── video/ │ ├── FrameCapture.* 相机采集(采集线程,V4L2 / MJPG / 帧率统计) │ ├── PreviewWorker.* 本地预览(预览线程,480P 缩放 + 水印) │ ├── FrameEncoder.* H.264 编码(编码线程,色彩转换 + 水印 + x264) │ └── WatermarkRenderer.* 水印渲染器(线程安全,Qt 5.14+ / 5.12 双路径) ├── rtsp/ │ ├── RtspStreamWorker.* 推流工作对象(推流线程,Qt 信号入口 + 状态转发) │ └── RtspManager.* RTSP 输出上下文管理、参数集缓存、帧分发、起停推流 └── utils/ ├── H264Splitter.* AnnexB / AVCC 探测与转换、NALU 拆分 ├── Logger.* 分级边框日志 └── GlobalDefinition.hpp 全局常量、状态码、NALU / FrameFormat ``` ## 数据流向图 ```mermaid flowchart TD subgraph T1["采集线程"] A["USB 相机 V4L2 / MJPG"] B["FrameCapture 循环 read"] C["信号 frameCaptured
shared_ptr<cv::Mat>"] C2["信号 captureFpsUpdated"] end subgraph T2["预览线程"] D["PreviewWorker 环形缓冲 3 帧"] E["WatermarkRenderer 水印"] F["cv::resize 854x480
BGR 转 RGB"] G["信号 onPreviewReady
QImage.copy"] end subgraph T3["编码线程"] H["FrameEncoder 环形缓冲 3 帧
未推流时直接丢弃"] I["WatermarkRenderer 水印
机构信息 + 实时时间"] J["sws_scale BGR 转 YUV420P"] K["libx264 编码
send/receive packet"] L["信号 onFrameEncode
H264 + 墙钟 PTS"] L2["信号 onOutputFormatChanged
extradata SPS/PPS"] end subgraph T4["推流线程"] M["RtspStreamWorker::onEncodedFrame"] N["H264Splitter 探测格式
AVCC 转 AnnexB"] O["RtspManager::writeVideoFrame
重组 SPS/PPS/IDR
等待首个 IDR"] P["av_interleaved_write_frame"] end subgraph T5["主线程 GUI"] W["MainWindow videoPreview 显示"] X["fps 标签 / URL 输入 / 推流按钮 / 水印开关"] Y["onOutputFormatChanged
cacheParameterSets"] end Q["外部 RTSP 服务器
如 ZLMediaKit :554"] R["播放端 ffplay / VLC"] A --> B --> C B --> C2 --> W C -->|QueuedConnection| D C -->|QueuedConnection| H D --> E --> F --> G --> W X -.->|水印开关| E X -.->|水印开关| I H --> I --> J --> K --> L K --> L2 --> Y L -->|跨线程信号| M --> N --> O --> P --> Q --> R Y -.->|InitSuccess| O ``` ## 线程模型 | 线程 | 载体 | 职责 | |------------|--------------------|----------------------------------------------------| | 主线程 GUI | `MainWindow` | 界面显示、URL 输入、推流按钮、水印开关、参数集转存 | | 采集线程 | `FrameCapture` | 循环 `read()` 相机帧,统计实际帧率 | | 预览线程 | `PreviewWorker` | 水印、缩放、色彩转换,发出 `QImage` 给 UI 显示 | | 编码线程 | `FrameEncoder` | 水印、像素格式转换、x264 编码、回传 `extradata` | | 推流线程 | `RtspStreamWorker` | 格式探测与转换、调用 `RtspManager`、状态回传 | - 采集帧通过 `Qt::QueuedConnection` 分发给预览与编码两个消费者,互不阻塞。 - 编码线程与推流线程之间走 Qt 队列信号,不经过 GUI 主线程,避免界面卡顿影响帧通路。 - `RtspManager` 为单例(`RtspManager::get()`),内部用 `std::mutex` 保护输出上下文与 SPS/PPS 缓存; 其状态回调经 `RtspStreamWorker`(`QPointer` 弱引用保护)转成 Qt 信号回到主线程。 - 退出时 `MainWindow` 析构函数按 **采集 → 预览 → 编码 → 推流**顺序 `quit()` + `wait()`。 ## 推流状态机 `RtspManager` 通过 `StatusCallback` 上报状态(`utils/GlobalDefinition.hpp` 的 `RtspStatus`): | 状态 | 含义 | 界面表现 | |-----------------|--------------------------------------|----------------------------------| | `InitSuccess` | SPS/PPS 已缓存,参数就绪 | 触发 `initialize()` 连接服务器 | | `InitFailed` | 参数集解析失败 / 建流失败 / 握手失败 | 仅记日志 | | `StreamStarted` | 输出头已写出,推流进行中 | 按钮恢复可用,文字变「停止推流」 | | `StreamFailed` | 推流过程异常 | 按钮文字恢复「开始推流」 | | `Released` | 已释放输出上下文并清空参数集 | 按钮文字恢复「开始推流」 | ## 启动流程 1. 界面输入框填写目标 RTSP 地址(默认 `rtsp://127.0.0.1:554/camera/live`),点击「开始推流」。 2. URL 非空校验通过后调 `FrameEncoder::setEncodingEnable(true)`,按钮暂时禁用,编码线程开始真正编码。 3. 编码首帧前通过 `onOutputFormatChanged` 把 `extradata`(SPS/PPS)回传主线程。 4. 主线程调 `RtspManager::cacheParameterSets()` 拆出 SPS/PPS,成功则回调 `InitSuccess`。 5. `MainWindow::onStatusChanged()` 收到 `InitSuccess` 后调 `RtspManager::initialize(url, VIDEO_FPS)`: 建输出上下文 → 建视频流(`time_base = 1/fps`)→ 写入 `[SPS][PPS]` 形式的 `extradata` → `avio_open2` → `avformat_write_header`。 6. 握手完成回调 `StreamStarted`,按钮恢复可用并变为「停止推流」。 7. `RtspManager` 丢弃 P 帧直到首个 IDR,之后才把 AnnexB 访问单元写入 muxer。 停止流程:点击「停止推流」→ `setEncodingEnable(false)` + `RtspManager::release()` (`av_write_trailer` → 关闭 AVIO → 释放上下文 → 清空 SPS/PPS),回调 `Released`,按钮复位。 再次开始时 `setEncodingEnable(true)` 会重置 `_isFormatSent`,重新回传一次 `extradata`。 ## 参数速查 | 参数 | 值 | 位置 | |-----------------------|------------------------------------|---------------------------------------| | 帧率 `VIDEO_FPS` | 30 | `utils/GlobalDefinition.hpp` | | 采集 / 编码分辨率 | 1920×1080 | `FRAME_WIDTH` / `FRAME_HEIGHT` | | 码率 `VIDEO_BIT_RATE` | 4500000 (4.5 Mbps) | `utils/GlobalDefinition.hpp` | | GOP / B 帧 | `VIDEO_FPS / 2` = 15 / 0 | `video/FrameEncoder.cpp` | | 推流编码预设 | `ultrafast` + `zerolatency` | `video/FrameEncoder.cpp` | | 默认相机索引 | 0(`/dev/video0`) | `FrameCapture(int index = 0)` | | 编码环形缓冲 | 3 帧 | `FrameEncoder(size_t bufferSize = 3)` | | 预览环形缓冲 | 3 帧 | `PreviewWorker(w, h, bufferSize = 3)` | | 预览分辨率 | 854×480 | `MainWindow::initFramePreview()` | | 默认推流地址 | `rtsp://127.0.0.1:554/camera/live` | `ui/MainWindow.ui`(`lineEdit`) | | RTSP 传输 / 超时 | TCP / `stimeout=5000000` | `rtsp/RtspManager.cpp` | | 音视频数据包 PTS | 墙钟微秒重采样到 `1/fps` | `RtspManager::writePacketLocked()` | | 单包 duration | 0 | `RtspManager::writePacketLocked()` | | 水印字体 / 基准字号 | Noto Sans CJK SC / 12 px @480 | `video/WatermarkRenderer.cpp` | > 编码器输入 `pts` 用自增帧序号,但送到 RTSP 的 packet `pts` 用系统墙钟(`dts = pts`),保证时间戳单调递增。 水印文案(机构信息与地址)在 `ui/MainWindow.cpp` 构造函数中硬编码,按需修改: ```cpp std::vector lines{}; lines.emplace_back("中国xxxxxxxxxx研究所"); lines.emplace_back("北京市海淀区"); ``` ## 构建 ### 依赖(Ubuntu) ```bash sudo apt-get install build-essential cmake pkg-config \ qtbase5-dev libopencv-dev \ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \ libssl-dev fonts-noto-cjk ``` H.264 编码器由系统 FFmpeg 内置的 libx264 提供(`avcodec_find_encoder(AV_CODEC_ID_H264)`), 不需要额外链接 `libx264`,但要求系统 FFmpeg 编译时启用了 `--enable-libx264` (Ubuntu 官方包默认启用)。 ### 构建工程 ```bash cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build -j ``` 产物为 `build/RtspPusherClient`,也可直接用 CLion 打开工程构建。 ## 运行 1. 先准备一台 RTSP 服务器接收推流,例如 ZLMediaKit: ```bash docker run -id --name zlm -p 554:554 -p 1935:1935 -p 80:80 \ zlmediakit/zlmediakit:master ``` 2. 启动 `RtspPusherClient`,在输入框填 `rtsp://<服务器IP>:554/camera/live`,点击「开始推流」。 3. 拉流验证: ```bash ffplay -fflags nobuffer -flags low_delay -framedrop \ -rtsp_transport tcp rtsp://127.0.0.1:554/camera/live ``` 未推流时相机仍在采集并本地预览,但不编码、不推流。程序启动后会在日志中打印相机的 实际输出格式、分辨率与帧率,便于核对采集参数。 ## 注意事项 - 采集后端写死 `cv::CAP_V4L2`,当前面向 **Linux(Ubuntu)**;Windows 需自行替换采集实现。 - 切换推流地址请先「停止推流」再修改 URL 重新开始,`RtspManager::initialize()` 在已推流时会忽略重复调用。 - 开始推流过程中按钮会被禁用,直到收到 `StreamStarted`;若握手失败(`InitFailed`)按钮将保持禁用, 需要检查服务器地址与连通性后重启程序。 - 窗口尺寸在 `MainWindow.ui` 中固定为 874×540,并隐藏了最大化按钮。 - `Logger` 直接输出 ANSI 彩色转义序列,在不支持虚拟终端的终端里会看到转义字符。 ## 编码规范 - 文件与类:大驼峰(`FrameEncoder`、`RtspManager`、`WatermarkRenderer`) - 函数、变量、参数:小驼峰(`writeVideoFrame`、`avccToAnnexB`、`ptsUs`) - 私有成员:前导下划线 + 小驼峰(`_frameBuffer`、`_codecContextPtr`),指针以 `Ptr` 结尾 - 编译期常量:`k` 前缀小驼峰(`kRefHeight`、`kRefFontSize`) - 宏与全局常量:全大写下划线(`VIDEO_FPS`、`LOG_TAG`、`FRAME_WIDTH`) - Qt / FFmpeg / OpenCV 的框架与库符号保持原名(`avcodec_send_frame`、`moveToThread` 等)