# 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` 等)