简介:本资源面向具备一定C++与Qt基础的开发者,提供一套基于Qt、RTSP协议与FFmpeg库实现的视频流播放器完整工程,可用于远程监控、视频会议、在线教育等需要多路实时视频播放的场景。工程重点实现三通道视频流同时播放与截图功能,通过多个QMediaPlayer实例分别绑定不同RTSP流地址,并借助信号与槽机制统一控制播放、暂停等操作,同时调用saveSnapshot()完成当前帧抓取保存。资源包共123个文件,约11.03MB,包含8个cpp源文件、83个h头文件、1个ui界面文件、1个pro工程文件及qss样式文件,另有5个dll动态库与5个a静态库用于支撑FFmpeg相关解码依赖,并附带可执行exe与编译中间产物,便于直接运行与二次开发。目前已有5526人学习下载,适合希望掌握Qt多媒体集成、FFmpeg解码调用与多通道视频同步控制的开发者参考实践。
1. Qt 做 RTSP 播放器:为什么很多人卡在“能连上但播不动”
如果你在 Qt 里用 QMediaPlayer 直接 setUrl 一个 RTSP 地址,大概率会遇到两种结局:要么窗口一片黑,要么日志里刷一堆 GStreamer 警告然后进程卡死。这不是 Qt 的锅,而是 RTSP 本身就不是一个“给播放器一个 URL 就能出画面”的协议——它需要先 DESCRIBE 拿到 SDP,再 SETUP 建传输通道,再 PLAY 才开始推流,中间还夹着 RTP 解包、时间戳对齐、解码器协商。Qt 自带的多媒体模块在桌面端底层走的是系统框架(Linux 上通常是 GStreamer),版本差异和插件缺失会让这条链路随时断掉。
所以真正能落地的方案,通常不是“用 Qt 的播放器”,而是“用 Qt 做界面和事件循环,用 FFmpeg 或 Live555 做协议与解码,再把帧交给 QWidget 或 QOpenGLWidget 渲染”。这篇笔记就按这个思路走:先讲清楚为什么这么选型,再给出一套能跑通的最小实现,最后把参数、线程模型和踩坑点摊开。适合已经会写 Qt 界面、但被 RTSP 卡住的开发者,也适合想从零搭一个可控播放器的同学。
2. 选型与链路拆解:Qt 负责什么,FFmpeg 负责什么
2.1 为什么不用 QMediaPlayer 直接播 RTSP
QMediaPlayer 在 Windows 上走 Media Foundation,在 Linux 上走 GStreamer,在 macOS 上走 AVFoundation。这三个后端对 RTSP 的支持程度完全不一样,而且 Qt 版本一换,行为就可能变。更麻烦的是,你没法控制缓冲策略、超时时间、重连逻辑,一旦网络抖动,播放器直接进入错误状态,连“重试”都要自己写。
常见做法是:Qt 只做 UI 和事件分发,FFmpeg 负责 avformat_open_input、av_read_frame、avcodec_send_packet/receive_frame 这一整套。这样你拿到的是原始 YUV 或 RGB 帧,想怎么渲染就怎么渲染,想加 AI 分析、想录像、想截图,都不受播放器框架限制。
2.2 最小链路:从 RTSP URL 到 QImage
整条链路可以拆成五步:
- avformat_open_input 打开 RTSP 地址,内部完成 OPTIONS/DESCRIBE/SETUP/PLAY。
- avformat_find_stream_info 拿到流信息,找到视频流索引。
- 用 avcodec_find_decoder 找到对应解码器,打开解码上下文。
- 循环 av_read_frame 读包,avcodec_send_packet 送进解码器,avcodec_receive_frame 取帧。
- 把 AVFrame 转成 QImage,通过信号槽发给 UI 线程显示。
这里的关键是第 4 步和第 5 步必须分线程。解码是阻塞的,放在 UI 线程里界面必卡。我一般会用一个 QThread 包住整个 FFmpeg 循环,解出帧后 emit 一个 signal,UI 侧只做 convert 和 paint。
2.3 环境准备与依赖版本
Linux 下用包管理器装 FFmpeg 开发库即可,Windows 下建议用 MSVC 编译好的 shared 包,把 include 和 lib 路径配进 .pro 或 CMakeLists。Qt 这边只需要 Core、Gui、Widgets 三个模块,如果要用 OpenGL 渲染再加 OpenGL。
# Ubuntu/Debian 常见安装方式 sudo apt install libavformat-dev libavcodec-dev libavutil-dev libswscale-dev# CMakeLists.txt 片段:链接 FFmpeg find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libavutil libswscale) target_link_libraries(MyPlayer Qt5::Core Qt5::Gui Qt5::Widgets ${FFMPEG_LIBRARIES} ) target_include_directories(MyPlayer PRIVATE ${FFMPEG_INCLUDE_DIRS})参数说明:libavformat 负责协议和容器,libavcodec 负责解码,libswscale 负责像素格式转换。少链接一个,编译能过但运行时会崩在符号找不到。
3. 动手实现:一个能跑通的最小 RTSP 播放器
3.1 解码线程的骨架代码
下面这段是解码线程的核心,省略了错误处理细节,但关键调用都在。
// DecoderThread.h class DecoderThread : public QThread { Q_OBJECT public: explicit DecoderThread(QObject *parent = nullptr); void setUrl(const QString &url); void stop(); signals: void frameReady(const QImage &img); void errorOccurred(const QString &msg); protected: void run() override; private: QString m_url; std::atomic<bool> m_running{false}; };// DecoderThread.cpp 核心循环 void DecoderThread::run() { avformat_network_init(); AVFormatContext *fmtCtx = nullptr; // 关键参数:rtsp_transport 用 tcp 更稳,stimeout 单位微秒 AVDictionary *opts = nullptr; av_dict_set(&opts, "rtsp_transport", "tcp", 0); av_dict_set(&opts, "stimeout", "5000000", 0); // 5 秒超时 if (avformat_open_input(&fmtCtx, m_url.toUtf8().constData(), nullptr, &opts) < 0) { emit errorOccurred("open input failed"); return; } if (avformat_find_stream_info(fmtCtx, nullptr) < 0) { emit errorOccurred("find stream info failed"); return; } int videoIdx = av_find_best_stream(fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); AVCodecParameters *codecPar = fmtCtx->streams[videoIdx]->codecpar; const AVCodec *decoder = avcodec_find_decoder(codecPar->codec_id); AVCodecContext *codecCtx = avcodec_alloc_context3(decoder); avcodec_parameters_to_context(codecCtx, codecPar); avcodec_open2(codecCtx, decoder, nullptr); SwsContext *swsCtx = sws_getContext( codecCtx->width, codecCtx->height, codecCtx->pix_fmt, codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB32, SWS_BILINEAR, nullptr, nullptr, nullptr); AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); m_running = true; while (m_running && av_read_frame(fmtCtx, pkt) >= 0) { if (pkt->stream_index == videoIdx) { avcodec_send_packet(codecCtx, pkt); while (avcodec_receive_frame(codecCtx, frame) == 0) { QImage img(codecCtx->width, codecCtx->height, QImage::Format_RGB32); uint8_t *dst[4] = { img.bits(), nullptr, nullptr, nullptr }; int dstStride[4] = { img.bytesPerLine(), 0, 0, 0 }; sws_scale(swsCtx, frame->data, frame->linesize, 0, codecCtx->height, dst, dstStride); emit frameReady(img.copy()); } } av_packet_unref(pkt); } // 释放顺序:sws -> codecCtx -> fmtCtx sws_freeContext(swsCtx); avcodec_free_context(&codecCtx); avformat_close_input(&fmtCtx); av_packet_free(&pkt); av_frame_free(&frame); }逻辑说明:avformat_open_input 的 opts 里 rtsp_transport 设成 tcp 是为了避开 UDP 丢包导致的马赛克;stimeout 控制阻塞超时,避免网络断了线程卡死。av_read_frame 返回负值就退出循环,实际项目里应该区分 EAGAIN 和真正的错误。sws_scale 把解码后的 YUV 转成 RGB32,方便直接构造 QImage。emit 之前 copy() 是必须的,因为 frame 的数据在下次 receive 时会被复用。
3.2 UI 侧接收帧与刷新
UI 侧只需要一个 QLabel 或自定义 QWidget,把 frameReady 信号连到槽里,setPixmap 或 update()。
// MainWindow.cpp 片段 connect(decoder, &DecoderThread::frameReady, this, [this](const QImage &img){ m_label->setPixmap(QPixmap::fromImage(img).scaled( m_label->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); }); connect(decoder, &DecoderThread::errorOccurred, this, [this](const QString &msg){ m_statusBar->showMessage(msg); });参数说明:scaled 里用 FastTransformation 是为了降低 UI 线程开销,如果追求画质可以换 SmoothTransformation,但帧率高时会吃 CPU。setPixmap 每次都会触发重绘,如果发现界面卡,可以改成自定义 widget 的 paintEvent 里 drawImage。
3.3 关键参数表:哪些值改了就翻车
| 参数 | 常用值 | 作用 | 改错的后果 |
|---|---|---|---|
| rtsp_transport | tcp / udp | 传输层协议 | udp 丢包花屏,tcp 延迟略高 |
| stimeout | 5000000 | 微秒级超时 | 太小频繁断连,太大卡死无响应 |
| probesize | 默认 | 探测流信息大小 | 太小找不到流,太大启动慢 |
| max_delay | 500000 | 最大缓冲延迟 | 影响首帧时间和抗抖动 |
| threads | auto / 1 | 解码线程数 | 多线程可能增加延迟 |
我一般会在打开前把 rtsp_transport 设成 tcp,stimeout 设 5 秒,probesize 不动。如果摄像头是低功耗设备,首帧慢,可以把 probesize 调小到 100000 左右试试。
4. 避坑与排查:RTSP 播放器最常见的 5 个翻车现场
4.1 现象:avformat_open_input 一直阻塞不返回
原因:默认的 RTSP 超时很长,网络不通时线程卡死。解决:在 AVDictionary 里设 stimeout,并且把 open_input 放在独立线程里,UI 侧加一个“正在连接”状态。如果还是卡,可以再设 timeout 参数(单位微秒),但注意不同 FFmpeg 版本对这两个 key 的支持有差异。
4.2 现象:能播但画面花屏、马赛克
原因:UDP 传输丢包,或者解码器没收到完整帧。解决:把 rtsp_transport 改成 tcp;如果必须用 UDP,可以开 reorder_queue_size 和 max_delay 来抗抖动。另外检查 sws_scale 的 linesize 是否对齐,QImage 的 bytesPerLine 和 FFmpeg 的 linesize 不一致时会出现斜纹。
4.3 现象:播放几秒后卡住,日志显示 receive_frame 返回 EAGAIN
原因:解码器内部缓冲满了,或者送包速度超过解码速度。解决:在 send_packet 之前判断返回值,如果是 EAGAIN 就先 receive_frame 把帧取出来再送。不要无脑循环 send,否则缓冲区溢出后直接丢包。
4.4 现象:界面卡顿,拖动窗口无响应
原因:解码和渲染在同一个线程,或者 QImage 转换太频繁。解决:解码线程只负责 emit 帧,UI 线程用定时器或信号槽异步刷新。如果帧率很高,可以在解码线程里做丢帧策略,比如只保留最新一帧,中间的直接跳过。
4.5 现象:程序退出时崩溃在 avcodec_free_context
原因:解码线程还在跑,资源就被释放了。解决:stop() 里先把 m_running 置 false,然后 wait() 等线程结束,再释放 FFmpeg 资源。不要在析构函数里直接 delete 线程对象,顺序错了必崩。
5. 进阶技巧:低延迟与多路播放的取舍
5.1 把延迟压到 200ms 以内的三个调整
第一,把 rtsp_transport 设成 tcp 后,再设 max_delay 为 200000(0.2 秒),让 FFmpeg 不要缓冲太多。第二,解码线程里收到帧就立刻 emit,不要攒批。第三,UI 侧用 QOpenGLWidget 代替 QLabel,把 RGB 转换放到 GPU 里做,省掉 sws_scale 的 CPU 开销。
// 低延迟参数组合 av_dict_set(&opts, "rtsp_transport", "tcp", 0); av_dict_set(&opts, "max_delay", "200000", 0); av_dict_set(&opts, "stimeout", "3000000", 0); av_dict_set(&opts, "fflags", "nobuffer", 0);fflags 设 nobuffer 会减少内部缓冲,但网络抖动时更容易花屏,适合局域网场景。公网环境建议保留一定缓冲,否则体验更差。
5.2 多路 RTSP 同时播放的线程模型
如果要做 4 路或 9 路,不要每路开一个 QThread 然后各自 emit 到 UI。更稳的做法是:每路一个解码线程,但帧统一发到一个 FrameQueue,UI 侧用一个定时器按固定帧率从队列取帧渲染。这样即使某一路卡了,也不会拖垮整个界面。
| 方案 | 线程数 | 优点 | 缺点 |
|---|---|---|---|
| 每路一线程直连 UI | N+1 | 实现简单 | 路数多时 UI 压力大 |
| 解码线程 + 帧队列 | N+1 | 解耦,抗卡顿 | 需要管理队列长度 |
| 线程池 + 轮询 | 固定 | 资源可控 | 实现复杂,调试难 |
我一般用第二种,队列长度设 3 到 5 帧,超过就丢最旧的。这样内存不会涨,延迟也可控。
5.3 验证方法:用命令行先确认流本身没问题
在写代码之前,先用 ffplay 确认 RTSP 地址能播。如果 ffplay 都播不了,Qt 里再怎么调也没用。
ffplay -rtsp_transport tcp -i rtsp://your_url如果 ffplay 能播但你的程序不能,优先检查解码线程的退出逻辑和资源释放顺序。如果 ffplay 也花屏,那就是网络或摄像头本身的问题,跟 Qt 无关。
最后说一个我自己的习惯:每次改完解码参数,先跑 10 分钟压力测试,看内存有没有缓慢增长。RTSP 播放器最容易出的问题不是第一次打不开,而是跑几个小时后崩在某个没释放的 AVPacket 上。希望帮到你。
本文还有配套的精品资源,点击获取