简介:面向 Qt 与流媒体开发者的 RTSP 取流工程,基于 FFmpeg 完成视频流拉取、解码与界面显示,适合需要快速实现播放器或实时监控预览的读者。压缩包共 158 个文件,约 18.78MB,包含可编译的 Qt 工程(.pro/.cpp/.ui)以及配套的 FFmpeg 开发库(dll、lib、a、def)和 115 个头文件,可直接将库与源码对接进自己的项目。工程中 mainwindow、videoplayer 等模块展示了 RTSP 会话建立、数据包读取、解码并转换为 QImage 显示的关键流程,对理解 FFmpeg API 与 Qt 的事件循环配合很有帮助。同时附带了 9 个库文件与 8 个动态库,兼顾静态链接与运行时部署需求。目前已有 831 人浏览学习,适合有一定 C++ 基础的开发者在实际工程中参考模仿、排错与二次开发。
1. 当 Qt 遇上 FFmpeg:一份能跑的 RTSP 取流工程该长什么样
手头这份qt_ffmpeg_rtsp工程,打开压缩包就是一堆.dll.a静态导入库和两个 cpp 文件,看起来寒酸,但这正是 Qt + FFmpeg 做 RTSP 取流最实在的骨架。视频监控、远程画面回显、无人值守设备预览,这类需求的实际下场就是从这段代码起步的。工程里集成了 libavcodec、libavformat、libswscale 等 FFmpeg 核心库,用videoplayer.cpp和mainwindow.cpp把拉流、解码、显示串成了一条完整链路。适合两类人:一是刚接触流媒体、想快速搭起一个能出画面的播放器的 Qt 开发者;二是被 ffmpeg 命令行弄得够够的,想自己控制每一帧数据的嵌入式或桌面端工程师。这份资源能解决的核心问题是:怎么在自己的 Qt 窗口里拉 RTSP 流、解码、显示,并且中途不崩、结束时干净退出。接下来我会把库的配置、取流流程、显示刷新和断线重连的细节一步步拆开。
2. 把 FFmpeg 塞进 Qt 工程:静态库配置与四类常见链接报错
2.1 这份工程拿了哪些库,分别干什么
很多人在 ready-made 项目里会看懵掉,libavcodec.dll.a、libavformat.dll.a、libavutil.dll.a,这些到底是什么角色。简单分一下工:libavformat负责打开 RTSP 流、解析封装格式、按包读取数据;libavcodec负责解码,也就是把 H.264/H.265 的压缩数据变成 YUV 原始帧;libavutil是公共工具库,提供内存分配、日志、常用数学函数,算是前两者的地基;libswscale做像素格式和分辨率的转换,比如把解码出的 YUV420P 转成 RGB32 给 QImage 用;libswresample处理音频重采样,这个工程如果只看视频的话它属于备用;libpostproc做后处理,一般也用不上。从工程保留的.dll.a后缀能看出,这是一套 MinGW 环境下的动态库导入库,也就是编译时链接、运行时加载 DLL 的模式。配置 Qt 工程时,.pro文件里要做这样几步:指定 FFmpeg 头文件所在的 INCLUDEPATH,指定库文件所在的 LIBS,并且引入对应的 DLL 到运行目录。常见的做法是把 FFmpeg 的bin目录整个拷进工程目录,再在.pro里用相对路径引用,这样换机器时只要把工程整包带走就能跑,不用改环境变量。
2.2 一个能通过编译的.pro文件长什么样
在 Qt Creator 里新建一个 Widgets 项目,把videoplayer.cpp和mainwindow.cpp替换进来,头文件按你现有目录结构放好后,.pro文件这样写就能把上面的库全部接进来:
QT += core gui widgets TARGET = RtspPlayer TEMPLATE = app CONFIG += c++11 INCLUDEPATH += $$PWD/ffmpeg/include LIBS += -L$$PWD/ffmpeg/lib \ -lavcodec \ -lavformat \ -lavutil \ -lswscale \ -lswresample \ -lavdevice \ -lavfilter \ -lpostproc SOURCES += \ main.cpp \ mainwindow.cpp \ videoplayer.cpp HEADERS += \ mainwindow.h \ videoplayer.h这里-L指定库所在目录,-l指定库名,注意 FFmpeg 的库名规则是去掉lib前缀和.a后缀。$$PWD表示工程文件所在路径,用相对路径而不是绝对路径是换机器不翻车的关键。如果你是 MSVC 编译器,库文件会是.lib格式,引用方式相同,但头文件里有时需要加extern "C"防止名字修饰导致链接失败;MinGW 通常不需要。另外,libavdevice和libavfilter在纯 RTSP 播放场景用不到,但如果后续想做拉流转封装或加滤镜,留着能少动一次工程的.pro。配置完先编译一次,看到程序能起来、窗口能弹出来,再进行下一步。此时即使没接 RTSP 地址,也能确认 FFmpeg 库是否被正确链接进来,因为 Qt 程序启动时会加载这些 DLL,缺了它会直接报“找不到 libavcodec.dll”之类的错。
2.3 MinGW 与 MSVC 的坑位区别
这一类工程报错,“fatal: cannot mix incompatible qt library”和“cannot find -lxxx”是两个最典型的现象。前者是 Qt 库与编译器不匹配,比如用 MSVC 版 Qt 去链 MinGW 编译的 FFmpeg 库;后者则是库文件路径不对或库名写错。在动手改代码之前,先把工具链理清楚。工程里给的是.dll.a格式,那么大概率是用 MinGW 的 Qt 套件在跑。建议直接用 Qt Creator 自带的 MinGW 套件打开.pro文件,编译器、Qt 库、FFmpeg 库三者必须同一套工具链。如果你的 Qt 是 MSVC 版,又只有这套 MinGW 的 FFmpeg 库,那就去下载对应 MSVC 版本的 FFmpeg 开发包,或者反过来装 MinGW 版 Qt。这块没有捷径,硬混的后果就是连续报几十个 undefined reference,每一个都指向avformat_open_input或av_read_frame这类 FFmpeg 函数。
3. RTSP 取流的完整链路:从 avformat_open_input 到持续读包
3.1 初始化只做一次,但顺序不能乱
RTSP 取流的第一步是初始化 FFmpeg 的全局环境和网络模块。老版本的 FFmpeg 要求调用av_register_all()注册所有容器格式和解码器,这个函数在新版本里已经废弃了,因为新版本在编译时默认启用了自动注册。但是,avformat_network_init()在 Windows 平台依然需要显式调用,否则avformat_open_input()在解析rtsp://前缀时可能直接返回失败。因此,这段初始化代码要在程序启动、打开任何流之前执行,放到 main 函数里最稳妥:
#include <QApplication> #include "mainwindow.h" extern "C" { #include <libavformat/avformat.h> } int main(int argc, char *argv[]) { QApplication a(argc, argv); avformat_network_init(); MainWindow w; w.show(); return a.exec(); }逻辑说明:avformat_network_init()是给网络协议模块做的内部初始化,RTSP 底层会用到 socket,不初始化的话在 Windows 上可能解析不出rtsp协议。放在QApplication创建之后、窗口显示之前就行,Qt 的事件循环不影响 FFmpeg 的网络行为。参数说明:这里没有参数可调,唯一要注意的是extern "C"包住 FFmpeg 头文件,因为 FFmpeg 是 C 语言库,C++ 编译器链接时需要用 C 链接规范找到符号。如果你的工程编译通过且能正常调用avformat_open_input,说明这段没问题。
3.2 打开 RTSP 流与探测格式
流地址是用户输入的,也可能是程序内置的摄像头地址,但打开流程是同一套。用avformat_open_input()打开流,然后用avformat_find_stream_info()拉取流详情,比如视频编码、分辨率、帧率:
AVFormatContext *fmtCtx = nullptr; int ret = avformat_open_input(&fmtCtx, url.toStdString().c_str(), nullptr, nullptr); if (ret < 0) { qDebug() << "open input failed:" << ret; return; } ret = avformat_find_stream_info(fmtCtx, nullptr); if (ret < 0) { qDebug() << "find stream info failed:" << ret; avformat_close_input(&fmtCtx); return; } int videoStreamIdx = av_find_best_stream(fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (videoStreamIdx < 0) { qDebug() << "no video stream"; avformat_close_input(&fmtCtx); return; }逻辑说明:avformat_open_input()只负责把连接建立起来,这时候 RTSP 的 DESCRIBE 和 SETUP 请求已经完成,但媒体信息还没拉全。avformat_find_stream_info()会读取流中的数据包,分析出编码格式、宽高、帧率等信息,这一步对后续选解码器至关重要。av_find_best_stream()帮我们选出最佳的视频流索引,如果有多个视频流时不用自己遍历。参数说明:avformat_open_input的第三个参数是AVInputFormat指针,传nullptr表示让 FFmpeg 根据 URL 前缀自动探测;第四个参数是AVDictionary选项,可以设置rtsp_transport为tcp或udp,默认是 UDP。对于摄像头画面,UDP 容易丢包导致花屏,我一般会在这里指定 TCP,如果后续发现延迟偏高再评估是否需要回调。
3.3 解码器打开与帧的获取
有了流索引,就可以查对应的解码器并打开它。注意这里要用avcodec_parameters_to_context()把流参数复制给解码上下文,这在较新的 FFmpeg 版本里是标准做法:
AVCodecParameters *codecParams = fmtCtx->streams[videoStreamIdx]->codecpar; AVCodec *codec = avcodec_find_decoder(codecParams->codec_id); if (!codec) { qDebug() << "decoder not found"; return; } AVCodecContext *codecCtx = avcodec_alloc_context3(codec); avcodec_parameters_to_context(codecCtx, codecParams); if (avcodec_open2(codecCtx, codec, nullptr) < 0) { qDebug() << "open codec failed"; avcodec_free_context(&codecCtx); return; }逻辑说明:avcodec_find_decoder()根据codec_id找到对应解码器,H.264 返回的通常是libx264的软解实现,H.265 也能找到,只要 FFmpeg 编译时带了对应解码器。avcodec_alloc_context3()分配解码器上下文,avcodec_parameters_to_context()把codecpar里的编码信息同步进去,比如 SPS/PPS、宽高、像素格式。这一步漏掉的话,avcodec_open2()可能报codec parameters missing。之后就进入读取循环:av_read_frame()不断从流中读取 AVPacket,判断包属于视频流后,丢给avcodec_send_packet()和avcodec_receive_frame(),这是新版本 FFmpeg 推荐的解码接口,代替了老版的avcodec_decode_video2()。每次拿到AVFrame后,再交给下一层的转换和显示。
4. 从 AVFrame 到 QImage:像素格式转换与定时器刷新
4.1 为什么不能直接把 YUV 丢给 QLabel
FFmpeg 解码出来的AVFrame默认是 YUV420P 格式,而 Qt 的 QImage 支持的是 RGB32、RGB888、ARGB32 这类格式。直接把 YUV 数据塞进 QImage 会得到一坨颜色完全错乱的画面,或者直接花屏。解决方案是用libswscale做一次像素格式转换,把 YUV420P 转成 RGB888。这个转换不仅在格式上做搬运,还会处理分辨率对齐、字节对齐的问题。sws_getContext()创建转换上下文,sws_scale()执行转换,每次解码出一帧就调用一次。考虑到效率,上下文只创建一次,后续每一帧复用。转换后的数据装进一个QImage,再触发界面刷新。这里有个常见误区:有些人为了省事,在每次拿到帧时都临时建一个 QImage,反复拷贝导致内存碎片和性能下降。正确的做法是把目标缓冲区开好,每次只做局部 memcpy 更新。
4.2 转换与显示的代码实现
我用一个自定义的VideoPlayer类来管理解码循环和画面更新,Qt 里用 QTimer 驱动取帧和刷新,这样不会阻塞 UI 线程:
// videoplayer.cpp 节选 bool VideoPlayer::initSws(AVCodecContext *codecCtx) { swsCtx = sws_getContext( codecCtx->width, codecCtx->height, codecCtx->pix_fmt, codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); return swsCtx != nullptr; } QImage VideoPlayer::frameToImage(AVFrame *frame) { int width = codecCtx->width; int height = codecCtx->height; if (rgbBuffer.isEmpty()) { rgbBuffer.resize(av_image_get_buffer_size(AV_PIX_FMT_RGB24, width, height, 1)); } uint8_t *dstData[1] = { reinterpret_cast<uint8_t *>(rgbBuffer.data()) }; int dstLinesize[1] = { width * 3 }; sws_scale(swsCtx, frame->data, frame->linesize, 0, height, dstData, dstLinesize); QImage img(reinterpret_cast<uchar *>(rgbBuffer.data()), width, height, QImage::Format_RGB888); return img.copy(); }逻辑说明:sws_getContext的第一个参数组是输入宽高和像素格式,第二组是输出宽高和像素格式,这里输入输出分辨率保持一致,只做格式转换,不做缩放。SWS_BILINEAR是双线性插值算法,缩放质量中等、速度较快;如果将来要缩小显示,可以改成SWS_LANCZOS提升清晰度,代价是 CPU 占用上升。rgbBuffer用QByteArray管理,大小由av_image_get_buffer_size计算,保证字节对齐。sws_scale调用时dstData和dstLinesize是输出缓冲区的数据指针和每行字节数,RGB24 的每行字节数就是宽乘 3。最后用rgbBuffer.data()构造 QImage,注意此时 QImage 引用的是内部 buffer,copy()是必须的,否则原 buffer 下次被覆盖,Qt 侧显示的就是脏数据。参数说明:dstLinesize必须等于width * 3,因为 RGB24 不涉及额外对齐;如果改成 RGB32,则要乘 4,且QImage::Format_RGB32要和数据格式严格对应。
4.3 QTimer 驱动刷新,帧率怎么控制
RTSP 流通常是 25 帧或 30 帧,但解码速度不一定跟得上,设置定时器间隔时不能只按帧率来。我一般会开一个 33ms 的定时器,对应 30fps,然后在每次定时器超时时取一帧解码并刷新界面:
// 在 startPlay 中启动定时器 timer->setInterval(33); connect(timer, &QTimer::timeout, this, &VideoPlayer::onUpdateFrame); timer->start();逻辑说明:QTimer 的timeout信号在 UI 线程触发,onUpdateFrame里做av_read_frame和sws_scale。这个方案的好处是简单、稳定,缺点是读 I/O 解码都在 UI 线程,如果流分辨率高或网络抖动,UI 会卡顿。如果遇到多路预览或 4K 解码场景,应该把取流推到一个QThread中,解出帧后通过信号槽把 QImage 发回 UI 线程。参数说明:33ms 只是经验值,如果发现 CPU 占满,可以把这个值调到 40ms 或 50ms,图像会稍微不流畅但程序不会崩。另一个方式是用av_usleep控制循环节奏,在独立线程里解码,配合QMetaObject::invokeMethod跨线程刷新。从这份工程的代码结构看,QTimer 方案就是最匹配的实现。
5. RTSP 播放器避坑清单:链接、延迟、断流与锁问题
5.1 编译通过但启动时提示找不到 DLL
现象:程序编译没有错,但双击运行或从 Qt Creator 启动时弹窗提示“找不到 libavcodec-.dll”或“libavformat-.dll”。原因:FFmpeg 的 DLL 没有被复制到程序运行目录,或者系统 PATH 里没有 FFmpeg 的 bin 路径。Qt Creator 运行时会把工作目录设到构建目录,但不会自动拷贝第三方 DLL。解决:在.pro文件中加上 DLL 拷贝的步骤,或者手动把 FFmpeg 的bin目录下所有.dll文件复制到构建输出目录。我习惯用 QMAKE_POST_LINK 自动完成:
QMAKE_POST_LINK += $$quote(cp -r $$PWD/ffmpeg/bin/*.dll $$OUT_PWD/$$TARGET)注意 Windows 下要用copy /Y代替cp,但 Qt Creator 的 MinGW 环境实际是一个 MSYS 模拟的 shell,cp往往也能用。如果不想折腾,直接在 Qt Creator 里给运行环境变量加上PATH指向 FFmpeg 的 bin 目录也行。
5.2 avformat_open_input 返回 -5 或一直卡住
现象:输入 RTSP 地址后,程序停在avformat_open_input()长达十几秒,然后返回-5(EIO) 或-110(ETIMEDOUT)。原因:RTSP 默认走 UDP 传输,而摄像头所在网络有防火墙或路由器限制了 UDP,或者摄像头本身对 UDP 不友好。另一种情况是地址末尾少了冒号端口号,比如rtsp://192.168.1.100/stream1中漏了:554,部分设备能自动补,部分不能。解决:在打开流之前设置 RTSP 传输方式为 TCP:
AVDictionary *opts = nullptr; av_dict_set(&opts, "rtsp_transport", "tcp", 0); int ret = avformat_open_input(&fmtCtx, url.toStdString().c_str(), nullptr, &opts); av_dict_free(&opts);参数说明:rtsp_transport的另一个可选值udp,TCP 模式延迟会高一些,但画面稳定,不会有马赛克。如果摄像头需要认证,还可以用av_dict_set(&opts, "authorization", "Basic xxx", 0)或直接在 URL 中带用户名密码。加入-stimeout参数可以控制连接超时时间,例如av_dict_set(&opts, "stimeout", "5000000", 0),单位是微秒,5 秒无响应就报错退出,避免死等。从那以后我每次配置 RTSP 地址,不管是什么品牌摄像头,都会先手动用 VLC 验证一遍通不通,再进代码调试,能省不少时间。希望帮到你。
本文还有配套的精品资源,点击获取