资讯动态

Qt+FFmpeg拉RTSP流:解码、显示与Makefile构建实战

发布时间:2026/10/17 12:07:25 来源:尧图企业网站定制
简介基于Qt与FFmpeg的RTSP演示项目面向需要在C/Qt环境中集成FFmpeg播放实时视频流的开发者适用于远程监控、在线直播、视频会议等场景也适合作为学习FFmpeg API与Qt多线程编程的入门实例。压缩包共16个文件包含5个C源文件、4个头文件、3个Qt Designer生成的界面文件及Makefile、.pro工程文件和说明文档。其中源文件实现主程序和FFmpeg调用逻辑头文件声明类接口UI文件描述图形布局Makefile负责自动化编译整体仅14KB结构紧凑、易于按模块阅读。已有671人学习下载。项目演示了avformat_open_input、av_read_frame、avcodec_decode_video2等关键API的调用方法并通过自定义封装类整合RTSP连接、流解析与解码流程结合Qt事件循环与多线程机制实现流畅画面显示同时Makefile也展示了自动化构建的配置思路。适合希望以轻量示例入门多媒体网络编程的开发者。1. 为什么用 Qt FFmpeg 拉 RTSP先讲一个常见痛点我平时接触最多的音视频需求是在 Qt 窗口里显示一路 RTSP 网络流。用 QtMultimedia 直接拉起 RTSP延迟经常高到没法看换成 FFmpeg 后链路清晰多了。这套 Qt-FFmpeg-RTSP-Demo 是一条完整的基线工程它把 RTSP 拉流、解码、像素转换、QImage 显示串成一条线并用 Makefile 组织编译源码包里的每个文件都有明确作用。它对两类人最有用一类是想快速跑通 RTSP 播放的 Qt 开发者另一类是已经有播放框架、但卡在延迟和显示兼容上的维护者。下面几章我会把关键动作拆开讲包括解码管线、Qt 集成姿势以及 Makefile 下最容易翻车的链接问题。2. RTSP 拉流链路从 URL 到第一帧画面需要掌握的三件事2.1 为什么是 FFmpeg 而不是 QtMultimedia很多同事问我Qt 不是有 QMediaPlayer 和 QML 的 VideoOutput 吗为什么还要背 FFmpeg 一堆结构体。这个问题我当初也纠结过。QtMultimedia 在 Windows 上走 DirectShow/MediaFoundation在 Linux 上走 GStreamer后端差异很大而且 RTSP 很多“不常规”的能力比如指定传输协议、控制 UDP 缓冲、重新连接逻辑Qt 的抽象层要么暴露不全要么版本之间改了行为。FFmpeg 就简单直接libavformat 负责把 RTSP 当成文件一样打开libavcodec 负责解码libswscale 负责色彩空间转换。它是纯 C 接口没有信号槽但稳定性和透明性都要高一个级别。更重要的是FFmpeg 的 demuxer 对 RTP/RTCP 的处理非常细遇到丢包、重传、时间戳跳变都有日志可查而 QtMultimedia 很多时候只给你一个“出现故障”的抽象错误。所以在这个 Demo 里选择 FFmpeg 不是因为它更“工程化”而是因为它把网络流解码这件事拆成了可调试的小步骤。后面我们所有的参数调整比如 TCP 还是 UDP、超时多久、缓冲多大都直接作用在 FFmpeg 的上下文结构上。2.2 解码管线的核心调用顺序先看一段打开 RTSP 并拿到解码器的代码这是整个链路的地基#include libavformat/avformat.h #include libavcodec/avcodec.h #include libavutil/opt.h // 网络拉流前初始化网络组件某些版本没有它会在 open 时报错 avformat_network_init(); // 一个管封装一个管解码 AVFormatContext *fmt_ctx NULL; AVDictionary *opts NULL; // 把 RTSP 常用参数先塞进去下面会单独讲参数含义 av_dict_set(opts, rtsp_transport, tcp, 0); av_dict_set(opts, stimeout, 3000000, 0); int ret avformat_open_input(fmt_ctx, url.c_str(), NULL, opts); if (ret 0) { char errbuf[128] {0}; av_strerror(ret, errbuf, sizeof(errbuf)); qWarning() open input failed: errbuf; return false; } // 读取流信息之后才能拿到流索索引和编码参数 ret avformat_find_stream_info(fmt_ctx, NULL); if (ret 0) { avformat_close_input(fmt_ctx); return false; } // 找到第一个视频流返回的是流索引不是编码器 int video_index av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (video_index 0) { avformat_close_input(fmt_ctx); return false; } // 从流里复制编码参数到 CodecContext注意不能直接赋值 AVCodecParameters *codec_par fmt_ctx-streams[video_index]-codecpar; const AVCodec *decoder avcodec_find_decoder(codec_par-codec_id); AVCodecContext *dec_ctx avcodec_alloc_context3(decoder); avcodec_parameters_to_context(dec_ctx, codec_par); ret avcodec_open2(dec_ctx, decoder, NULL);这段代码里av_dict_set的两个参数值得记一下rtsp_transporttcp强制走 TCP避免摄像头在 UDP 模式下丢包导致长时间无响应stimeout单位是微秒这里 3000000 就是 3 秒超时会快速返回错误而不是无限卡死。av_find_best_stream比手动遍历流列表更可靠它会自动跳过不支持的流类型。avcodec_parameters_to_context是做深拷贝避免后续释放帧时访问悬空指针。拿到解码器后进入循环读取网络包AVPacket *pkt av_packet_alloc(); AVFrame *frame av_frame_alloc(); while (keep_running) { int ret av_read_frame(fmt_ctx, pkt); if (ret AVERROR(EAGAIN) || ret AVERROR(EINTR)) { continue; } if (ret 0) { break; } if (pkt-stream_index video_index) { // 送压缩包进解码器 avcodec_send_packet(dec_ctx, pkt); // 解码器可能缓冲多帧一次 send 要循环 receive while (avcodec_receive_frame(dec_ctx, frame) 0) { handleDecodedFrame(frame); } } av_packet_unref(pkt); }注意av_read_frame返回的是压缩数据包必须通过avcodec_send_packet送到解码器然后循环调用avcodec_receive_frame把解出的原始帧全部取走。很多初学朋友只调用一次receive结果每隔几帧画面就跳一下。另外每次循环结束前都要av_packet_unref否则内存会持续增长尤其是长时间跑监控画面时特别明显。2.3 PTS 与缓冲不处理好就会越拉越卡解码器输出的 frame 自带 pts表示这一帧在原始流中应该显示的时间。如果你直接把它送到 UI而 RTSP 源端发送速度大于显示速度frame 会在解码线程里堆积。更坑的是某些摄像头在关键帧间隔很大时pts 会跳过一截积压的帧会瞬间全涌出来界面看起来就像放幻灯片。常见做法是记录第一帧的 pts 作为起点后续每帧的 pts 换算成毫秒与上一次显示时间比较落后就丢帧。代码可以这样写static int64_t base_pts_ms AV_NOPTS_VALUE; static int64_t last_display_ms 0; void handleDecodedFrame(AVFrame *frame) { if (frame-pts AV_NOPTS_VALUE) { av_frame_unref(frame); return; } // 把 pts 按解码器 time_base 换算成毫秒 int64_t pts_ms av_rescale_q(frame-pts, dec_ctx-time_base, AVRational{1, 1000}); if (base_pts_ms AV_NOPTS_VALUE) { base_pts_ms pts_ms; last_display_ms 0; } int64_t elapsed pts_ms - base_pts_ms; // 这一帧显示时间比上一次还早说明积压了直接丢掉 if (elapsed last_display_ms) { av_frame_unref(frame); return; } last_display_ms elapsed; convertAndSend(frame); }这里av_rescale_q是 FFmpeg 最安全的换算函数避免不同时间基直接乘除导致的精度问题。AV_NOPTS_VALUE表示没有有效时间戳。这种丢帧策略适合实时监控不适合本地文件播放本地文件要求每一帧都到位。另有一个辅助手段是调大 socket 接收缓冲区在 av_dict_set 中加入av_dict_set(opts, buffer_size, 1048576, 0);这个值对高码率摄像头有用但会稍微增加延迟一般先设 1MB再按实际画面情况上下调。3. 把 FFmpeg 接进 Qt 界面显示与事件循环的配合3.1 用 QTimer 驱动取流 vs 用线程很多初学朋友第一反应是放一个 QTimer定时去调用av_read_frame看了几帧就发现界面卡死。原因很简单av_read_frame是阻塞调用网络一抖动主线程就被堵住窗口连重绘都做不了。所以常见的做法是单独开一个线程做拉流和解码用信号槽把 QImage 发回主线程更新界面。这个 Demo 里我倾向于用 QThread而不是 QtConcurrent 或裸 std::thread。QThread 的优势是可以用quit()配合wait()优雅停止线程避免在析构时线程还在跑裸奔。一个简化的线程头长这样class RtspPlayer : public QThread { Q_OBJECT public: RtspPlayer(QString url, QObject *parent nullptr); ~RtspPlayer() override; void stop(); signals: void frameReady(const QImage image); protected: void run() override; private: QString url_; std::atomicbool running_{false}; };run 方法里就是我们在第二章写的循环只是把handleDecodedFrame换成发送信号。注意在线程中不能直接操作 QWidget必须通过信号把数据传回主线程信号参数会由 Qt 自动排到事件循环中。3.2 像素格式转换与 QImage 显示FFmpeg 解码出来的是 YUV420PQImage 能直接显示的是 RGB32 或 RGB888。中间需要一个sws_scale转换。转换上下文的创建和缓冲区分配经常被忽略这里写一个完整片段#include libswscale/swscale.h SwsContext *sws_ctx sws_getContext( src_w, src_h, AV_PIX_FMT_YUV420P, dst_w, dst_h, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL ); AVFrame *rgb_frame av_frame_alloc(); int num_bytes av_image_get_buffer_size(AV_PIX_FMT_RGB24, dst_w, dst_h, 1); uint8_t *rgb_buf (uint8_t*)av_malloc(num_bytes); av_image_fill_arrays(rgb_frame-data, rgb_frame-linesize, rgb_buf, AV_PIX_FMT_RGB24, dst_w, dst_h, 1);然后每一帧转换sws_scale(sws_ctx, (uint8_t* const*)frame-data, frame-linesize, 0, src_h, rgb_frame-data, rgb_frame-linesize); QImage image(rgb_frame-data[0], dst_w, dst_h, rgb_frame-linesize[0], QImage::Format_RGB888);这里有个大坑QImage 构造时默认不拷贝像素数据它只是保存了指针。如果 rgb_buf 在线程栈上或者在一次循环后被释放到主线程槽函数里再访问图像就变成悬空内存。所以发送前必须深拷贝QImage image_copy image.copy(); emit frameReady(image_copy);copy 会生成一份独立的内存信号槽传递时不再担心生命周期。3.3 一个可跑的显示循环示例把第二章和上面的片段拼起来run 方法大概长这样void RtspPlayer::run() { // 这里放第二章的 avformat_open_input 等初始化 // ... while (running_.load()) { int ret av_read_frame(fmt_ctx, pkt); if (ret 0) { // 超时或断流等待后重试 msleep(500); continue; } if (pkt-stream_index video_index) { avcodec_send_packet(dec_ctx, pkt); while (avcodec_receive_frame(dec_ctx, frame) 0) { // 可选的丢帧逻辑放这里 // 转换成 RGB sws_scale(...); QImage image(...); emit frameReady(image.copy()); av_frame_unref(frame); } } av_packet_unref(pkt); } }注意 while 条件里使用std::atomicbool不要用普通 bool否则停止信号可能在线程里永远看不到。窗口侧连接信号时也要考虑帧率过高导致 Qt 事件队列打满。这个 Demo 里可以简单点每帧都发实际项目里可以每秒只显示最新一帧其余丢弃这也是低延迟的另一个有效手段。4. Makefile 构建不用 qmake 也能把 FFmpeg 链起来4.1 为什么这个 Demo 选择 Makefileqmake 和 CMake 是 Qt 项目的主流但用 Makefile 演示有一个实际好处它能直观看清每一步编译和链接的依赖。特别是 FFmpeg 这类 C 库静态链接顺序、pkg-config 用法、Qt 头文件路径都在 Makefile 里一目了然。对于从源码包直接编的人Makefile 只需要改两三个路径就能跑起来不用引入复杂的 cmake 模块。另外在嵌入式场景交叉编译链经常只有 Makefile 支持掌握了这个写法后面改成交叉编译也会顺手很多。4.2 一个能编译通过的 Makefile 示例下面是一份适用于 Linux 桌面环境的 Makefile源码假设是 main.cpp 和 player.cppCXX g CXXFLAGS -stdc11 -Wall -O2 -fPIC # Qt5 头文件路径不同发行版路径不同按实际修改 QT_INCLUDE -I/usr/include/x86_64-linux-gnu/qt5 \ -I/usr/include/x86_64-linux-gnu/qt5/QtWidgets \ -I/usr/include/x86_64-linux-gnu/qt5/QtGui \ -I/usr/include/x86_64-linux-gnu/qt5/QtCore # FFmpeg 编译参数由 pkg-config 自动生成 FFMPEG_CFLAGS : $(shell pkg-config --cflags libavformat libavcodec libavutil libswscale) FFMPEG_LIBS : $(shell pkg-config --libs libavformat libavcodec libavutil libswscale) # Qt 库文件放在 FFmpeg 后面避免出现“库在源文件前”的链接错误 QT_LIBS -lQt5Widgets -lQt5Gui -lQt5Core # 注意pthread 放最后因为 FFmpeg 的 libavformat 依赖它 LIBS $(FFMPEG_LIBS) $(QT_LIBS) -lpthread SRC main.cpp player.cpp OBJ $(SRC:.cpp.o) TARGET rtsp_demo all: $(TARGET) $(TARGET): $(OBJ) $(CXX) $(CXXFLAGS) $(OBJ) -o $(TARGET) $(LIBS) %.o: %.cpp $(CXX) $(CXXFLAGS) $(QT_INCLUDE) $(FFMPEG_CFLAGS) -c $ -o $ clean: rm -f $(OBJ) $(TARGET) .PHONY: all clean这份 Makefile 最关键的点在链接顺序目标文件必须放在所有-l参数之前FFmpeg 库要在 Qt 库前面最后跟-lpthread。用pkg-config --libs会自动生成类似-lavformat的内容顺序一般可信但如果你手动指定静态库比如/usr/lib/libavformat.a就必须把被依赖的库放在后面否则链接器会报一堆undefined reference。编译时还有一个小技巧可以用qmake -query QT_INSTALL_HEADERS查看 Qt 头文件真实路径避免写死发行版路径。4.3 调整 RTSP 参数把延迟和超时控制住RTSP 播放的很多玄学问题最后都落在一组参数上。常见做法是抽一个函数统一设置void setupRtspOptions(AVDictionary **opts, bool useTcp, int timeoutMs) { av_dict_set(opts, rtsp_transport, useTcp ? tcp : udp, 0); char timeout[32]; snprintf(timeout, sizeof(timeout), %d, timeoutMs * 1000); av_dict_set(opts, stimeout, timeout, 0); // max_delay 是内部缓冲的最大时间单位微秒 av_dict_set(opts, max_delay, 500000, 0); }max_delay默认值可能偏大导致延迟一路累加。500000 微秒就是 500ms对多数摄像头够用。如果希望更低延迟可以再调小到 200ms但要在网络抖动时留余地。另外还可以设置probesize和analyzeduration来加速首帧出现这两个参数越小打开越快但可能拿不到完整的流信息建议只用于直播流。在 Makefile 里加一个宏开关很方便比如这样CXXFLAGS -DUSE_LOW_DELAY1源码里用#ifdef包裹低延迟参数一般测试完再决定要不要默认开。这个做法在多个摄像头源切换时特别好用不需要重新编译就可以通过命令行传参来覆盖默认配置。5. 避坑与常见问题RTSP 卡死、花屏、链接失败排查5.1 画面卡在打开阶段大多数是 UDP 惹的祸现象程序停在avformat_open_input或av_read_frame大约几十秒最后报错超时。原因RTSP 协议默认走 UDP 传输 RTP摄像头和客户端之间如果隔了防火墙或 NATUDP 丢包后 RTCP 反馈不回来握手就一直挂起。解决在av_dict_set里强制使用 TCP即rtsp_transporttcp。这招解决了差不多九成“打不开”的情况。如果还卡把stimeout设小一点比如 3 秒能很快返回错误便于日志定位。5.2 花屏或者绿屏像素格式和步幅不一致现象解码没报错但画面是一条条彩条或整体绿色。原因QImage 构造时用width * 3做行字节数但 sws 输出的 linesize 可能因为硬件对齐比width * 3大。解码源是 YUV420P直接当成 RGB 交给 QImage 就花屏。解决必须用av_image_get_buffer_size和av_image_fill_arrays来分配 RGB 缓冲区QImage 构造时传入rgb_frame-linesize[0]而不是自己算的 bytesPerLine。另外记得先sws_scale再copy。5.3 Makefile 链接报错顺序不对现象编译正常链接时一大堆undefined reference to av_read_frame或Qt5::Widgets。原因GCC 在使用静态库时按出现顺序解析符号如果你把库放在目标文件前面库里的符号对后面的目标文件不可见。FFmpeg 和 Qt 混用时还会因为 pthread 放错位置而报undefined reference to pthread_*。解决把所有-l参数放在源文件或目标文件之后顺序保持“FFmpeg 库 - Qt 库 - pthread”。这个顺序我踩过好几次了尤其把-lpthread放最前面时必炸。如果还不放心可以为每个库单独写一行并在 Makefile 注释里写明这个顺序不能乱改。5.4 延迟越来越大最后画面像慢动作现象刚打开时延迟几百毫秒五分钟后延迟几秒声音和画面完全错开。原因解码器内部或显示队列积压的帧没有丢弃。RTSP 源端是按帧率推流的而 Qt 的 repaint 频率可能低于帧率越积越多。解决按第二章的 PTS 丢帧逻辑来做或者给显示侧加一个最大队列长度。更彻底的办法是定期清空 AVFormatContext 的队列但这会丢关键帧可能引起画面闪烁需慎用。5.5 关闭窗口时崩溃现象点击关闭按钮后程序直接段错误或者报 double free。原因QThread 的 run 还在读帧主线程就执行了avformat_close_input上下文已经被释放。另一个是 QImage 临时量未 copy底层数据在 emit 后释放槽函数再访问就崩。解决析构函数中先调用stop()设置runningfalse然后wait()等待线程真正退出再按“解封 - 关解码 - 关输入”顺序释放。QImage 在发送前调用copy()这个万金油做法建议无脑用。6. 进阶用法把硬解码和低延迟选项加进去6.1 换硬解码只改一行如果 CPU 占用高可以考虑把软解换成硬解。以 NVIDIA 的 h264_cuvid 为例const AVCodec *decoder avcodec_find_decoder_by_name(h264_cuvid); if (!decoder) { // 回退到软解 decoder avcodec_find_decoder(codec_par-codec_id); }前提是你的 FFmpeg 编译时启用了对应硬件模块且驱动能正常工作。硬解码对 RTSP 的容错可能不如软解遇到损坏的帧会直接失败所以我的习惯是硬解为主、软解兜底。如果你用的是 Intel 平台可以把h264_cuvid换成h264_vaapi代码思路一样。6.2 低延迟参数组合低延迟并不是单个参数能解决的通常要一组搭配参数作用注意rtsp_transporttcp避免 UDP 丢包延迟略高于本地 UDP但稳定max_delay200000限制缓冲时长200ms 适合内网公网可能裂fflagsnobuffer跳过 FFmpeg 内部缓冲适合直播不适合文件probesize32768加快首帧分析太小可能拿不到编码信息analyzeduration500000限制流分析时长与 probesize 配合这些参数可以在运行时通过命令行传入也可以在源码里用av_dict_set预设。测试方法是先开一路流观察延迟增量如果稳定且没有丢帧就保持如果花屏必须调大 max_delay。有一次做视频墙项目我习惯性地把基础 Demo 跑通就上了环境结果画面延迟越来越大最后整面墙比真实时间慢了半分钟。排查了两天才发现解码队列里悄悄堆了两百多帧而显示端根本没有丢帧机制。从那以后我每次搭新的 RTSP 工程都会先写好 PTS 丢帧逻辑和低延迟参数再跑一轮半小时的延迟对比测试通过后才关掉调试打印。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑