Qt+FFmpeg实现RTSP取流显示:从环境搭建到性能调优
2026/9/24 23:15:31 网站建设 项目流程

简介:面向Qt环境下流媒体应用开发者的RTSP取流资源,以FFmpeg库为核心,解决在Qt中拉取RTSP视频流、解码并播放的实际问题,适合C++/Qt中高级开发者及多媒体入门学习者参考。压缩包共158个文件,包含115个头文件、3个C++源文件、1个UI文件与1个pro工程文件,同时附带9个lib导入库、8个dll动态库、8个a静态库及4个exe可执行程序,整体约18.78MB,既可用于工程配置对照,也可直接运行体验。已有831人学习,资源覆盖FFmpeg初始化、avformat_open_input打开RTSP流、流信息读取、解码器配置、AVFrame到QImage转换及定时刷新显示等完整流程,并涉及播放控制与资源释放。对于实时监控、远程教学等场景,这套代码能帮助开发者避开常见FFmpeg与Qt联调陷阱,快速搭建可用的流媒体播放框架。

1. 先厘清 qt_ffmpeg_rtsp 到底在解决什么:不是播放器,是取流管线

做嵌入式或者桌面监控软件的工程师,迟早会遇到这么一件事:手里一块 Qt 界面,要接一路网络摄像头的 RTSP 流,实时显示在窗口上。新手的本能是去搜 QMediaPlayer 能不能播 rtsp,搜完发现要么是解码格式不全,要么是延迟不可控,折腾一上午连个画面都出不来。这时候才会意识到,qt_ffmpeg_rtsp 这个标题背后真正要干的活,不是“调一个播放器”,而是用 Qt 搭界面和事件循环、用 FFmpeg 做协议解析和解码,在两者之间自己建立起一条从网络流到 QImage 的管线。这两条腿缺一不可,也是所有取流方案的共同骨架。

我见过太多同行在这上面翻车:要么是 FFmpeg 库版本和 Qt 编译器的位数不匹配,要么是 rtsp 走 UDP 丢包卡成幻灯片,要么是界面线程直接去拉流导致窗口假死。这篇文章把这条管线从环境搭建、取流参数、解码显示到排错验证完整拆开讲,按步骤走基本能复现出一台稳定出画面的 RTSP 取流程序。适合手里已经有 Qt 基础、想接手摄像头取流任务的开发者,也适合刚被领导安排做视频监控、连 H.264 和 YUV 都还没分清的初学者。

2. 搭建 Qt + FFmpeg 开发环境:版本匹配与最小可跑工程

2.1 选 FFmpeg 版本:Windows 预编译与 Linux 包管理的差异

取流开发的第一个坎不是写代码,是搞清楚“你手里的 FFmpeg 是从哪来的”。常见做法是 Windows 上直接下载 FFmpeg 官方发布的预编译 Windows 版本,Linux 上走 apt 或 yum 装系统包。两条路殊途同归,但细节差异很大,直接决定了后面会不会遇到cannot mix incompatible qt library (version ex50601) with this library这种编译错。

Windows 预编译版要分清三个细节:编译器版本(MinGW 还是 MSVC)、架构(x86 还是 x64)、静态还是动态链接。Qt 如果用的是 MinGW 64 位编译器,FFmpeg 包就必须选 MinGW 架构的 64 位版本。要是不小心把 MSVC 编译的 FFmpeg 库塞给 MinGW 的 Qt 工程,链接阶段就开始报莫名其妙的错。Linux 上相对省心,apt install libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libswresample-dev一条命令就齐了,但版本可能偏旧,比如 Ubuntu 20.04 自带的 FFmpeg 是 4.2.x,行为规范和最新版有一定差别。

我在写 Qt 程序时建议固定一个 FFmpeg 版本写代码,别追新。FFmpeg 是出了名的不兼容旧接口,比如av_register_all()在 4.x 之后就成了空壳,avcodec_decode_video2早被avcodec_send_packetavcodec_receive_frame替代。先把版本定了,后面查资料的答案才一致。

2.2 .pro 文件配置:把库路径和头文件路径交代清楚

先给一份可用的.pro配置,基于 Qt 5.15 + FFmpeg 5.x/6.x,Windows MinGW 和 Linux 通用思路如下:

QT += core gui widgets multimedia TEMPLATE = app TARGET = rtsp_viewer CONFIG += c++11 # Windows 预编译库路径(按自己实际解压位置改) win32 { FFMPEG_HOME = E:/libs/ffmpeg-6.0-full_build INCLUDEPATH += $${FFMPEG_HOME}/include LIBS += -L$${FFMPEG_HOME}/lib \ -lavcodec -lavformat -lavutil -lswscale -lswresample } # Linux 系统包方式 unix:!macx { CONFIG += link_pkgconfig PKGCONFIG += libavcodec libavformat libavutil libswscale } SOURCES += main.cpp \ playerwidget.cpp HEADERS += playerwidget.h

这段配置里有两个关键点。LIBS里的-lavformat是协议层,负责把 RTSP 封装解析成一个个 AVPacket 的入口;-lswscale负责像素格式转换,没有它你解出来的 YUV 画面没法直接铺到 Qt 控件上。INCLUDEPATH如果漏配了 FFmpeg 的 include 目录,编译时会报找不到libavformat/avformat.h之类的头文件错误。

Linux 上用pkg-config的好处是不用手动维护路径,系统装什么版本就引什么版本。但要注意一点:pkg-config --cflags --libs libavformat的输出里如果带-Wl,-rpath,直接扔进 Qt Creator 可能会出现运行时动态库找不到的情况,稳妥的做法是编译后在运行目录下用ldd确认libavformat.so的解析路径。

2.3 用一段最小代码验证 FFmpeg 能否被 Qt 工程正常调用

不要一上来就写界面,先用一个空窗口工程验证“Qt 能调到 FFmpeg 函数”,这是排查环境问题的最短路径。以下代码放在main.cpp

#include <QApplication> #include <QDebug> #include <libavformat/avformat.h> int main(int argc, char *argv[]) { QApplication app(argc, argv); // 打印 FFmpeg 版本号,确认链接成功 qDebug() << "FFmpeg version:" << av_version_info(); const char *url = "rtsp://127.0.0.1:8554/test"; AVFormatContext *fmt_ctx = nullptr; // 注册网络协议,新版 FFmpeg 仍需显式调用 avformat_network_init(); AVDictionary *opts = nullptr; // 强制走 TCP,避免丢包;设置连接超时 3 秒 av_dict_set(&opts, "rtsp_transport", "tcp", 0); av_dict_set(&opts, "stimeout", "3000000", 0); // 单位是微秒 int ret = avformat_open_input(&fmt_ctx, url, nullptr, &opts); if (ret != 0) { qDebug() << "avformat_open_input failed, ret =" << ret; return 1; } qDebug() << "Open success, streams:" << fmt_ctx->nb_streams; avformat_close_input(&fmt_ctx); avformat_network_deinit(); return 0; }

这段代码验证的事情非常关键。第一,avformat_network_init()是网络协议初始化的入口,漏掉它有的版本会直接 open 失败;第二,rtsp_transport=tcp这个参数决定了你走 TCP 还是 UDP 拉流,千万别用默认值,默认走 UDP,局域网倒还稳定,跨网段丢包丢到怀疑人生;第三,stimeout设置的是网络超时时间,单位是微秒,3 秒的意思是最多阻塞 3 秒就返回失败,不会让程序卡死在 open 里。

跑起来如果打印出版本号和Open success,说明环境链路已经打通。这一步跑不通,后面写再多播放逻辑都是空中楼阁。版本号打印不出来的话,优先回头查编译器位数和库目录是否匹配。

3. RTSP 取流参数与 avformat_open_input:tcp、超时与缓冲怎么设

3.1 理解 RTSP 的本质:信令走 TCP、媒体走 UDP/TCP 的取舍

RTSP(实时流协议)本身不是一个“传输协议”,它更像一个控制协议。客户端先通过 RTSP 信令协商:我要播放哪个地址、用什么编码、用哪种传输方式,服务器确认后才会真正把 H.264/H.265 的媒体数据推下来。媒体数据的实际承载有两种模式:RTP over UDP 和 RTP over TCP。

UDP 模式的优点是延迟低,局域网内通常 200ms 以内能出画面,适合对实时性要求高的场景;缺点是丢包不重传,一旦网络抖动,画面直接花屏、马赛克,而且症状是间歇性的,外网拉到一半就卡死的情况特别多。TCP 模式延迟稍高一点,但包丢了内核会重传,画面稳定得多。我一般推荐安防监控场景一律走 TCP,尤其是多路取流的时候,UDP 的组播冲突和路由器缓冲溢出会让你排查到崩溃。

这就是为什么av_dict_set(&opts, "rtsp_transport", "tcp", 0)成了我写取流函数的第一行。从热词里你能看出大家都被这个问题折腾过:potplayer rtsp 流 反复缓冲rtsp重连,十有八九都是走的 UDP 且网络条件不够理想。

3.2 avformat_open_input 的参数细节:不是只传个 URL 就行

avformat_open_input的完整签名是:

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

第四个参数AVDictionary **options是可选的,但取流场景它几乎必须用。它负责把各种协议选项塞给 FFmpeg 的解复用器。实际项目中我看到过不少人直接传nullptr,然后遇到各种玄学问题:卡死、超时时间不可控、无法指定传输方式。下面的代码是一个更完整的取流初始化函数,包含了取流里最常见的参数组合:

static bool open_rtsp_stream(const std::string &url, AVFormatContext **fmt_ctx) { AVDictionary *opts = nullptr; // 超时设置:连接超时 5 秒,IO 超时 5 秒 av_dict_set(&opts, "stimeout", "5000000", 0); av_dict_set(&opts, "rtsp_transport", "tcp", 0); // tcp/udp/udp_multicast // 对数据缓冲队列的限制:收到的最大包数量,防止内存暴涨 av_dict_set(&opts, "buffer_size", "655360", 0); // 单位字节,仅在 UDP 下生效 av_dict_set(&opts, "max_delay", "500000", 0); // 最大延迟微秒,500ms // 打开输入 int ret = avformat_open_input(fmt_ctx, url.c_str(), nullptr, &opts); if (ret != 0) { char err_buf[128] = {0}; av_strerror(ret, err_buf, sizeof(err_buf)); fprintf(stderr, "open failed: %s\n", err_buf); return false; } // 读取一段流信息,拿到视频流的宽高、编码格式等元数据 ret = avformat_find_stream_info(*fmt_ctx, nullptr); if (ret < 0) { fprintf(stderr, "find stream info failed\n"); return false; } return true; }

这个函数里三个参数需要重点解释。

stimeout是 FFmpeg 网络层的 IO 超时时间,单位是微秒。设了它,av_read_frame阻塞超过这个时间会返回错误码,你就能感知到断流并触发重连,而不是让程序永远卡在读取里。不设的话,断网瞬间av_read_frame可能阻塞很久甚至永远不返回,这在无人值守的监控软件里是不可接受的。

max_delay控制的是解复用器缓冲多长时间的包再往解码器送。值越大,抗抖动能力越强,但延迟越高;值越小,延迟低但遇到网络抖动容易卡。做实时监控我一般取 300ms~500ms,做录像回放可以放松到 1000ms。

还有一个经常被忽略的参数是buffer_size,它只对 UDP 生效,单位是字节。如果走 UDP 且默认值造成丢包严重,把它调大(比如 655360,即 640KB)能在一定程度上缓解。不过记住一点:这只是治标,网络本身差的时候 TCP 才是最终的后悔药。

3.3 用 av_find_best_stream 找到视频流索引

打开文件上下文后,下一步要找视频流索引,这直接决定后续av_read_frame拿到的包该不该送去解码。手写循环遍历fmt_ctx->streams[i]->codecpar->codec_type当然可以,但 FFmpeg 提供了更简洁的方式:

#include <libavformat/avformat.h> #include <libavcodec/avcodec.h> int video_stream_index = av_find_best_stream(*fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (video_stream_index < 0) { fprintf(stderr, "no video stream found\n"); return false; } // 拿到流对应的编码参数 AVCodecParameters *codec_params = (*fmt_ctx)->streams[video_stream_index]->codecpar; // 找到对应的解码器 const AVCodec *decoder = avcodec_find_decoder(codec_params->codec_id); if (!decoder) { fprintf(stderr, "unsupported codec: %d\n", codec_params->codec_id); return false; }

av_find_best_stream的参数里,第二个是媒体类型,第三个-1表示不指定流索引,让 FFmpeg 自动选最合适的;第四个参数配合-1表示不限定编码器类型。

拿到codecpar以后,要立刻检查两个关键信息。第一是codec_id,常见的是AV_CODEC_ID_H264(海康、大华默认)和AV_CODEC_ID_HEVC(H.265),极少数老模拟摄像头升级上来的可能是 MJPEG 或 MPEG4。第二是widthheight,后续sws_scale转换时要用真实分辨率开辟缓冲区,不能凭猜的。

4. 解码、格式转换与界面显示:从 AVPacket 到 QImage 的最短路径

4.1 解码线程与界面线程:谁负责拉流、谁负责刷新

进入解码环节,第一个要建立的工程观念是“界面线程绝不碰网络”。Qt 的主线程跑着事件循环,界面的绘制、鼠标键盘响应都在这个线程里。如果你在槽函数里直接调av_read_frame,网络卡一下界面就整个冻结,窗口标题会显示“无响应”,这让任何基于界面交互的应用都不可接受。

正确划分方式是:拉流、解封装、解码、像素格式转换放一个子线程std::threadQThread,转出来的QImage通过信号跨线程发到主线程,主线程只负责QLabel::setPixmapQWidget::update。注意QImage在跨线程传递时必须是独立的像素缓冲区,不能引用解码器内部的内存——否则对方线程在用的时候,解码线程可能已经把这块内存覆写了。

下面是一个简化的解码线程结构,省去了线程封装细节,聚焦于取帧到发图的链路:

// 解码循环:读包 -> 解码 -> 转格式 -> 发 QImage while (!stop_flag) { AVPacket *pkt = av_packet_alloc(); int ret = av_read_frame(fmt_ctx, pkt); if (ret < 0) { // 读流失败,等一会儿再重试,避免死循环狂转 std::this_thread::sleep_for(std::chrono::milliseconds(200)); av_packet_free(&pkt); continue; } if (pkt->stream_index == video_stream_index) { // 把压缩后的数据送给解码器 ret = avcodec_send_packet(codec_ctx, pkt); if (ret < 0) { av_packet_free(&pkt); continue; } AVFrame *frame = av_frame_alloc(); ret = avcodec_receive_frame(codec_ctx, frame); if (ret == 0) { // 解码成功,做格式转换,生成 QImage QImage img = convert_frame_to_qimage(frame); emit frame_ready(img); } av_frame_free(&frame); } av_packet_free(&pkt); }

这段循环里有个细节值得注意。av_read_frame返回的包里既有视频也有音频,甚至可能包含字幕流,必须用pkt->stream_index做过滤,否则把非视频包喂给视频解码器,avcodec_send_packet立刻报错。

avcodec_send_packet/avcodec_receive_frame是一对兄弟接口。新手常见错误是“发一个包就必须收一帧”,实际不是——H.264 的 B 帧和帧重排序机制导致解码器可能攒好几个包才吐一帧,所以正确姿势是发完包后循环接收,收到AVERROR(EAGAIN)说明当前包数据不足,再读下一个包。

4.2 sws_scale:把 YUV 变成 QImage 能画的东西

FFmpeg 解码后的帧默认是 YUV420P(PixelFormat 为AV_PIX_FMT_YUV420P),QImage 虽然理论上也支持 YUV 格式,但 Qt 的绘制引擎对 YUV 的渲染效率远不如 RGB,而且很多控件操作(模糊、缩放、Alpha 混合)根本不支持 YUV。所以常见做法是统一转换到 RGB 系颜色空间。

先看转换代码:

#include <libswscale/swscale.h> QImage convert_frame_to_qimage(AVFrame *frame) { // 为转换做准备:先确定输出像素格式与尺寸 AVPixelFormat src_pix_fmt = (AVPixelFormat)frame->format; int width = frame->width; int height = frame->height; // 初始化转换器,只在第一次或者尺寸变化时执行 static SwsContext *sws_ctx = nullptr; static int last_w = 0, last_h = 0; static AVPixelFormat last_fmt = AV_PIX_FMT_NONE; if (!sws_ctx || width != last_w || height != last_h || src_pix_fmt != last_fmt) { if (sws_ctx) sws_freeContext(sws_ctx); sws_ctx = sws_getContext(width, height, src_pix_fmt, width, height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); last_w = width; last_h = height; last_fmt = src_pix_fmt; } // 分配 RGB 缓冲 std::vector<uint8_t> rgb_buffer(width * height * 3); uint8_t *dst_data[4] = { rgb_buffer.data(), nullptr, nullptr, nullptr }; int dst_linesize[4] = { width * 3, 0, 0, 0 }; sws_scale(sws_ctx, frame->data, frame->linesize, 0, height, dst_data, dst_linesize); // 用 RGB 数据构造 QImage,注意深拷贝 QImage img(dst_data[0], width, height, dst_linesize[0], QImage::Format_RGB888); return img.copy(); }

这段代码有意识地处理了两个隐患。

第一个是sws_getContext不能每帧调用,它的耗时相当可观,做成全局静态并在输入参数变化时重建是常见优化手段。多数摄像头分辨率固定,sws_getContext只需要执行一次。第二个是img.copy(),这一步做了像素数据的深拷贝——如果你返回的 QImage 引用了局部变量rgb_buffer,函数返回后内存释放,主线程画出来就是花屏或者崩溃。一个copy()避免了这类“偶发但致命”的 bug。

4.3 Qt 侧刷新:QLabel 还是 QOpenGLWidget

拿到 QImage 后,显示方式按性能需求分级。最小实现是把 QImage 转成QPixmap塞给 QLabel:

void PlayerWidget::on_frame_ready(const QImage &frame) { // 缩放到控件大小,转换和绘制一起完成 QPixmap pix = QPixmap::fromImage(frame).scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); m_label->setPixmap(pix); }

这种方式代码最短,跑 1080p 的流 CPU 占用也比较可观,尤其是SmoothTransformation缩放会吃掉不少算力。如果只是测试,可以换成Qt::FastTransformation减少性能损耗。

生产环境里 CPU 占用率敏感时,第二种选择是QOpenGLWidget配合纹理上传,把QImage转成QOpenGLTexture绘制到屏幕上。这能让 4 路 1080p 同时流畅显示,但也把开发复杂度拉高了一个量级,需要处理 OpenGL 上下文、纹理生命周期和窗口 resize 时的映射关系。我一般建议先上 QLabel 验证业务逻辑,性能不够再升级 OpenGL,不要一开始就把台阶抬得太高。

控制刷新频率还有一个细节:摄像头通常 25fps,但 UI 完全没必要每帧刷新。比较省事的做法是设置一个QTimer,按 30ms 间隔从队列里取最新一帧显示,跳过的旧帧直接丢弃。这样界面刷新节奏稳定,而且避免了信号风暴。

5. 踩坑与排查:编译链接、缓冲卡顿、断流重连的常见翻车现场

5.1 cannot mix incompatible qt library (version ex50601):如何定位版本混用

现象:编译或运行时报fatal: cannot mix incompatible qt library (version ex50601) with this library,程序根本起不来,或者起来后窗口直接崩溃。

原因:Qt 的编译期主版本和运行期库版本不一致,比如用 Qt 5.14 的头文件编译,但运行时加载的是 Qt 5.6 的 DLL。这种错误很少是 FFmpeg 导致的,更多是 Qt Creator 里 Kit(编译器套装)切换以后,清理缓存不彻底,或者 PATH 环境变量里混入了其他版本的 Qt bin 目录。

解决:第一步在项目里执行qmake && make clean,把.pro.userMakefile删除后重新构建;第二步检查PATH环境变量,确认qmake实际指向的 Qt bin 目录和 Qt Creator 配置的 Kit 路径一致;第三步用ldd(Linux)或 Dependency Walker 类的工具确认程序运行时加载的Qt5Core.dll的绝对路径。注意千万别图省事直接把PATH里的旧 Qt 目录删了,可能系统里还有别的程序依赖它,先在 Qt Creator 里把“运行环境”的手动 PATH 配置去掉,让它完全继承当前 Kit 的环境。

5.2 RTSP 反复缓冲、画面卡顿:十有八九是传输方式和缓冲参数错了

现象potplayer rtsp 流 反复缓冲、自己写的取流程序画面卡成幻灯片,但 CPU 占用率又不高。网络稍有波动就直接停住几秒,随后又跳帧往前赶。

原因:RTSP 走的是 UDP,路由器或交换机的缓冲队列一满就开始丢包,而 UDP 丢了就丢了,解码器拿不到参考帧会一直等待,表现为反复缓冲。另外max_delay设得过小(比如默认的 700ms 在某些相机上换算不对)导致 jitter buffer 装不下包,也会造成同样的症状。

解决:在打开 RTSP 时用av_dict_set(&opts, "rtsp_transport", "tcp", 0)强制 TCP。TCP 模式下包的到达是可靠的,不会出现“缺了关键帧等半天”的问题。如果非得走 UDP(比如延迟要求低于 100ms 的交互场景),把buffer_size调大到 1MB 级别,再把max_delay调整到至少 1 秒,先换来稳定再慢慢收小。最后做一次“换播放器对照实验”:用 VLC 或 ffplay(命令ffplay -rtsp_transport tcp rtsp://ip:port/stream)播放同一个地址,如果 ffplay 正常而你的程序卡,问题出在代码;如果 ffplay 也卡,问题出在网络和摄像头端配置。

5.3 avformat_open_input 返回失败:解码器没找到还是网络根本没通

现象avformat_open_input返回负值,av_strerror打印出来的是Connection refused或者Protocol not found,甚至莫名报一个“Invalid data”找不到流的错误。

原因:除了 IP 写错、端口不通这类低级问题外,最容易被忽略的是 FFmpeg 编译时没启用 RTSP 协议。你用的预编译包里没有rtsp://对应的 demuxer 时,FFmpeg 会告诉你Protocol not found。老旧代码里还有一个坑:某些写死av_register_all()的教程要求这个函数,但新版已废弃,不调用甚至可能直接导致rtsp协议解析失败。

解决:第一步先确认网络通不通,在命令行 ping 摄像头 IP,然后确认 RTSP 端口 554 能连通。第二步在程序里打印avio_enum_protocols(nullptr, 0)枚举出的协议列表,确认里面有rtsptcp。如果没有,换一份完整构建的 FFmpeg 库或者检查系统 FFmpeg 编译选项。第三步把 URL 在ffprobe里跑一遍:ffprobe -rtsp_transport tcp -i rtsp://user:pass@ip:port/stream,能出流的 URL 放在自己代码里也应该能出流,这一步能有效区分“这是 FFmpeg 的问题”还是“这是代码的问题”。

5.4 断流重连后的黑屏或白屏:需要清理的不止是网络连接

现象:摄像头断网、重启或者程序主动断流重连后,avformat_open_input成功但画面全黑/全白,或者报解码错误。

原因:断流重连时写了重连逻辑,但解码器上下文(AVCodecContext)、SwsContext 和内部帧缓冲没有复位。摄像头重启后可能会发送新的 SPS/PPS 参数,如果你的解码器还沿用旧参数,H.264 的解码状态机就乱套了。

解决:重连时做一次“全量复位”:avcodec_flush_buffers(codec_ctx)清空解码器内部缓冲,然后重新调用avformat_find_stream_info刷新分辨率等参数。不要复用旧的AVCodecContext,直接avcodec_free_context再根据新流参数重新创建。另外sws_getContext也基于旧分辨率做了缓存,如果新分辨率变了却不重建,转出来的图就是花的。一个懒人习惯是:重连成功以后先用sleep(200ms)等第一个关键帧(SPS/PPS)到齐再开始显示,能避免大量解码失败日志。

5.5 运行时提示 qt.qpa.plugin: could not find the qt platform plugin “linuxfb”

现象:在 Linux 嵌入式板子上编译好的取流程序,板子上运行时报qt.qpa.plugin: could not find the qt platform plugin “linuxfb”,程序直接退出。

原因:Qt 程序运行时需要对应的 QPA(Qt Platform Abstraction)插件。桌面 Linux 用xcb,嵌入式开发板普遍用linuxfb或者eglfs。你的程序编译时带了QT += widgets,但运行时platforms目录下找不到libqlinuxfb.so。常见原因是交叉编译时 Qt 的 platform plugins 路径没打进去,或者环境变量QT_QPA_PLATFORM_PLUGIN_PATH没指向正确的插件目录。

解决:确认开发板上 Qt 库的安装路径,然后在运行该程序前导出一个环境变量,例如:

export QT_QPA_PLATFORM=linuxfb export QT_QPA_PLATFORM_PLUGIN_PATH=/opt/qt5.15/plugins/platforms ./rtsp_viewer

如果用的不是 linuxfb 而是 eglfs,则把QT_QPA_PLATFORM换成eglfs。还有一个频繁踩到的点:linuxfb插件没有鼠标光标支持,如果在带触摸屏的板子上开发,建议换成eglfs或者直接加载tslib插件,否则 UI 看起来像“死了”一样完全没反应。

6. 性能参数调优与验证:让取流长时间稳定跑起来的检查清单

6.1 延迟优先与稳定优先的两套参数组合

同一套代码,直播场景和监控录像场景的目标不一样,参数组合也完全不同。我把成熟的参数组合整理为下表,可以直接抄作业。

场景延迟优先(如在线预览、云台控制)稳定优先(如安防录像、多路轮巡)
rtsp_transporttcp(特殊情况 udp)tcp 固定不变
stimeout3000000(3s)10000000(10s)
max_delay200000(200ms)1000000(1000ms)
buffer_size默认或 256KB1MB 以上
解码器线程数默认自动明确设 2~4
帧队列长度1(丢旧帧保实时)5~10(保流畅)

max_delay是最影响手感的参数。云台控制时如果图像延迟超过 500ms,操作者会觉得“手感黏滞”,因为画面的反馈跟不上手指的动作;但往大了调能换来画面的顺滑度。需要手动调节画面和网络抖动之间的平衡,每次改完参数后要跑至少 30 分钟再下结论,因为网络丢包是有随机性的,只看两三分钟样本得不出真实结果。

还有一个和 FFmpeg 无关但经常被忽略的参数:解码线程的调度优先级。取流的子线程在 Windows 上默认是普通优先级,遇到 CPU 满载时会被其他线程抢占,导致丢帧。可以试试用QThread::setPriority(QThread::TimeCriticalPriority)把拉流线程的优先级抬高一级,画面会稳定很多。

6.2 搭建本地 RTSP 测试源:ffmpeg 推送本地文件避免蹭摄像头

开发初期网上一堆测试源,但依赖它们不稳定,随时可能失效。更可控的做法是在本地用 ffmpeg 直接推一路流,既方便反复测试,又能复现弱网卡顿:

# 用本机摄像头文件(或图像序列)推 RTSP 流到本地 8554 端口 ffmpeg -re -stream_loop -1 \ -i /path/to/test.mp4 \ -c copy -f rtsp \ -rtsp_transport tcp \ rtsp://127.0.0.1:8554/test

-re表示按原始帧率读取,模拟真实摄像头推流节奏;-stream_loop -1让视频循环播放,做长时间稳定性测试时不用管它;-c copy直接复制编码流,不重新编码,降低本机负载。这时候可以顺手打开第二个终端用ffprobe验证源:

ffprobe -rtsp_transport tcp -v error -show_streams rtsp://127.0.0.1:8554/test

能看到视频流信息就说明本机测试源已经就绪。这样测试过程中所有网络问题都发生在环回接口上,可以最大程度排除“摄像头端问题”的干扰。

6.3 用 FFmpeg 日志级别和 av_err2str 缩短定位周期

最后分享一个贯穿整个调试过程的有效习惯:把 FFmpeg 的日志级别调整到AV_LOG_DEBUG并输出到 stderr,很多问题当场就能在控制台看到线索。FFmpeg 的默认日志级别是AV_LOG_INFO,有些细节看不见,需要主动打开:

#include <libavutil/log.h> // 放在 main 函数开头 av_log_set_level(AV_LOG_DEBUG); av_log_set_callback([](void *ptr, int level, const char *fmt, va_list vl) { if (level <= AV_LOG_WARNING) { vfprintf(stderr, fmt, vl); fflush(stderr); } });

回调里只过滤AV_LOG_WARNING以上级别,避免 DEBUG 输出刷满控制台。当av_read_frameavformat_open_input返回负值时,用av_strerror(ret, err_buf, sizeof(err_buf))把错误码变成人话,而不是只打印一个数字。比如-1094995529AVERROR_INVALIDDATA-5EIO,这些数字背不下来,但av_strerror打印出Invalid data found when processing input,立刻就能定位到是码流没对齐还是协议握手失败。

我现在的开发习惯默认就是开 DEBUG 级日志跑 20 分钟,断流、错包、重连的过程全都有据可查,比出了 bug 再回头加打印快得多。很多取流上的玄学问题,最后都会发现其实是日志没开够、参数没配对。先把环境捋顺、参数钉死,剩下的交给时间验证稳定性。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询