简介面向 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 c11 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 丢给 QLabelFFmpeg 解码出来的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_castuint8_t *(rgbBuffer.data()) }; int dstLinesize[1] { width * 3 }; sws_scale(swsCtx, frame-data, frame-linesize, 0, height, dstData, dstLinesize); QImage img(reinterpret_castuchar *(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 引用的是内部 buffercopy()是必须的否则原 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 模拟的 shellcp往往也能用。如果不想折腾直接在 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 传输方式为 TCPAVDictionary *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的另一个可选值udpTCP 模式延迟会高一些但画面稳定不会有马赛克。如果摄像头需要认证还可以用av_dict_set(opts, authorization, Basic xxx, 0)或直接在 URL 中带用户名密码。加入-stimeout参数可以控制连接超时时间例如av_dict_set(opts, stimeout, 5000000, 0)单位是微秒5 秒无响应就报错退出避免死等。从那以后我每次配置 RTSP 地址不管是什么品牌摄像头都会先手动用 VLC 验证一遍通不通再进代码调试能省不少时间。希望帮到你。本文还有配套的精品资源点击获取
