基于Qt与FFmpeg的RTSP视频流播放器Demo实现 📅 发布时间:2026/9/9 20:08:04 👁 浏览次数: 简介一套基于Qt框架与FFmpeg库的RTSP播放演示工程适合熟悉C基础、希望快速上手音视频流处理的开发者也可作为Qt集成FFmpeg的参考范例。工程展示了从RTSP拉流、解码到界面显示的完整流程封装了FFmpeg调用逻辑并提供Makefile与pro工程文件便于在Linux或类Unix环境下一键构建。压缩包共16个文件以cpp、h源文件为主辅以ui界面文件、项目配置说明及README文档整体仅14KB结构精简适合直接阅读源码理解核心实现。已有669人学习下载。参考该项目可掌握avformat_open_input、av_read_frame等关键API的实际用法同时了解Qt事件循环与多线程在视频流处理中的配合方式对后续开发监控播放器或直播客户端有不错的借鉴意义。 最近我在折腾一个新的播放器小项目顺手把 Qt、FFmpeg、RTSP 这三样东西串起来写了一个 DemoQt 负责界面和渲染FFmpeg 负责拉流和解码RTSP 则是统一使用的视频流协议。这个 Demo 不追求做成完整播放器而是把“从网络摄像头或流媒体服务器上拉 RTSP 视频流解码后显示在 Qt 窗口”这条主线跑通。适合刚接触 Qt 和 FFmpeg、又需要做监控或视频接入的同学参考。1. 先聊清楚这个Demo到底在做什么1.1 项目解决的核心问题你打开 VLC 能播 RTSP 是一回事但要在自己的 Qt 程序里播就避不开三个环节如何建立网络连接、如何解码 H.264/H.265 裸流、如何把解码后的图像搬到 UI 控件上。RTSP 只是一个约定好“怎么传输和播放会话”的控制协议真正传给解码器的数据要经过 RTP/RTCP 通道FFmpeg 在这套体系里做了最重要的活把 RTSP 地址直接变成 AVPacket 和 AVFrame我们只需要调用它暴露的 C 接口。这个 Demo 最直接的价值是把这三个环节做成最小闭环。它不处理音频、不做倍速暂停、不搞复杂的倍率界面只做一个串口同步的“视频通道”。对于摄像头接入、大屏展示和快速原型验证这类场景这套框架已经可以当作起点。你在里面替换成局域网摄像头的地址再改一改界面布局基本就是一个轻量监控客户端了。1.2 整体架构内的三个角色Qt、FFmpeg、RTSP 三个角色需要明确分工。RTSP 是传输层面的协议负责协商视频格式和传输方式但协议本身不负责解码。FFmpeg 的 libavformat 库把我们传进去的 RTSP URL 解析成输入流然后由 libavcodec 完成真正的解码。Qt 在这里不碰任何音视频算法只接收 FFmpeg 解码产生的 QImage 对象并用 QLabel 或者自绘控件显示。这里最需要想清楚的是线程模型。FFmpeg 的 av_read_frame 是一个阻塞调用如果直接放在 UI 线程里一旦网络握手慢或者解码卡顿整个窗口就会假死。所以我在 Demo 里单独开了一个 QThread把拉流、解码、格式转换全部放在工作线程里等拿到一帧完整的 RGB 图像后通过 Qt 信号把 QImage 发给主窗口。这也是后续调试里最容易踩雷的地方因为一不小心就会在线程中直接操作 UI 对象导致崩溃。1.3 核心技术方案的取舍我一开始也纠结过要不要用 QtMultimedia但实测下来它对 RTSP 的支持很别扭特别是在 Windows 上要依赖后端解码器拿到原始帧也费劲。FFmpeg 的优势是全平台统一接口稳定摄像头厂商给的 RTSP 流几乎都能兼容。虽然要自己管理队列和线程但自由度更高。为了减少依赖我选择了比较老派但很稳的 QThread 派生方案而不是 moveToThread。原因很简单Demo 里只有一个采集循环不需要把任务拆成多个槽函数异步执行。QThread 的 run() 里写一个 while 循环从打开 URL 到读取 packet再到解码发送流程是线性的。这个方案在排查崩溃时特别直观不会因为信号槽跨线程引发找不到 bug 的困扰。2. 环境准备版本、下载和工程配置2.1 Qt与FFmpeg版本组合我在项目里用的是 Qt 5.15.2 FFmpeg 6.0。Qt 5.15 虽然老一点但是和很多第三方库的兼容性最好网上遇到的坑基本都有答案。Qt 6 也不是不能用只是 FFmpeg 的 include/lib 链接方式和信号槽用法差异很小没必要为了新版给自己增加不必要的风险。FFmpeg 的版本选择建议直接上 6.x5.x 也可以但要注意库文件名里有没有版本号。我的工程下载的是 FFmpeg 6.0 Windows 版编译方式是 MinGW 64。这里有个关键点你用 MSVC 编译 Qt就要下载 FFmpeg 对应 MSVC 的构建版本用 MinGW 的 Qt就下载 MinGW 构建版。混用会导致链接时找不到符号报一堆 LNK2019 或 undefined reference。2.2 下载FFmpeg时别选错包FFmpeg 的 Windows 构建通常有 dev、shared、full 三类包。dev 包里有 include 头文件和 int 库比如 avcodec.libshared 包里有 bin 下的 DLL 和对应的导入库full 包则是两者加一堆工具。我们只需要 dev 和 shared。把两个包解压到同一个目录你会看到 include、lib、bin 三个文件夹这个目录结构在工程里直接引用即可。下载时还要注意架构x86_64 对应 64 位x86 对应 32 位。如果 Qt 是 64 位就一定要选 x86_64 的 FFmpeg。我在第一次搭项目时因为图省事下了 32 位版本结果 Qt Widgets 界面能编译但链接 FFmpeg 相关函数时全部报错排查了半天才发现位数不一致。2.3 pro文件与运行环境配置因为项目是 Qt Widgets 工程我用的是 .pro 文件。为了避免写死的绝对路径我用 $$PWD 来定位 FFmpeg 目录这样换台电脑时只要把 FFmpeg 解压到工程根目录命名成 ffmpeg 文件夹就能直接编译。QT core gui widgets TEMPLATE app TARGET QtFFmpegRTSPDemo INCLUDEPATH $$PWD/ffmpeg/include LIBS -L$$PWD/ffmpeg/lib \ -lavformat \ -lavcodec \ -lavutil \ -lswscale SOURCES \ main.cpp \ mainwindow.cpp \ videothread.cpp HEADERS \ mainwindow.h \ videothread.h编译出来之后别忘了把 ffmpeg/bin 里的 DLL 拷贝到 exe 同级目录否则运行时提示找不到 avformat-*.dll。这一步经常被新手忽略我习惯在 pro 里加一个自定义构建步骤或者在每次构建后手动同步操作虽然土但非常稳。3. 核心代码实现从拉流到上屏3.1 初始化RTSP流并打开解码器采集线程的头文件里我保留了三个对外接口setUrl、stop、frameReady 信号。打开流的关键是给 avformat_open_input 传参这个函数会根据 URL 前缀自动识别 RTSP 协议。在调用之前还可以用 AVDictionary 设置一些传输选项比如强制走 TCP避免 UDP 丢包导致花屏。AVDictionary *opts nullptr; av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 5000000, 0); // 5秒超时 av_dict_set(opts, max_delay, 2000000, 0); // 2秒缓冲上限 AVFormatContext *fmtCtx nullptr; int ret avformat_open_input(fmtCtx, url_.toStdString().c_str(), nullptr, opts); if (ret ! 0) { emit errorOccurred(QStringLiteral(打开RTSP流失败错误码%1).arg(ret)); return; } avformat_find_stream_info(fmtCtx, nullptr);打开之后用 av_find_best_stream 拿到视频流索引然后通过 codecpar 获得编码参数。这里不用猜编码类型直接交给 avcodec_find_decoder 查解码器。H.264 流很常见但有些摄像头会输出 H.265FFmpeg 的 libx265 解码器如果没在构建里启用会调用失败需要在下载时选带更多解码器的 build 类型。在代码里可以打印 codec_id方便排查。int videoIndex av_find_best_stream(fmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); AVStream *stream fmtCtx-streams[videoIndex]; const AVCodec *decoder avcodec_find_decoder(stream-codecpar-codec_id); AVCodecContext *codecCtx avcodec_alloc_context3(decoder); avcodec_parameters_to_context(codecCtx, stream-codecpar); if (avcodec_open2(codecCtx, decoder, nullptr) ! 0) { emit errorOccurred(QStringLiteral(解码器打开失败)); return; }3.2 解码循环、像素格式转换与信号发射解码循环我放在了 run() 里。先说说为什么要做像素格式转换。FFmpeg 解码出来的原始帧一般是 YUV420P 或 NV12Qt 的 QImage 原生支持 RGB 格式所以我用 libswscale 把 YUV 转成 RGB24。转换之前要分配一个目标 AVFrame并给它填上 buffer。这个 buffer 要提前用 av_image_get_buffer_size 计算大小别随手 mallocFFmpeg 对对齐有要求直接用 av_malloc 最稳。SwsContext *swsCtx sws_getContext( codecCtx-width, codecCtx-height, codecCtx-pix_fmt, codecCtx-width, codecCtx-height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); AVFrame *frame av_frame_alloc(); AVFrame *rgbFrame av_frame_alloc(); int byteSize av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecCtx-width, codecCtx-height, 1); uint8_t *rgbBuffer (uint8_t *)av_malloc(byteSize); av_image_fill_arrays(rgbFrame-data, rgbFrame-linesize, rgbBuffer, AV_PIX_FMT_RGB24, codecCtx-width, codecCtx-height, 1);循环读取 packet 时要注意av_read_frame 拿到的 packet 可能包含音频流或字幕流必须判断 stream_index 是不是之前找到的视频索引。把 packet 送给 avcodec_send_packet 之后再用 avcodec_receive_frame 把解码出来的 frame 取走。这两个函数组成一套现代解码接口比旧的 avcodec_decode_video2 安全得多。AVPacket *packet av_packet_alloc(); while (!stopFlag_) { int ret av_read_frame(fmtCtx, packet); if (ret 0) { break; } if (packet-stream_index videoIndex) { ret avcodec_send_packet(codecCtx, packet); while (ret 0) { ret avcodec_receive_frame(codecCtx, frame); if (ret 0) { break; } sws_scale(swsCtx, frame-data, frame-linesize, 0, codecCtx-height, rgbFrame-data, rgbFrame-linesize); QImage image(rgbFrame-data[0], codecCtx-width, codecCtx-height, rgbFrame-linesize[0], QImage::Format_RGB888); emit frameReady(image.copy()); } } av_packet_unref(packet); }这里有个细节QImage 构造时只是引用了 rgbBuffer 的内存并没有拷贝数据。等到下一帧 sws_scale 覆盖 buffer 时上一帧的内容就变了。所以我用 image.copy() 做了一次深拷贝再通过信号发出去。如果不 copy主界面很容易显示闪烁的错乱图像。3.3 UI侧接收帧并渲染主窗口里连接 frameReady 信号时要保证连接方式是默认的 AutoConnection。信号在工作线程发射槽函数在主线程执行Qt 会自动做队列投递。槽函数只做两件事把收到的 QImage 缩放后设置到 QLabel 上并更新一个状态标签提示当前帧率。void MainWindow::onFrameReady(const QImage image) { ui-videoLabel-setPixmap( QPixmap::fromImage(image).scaled( ui-videoLabel-size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); }我还会在 MainWindow 构造函数里把 label 的尺寸策略设为 Ignored这样窗口拉伸时 label 会跟着变大而不会出现黑边。如果你要更精细地控制可以重写 paintEvent 或使用 QOpenGLWidget但Demo阶段 QLabel QPixmap 已经足够流畅。4. 真正跑通后踩过的坑4.1 no Qt platform plugin could be initialized这个问题在 Windows 上特别典型。你编译出来的程序在没有安装 Qt 的机器上运行报错no Qt platform plugin could be initialized。原因是 Qt 运行时找不到 qwindows.dll。这个插件一般位于 Qt 安装目录的 plugins/platforms 下。程序会到可执行文件同级目录的 platforms 文件夹里找。解决办法有两种。最省事的是用 windeployqt 工具它会自动把 Qt 的 DLL、插件和依赖拷贝到 exe 目录。命令如下windeployqt QtFFmpegRTSPDemo.exe如果只想手动解决就在 exe 所在目录创建 platforms 文件夹把 qwindows.dll 复制进去。同时确保 Qt 的 bin 目录在 PATH 环境变量里或者把相关 Qt DLL 一并拷过来。这个坑不是 FFmpeg 引起的但却是项目打包时最常见的拦路虎。4.2 RTSP延迟大、画面卡顿调试时我发现延迟一直在 3 秒以上看起来卡得不行。第一个原因是 RTSP 默认传输方式可能走 UDP在局域网里丢包后解码器会等重传导致画面卡住。用 av_dict_set 把 rtsp_transport 强制改成 tcp 后延迟立刻降下来了。代价是网络占用会高一些但对监控场景来说稳定更重要。第二个原因是 FFmpeg 内部开了很大的缓冲区。通过 max_delay 可以控制缓冲上限。我设置为 2 秒单位是微秒所以传入 2000000。你还可以通过减少buffer_size来降低延迟但不要设太小否则画面会出现花屏。实测 2048000 字节对 1080p 的码流足够。第三个原因比较隐蔽RTSP 源如果长时间不发送关键帧画面会一直黑屏或停留在首帧。这和网络无关是解码器没有参考帧导致的。可以在解码循环里计数超过 150 帧仍没有关键帧时主动断开重连这是很多商业播放器采用的重试策略。4.3 线程退出时的崩溃Demo 刚写完时关闭窗口经常会崩溃。原因在于 QThread 的子类析构时run() 还阻塞在 av_read_frame 里网络流没有返回线程自然无法退出。我最初在析构里调用 stop()但 stop() 只设置标志位av_read_frame 还是卡着。我的解决方案是在 stop() 里同时设置标志位和调用 avformat_close_input。因为 FFmpeg 的关闭函数会唤醒阻塞中的 read 操作让 av_read_frame 返回错误码循环就能正常跳出。不过这里要注意avformat_close_input 必须和 avformat_open_input 成对出现而且要保证在同一个线程内调用。如果 stop() 是从 UI 线程调用的就不能直接操作 fmtCtx应该是通过一个请求标志让 run() 内部自己调用关闭函数。最终的停止逻辑设计成双阶段先请求停止等待线程退出再释放资源。在 while 循环里我还会用 msleep(10) 做一次极小休眠避免忙碌循环占用 100% CPU。这个习惯在解码类项目里很重要因为纯 while 空转会白烧 CPU。5. 验证与调优5.1 用公开RTSP流测试写好代码后第一步是用公开 RTSP 流验证正确性。我常用这几个rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4rtsp://freertsp.org:554/live.sdprtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mp4公共流不稳定是常态所以我会先用 FFmpeg 命令行探一下ffmpeg -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4 -t 5 test.mp4如果这条命令能正常生成文件说明公网地址可用。否则换一个。在 Demo 的 URL 输入框里填地址点连接窗口出现画面就算成功。还有一种方式是本地推流在另一台机器执行ffmpeg -re -i input.mp4 -c copy -f rtsp rtsp://localhost:8554/test只要你本地跑了一个 RTSP 服务器就能构造一个稳定的测试环境。不过这一步会引入额外服务我建议先用公共流跑通代码再切到真实的摄像头地址。5.2 观察指标与优化建议我在界面上添加了一个简单的状态栏显示实时帧率和累计帧数。对于 720p 的 RTSP 视频流在 Qt 5.15.2 FFmpeg 6.0 的 Debug 构建下CPU 占用大约 30%~40%切到 Release 后能降到 10%~20%。如果开多个视频流CPU 会线性增加这时候就要考虑硬解码或者减少缩放次数。硬解码的接入点是 avcodec_find_decoder_by_name(h264_cuvid) 或者 “h264_qsv”取决于你的显卡。硬解码能大幅降低 CPU 消耗但要注意显存和像素格式转换的开销不是所有场景都能获得明显收益。优化时优先看 sws_scale 是不是频繁全尺寸转换我一般会把输出尺寸对齐到偶数避免奇宽奇高导致转换失败。还有一个容易被忽略的小技巧FFmpeg 的日志默认打到 stderr在 GUI 程序里看不到。调试时在 main() 里设置 av_log_set_level(AV_LOG_INFO)并把 av_log_set_callback 指向自定义函数就可以把 FFmpeg 日志重定向到 Qt 的 qDebug 窗口排查 RTSP 握手失败时特别有用。这个 Demo 跑通之后你可以继续扩展的方向很多把 QImage 渲染改成 QOpenGLWidget提升高分辨率流的绘制效率把解码后的 YUV 帧直接通过 QVideoFrame 送给 QMediaPlayer 做零拷贝渲染或者接入海康、大华的私有 SDK 做设备发现。但不管怎么改Qt、FFmpeg、RTSP 这套骨架是不会变的。我个人实际开发里最深的体会是视频相关项目的坑大多不在接口而在内存和线程。如果你想把这种信号槽跨线程传图的模式用在自己的项目里建议从一开始就把 QImage 的拷贝、缓冲区的生命周期和线程停止策略想清楚不然后面改起来很痛苦。本文还有配套的精品资源点击获取