简介:一套基于Qt 5.9.6与FFmpeg的RTSP视频流播放器工程源码,面向具备C++/Qt基础、需要接入实时流媒体的开发者,可解决多路RTSP流同步播放、暂停及画面截图等典型需求。压缩包内共123个文件,约11MB,以83个头文件、8个cpp源文件为核心,并含pro/ui/qrc等工程配置、5个DLL与5个a链接库,打开即可编译运行。源码围绕frmmain主窗口与qffmpeg封装类展开,清晰展示三通道同时播放的实现:创建多个播放器实例绑定不同RTSP地址,通过截图按钮调用快照保存接口;FFmpeg解码库的接入方法也体现在工程文件中。已有5520人学习,适合远程监控、视频会议等场景的参考与二次开发,对照头文件与源码可快速掌握播放器框架及FFmpeg集成要点。
1. 为什么用Qt做RTSP播放器:需求与选型
拿到“Qt实现RTSP视频流播放器”这个题目,很多人第一反应是:直接拖个QMediaPlayer不就行了?实际动手就会发现,海康威视、小米摄像头、各种公网RTSP测试源,QMediaPlayer在Windows上对RTSP协议的支持非常看人品,经常出现能出声音不出画面、播放几秒就卡死、换个编码格式直接黑屏等情况。尤其是公司要对接的摄像头可能带着H.265编码、私有流格式,内置解码器的播放方案基本等于废掉。所以这个项目要解决的核心问题不是“写一个能播视频的界面”,而是要拿到RTSP流之后自己控制解码、渲染、缓存、断线重连这一整条链路,保证摄像头画面在Qt窗口里稳定、低延迟、随时能截帧和回放。
从技术角度看,这个项目的本质是“拉流+解码+渲染”三件事。拉流走RTSP协议,业内最成熟的方式是用FFmpeg的libavformat去解析RTSP地址,底层会自动处理TCP/UDP传输和RTSP信令;解码用libavcodec,H.264/H.265都能解,甚至MPEG4、MJPEG也能顺手搞定;渲染有两种主流做法,一是拿解码出的YUV帧转成QImage直接画在QWidget上,二是走OpenGL纹理上屏。考虑到大多数Windows工控机的显卡性能一般,而且项目偏业务功能而非极致画质,用QPainter绘制QImage是最直观、最不容易出问题的方案,也是新手最容易上手的路径。
这个项目适合谁?如果你已经在用Qt做上位机或桌面工具,突然被分配了“把摄像头画面集成到软件里”的需求;或者你想搞懂视频流从网络到屏幕到底经历了什么,不想再靠别人封装的现成组件,那这篇内容正好合适。我会把我实际踩过的坑、验证过的方案、以及最终能在Windows和Linux上稳定跑的代码骨架整理出来。下面先说说选型,因为这一步选错,后面全是在给架构还债。
1.1 解码方案取舍:FFmpeg、QtAV还是GStreamer
我把常用的几套方案都试过一遍,逐个说结论。首先是Qt自带的QMediaPlayer,优点是调用简单,缺点非常明显:它依赖系统自带的多媒体框架,Windows上走的是WMF,对RTSP的支持不稳定,而且没法拿到解码后的原始帧,意味着你不能做自定义分析、不能截帧,更不要想叠加OSD。如果只是本地MP4播放,用它没问题,但做RTSP播放器,我建议直接跳过。
第二套是QtAV,这是一套基于FFmpeg封装的Qt多媒体库,接口比原生FFmpeg友好,支持QML和Widgets,截图、GPU渲染这些功能都有。问题在于项目维护不算活跃,遇到新版Qt(比如5.15、6.x)总要自己编译适配,一旦摄像头传的是比较新的H.265封装,它内部的一些处理逻辑又可能跟不上。作为学习参考不错,但做产品交付我会更谨慎。
第三套是GStreamer,在Linux嵌入式和工控机上很流行,配合gst-rtsp-server还能自己搭个流媒体服务端。可它最大的问题是Windows端部署体积大、环境变量和插件路径容易搞出幺蛾子,团队如果平时不搞Linux,学习成本偏高。我的最终选择是直接用FFmpeg的C API,配合Qt做界面和线程管理。FFmpeg库本身不管界面,正好发挥Qt在跨平台界面上的优势,两边各干各的,责任清晰,出问题也好定位。而且FFmpeg的api在4.x和5.x/6.x之间保持得比较稳定,写一版代码能支撑很多年。
1.2 播放器功能边界与性能目标
动手前先明确功能边界,否则做着做着就容易跑偏。我这里定义的目标是:能输入RTSP地址并播放、支持暂停和恢复、画面能自适应窗口缩放、延迟控制在1秒以内、断流后能自动重连、能手动截取当前帧保存为图片。这基本覆盖了大部分安防集成和设备调试场景。至于回放录像、云台控制、音频播放这些功能,初期可以留出接口但不必一上来就做,不然项目很容易烂尾。
性能方面要重点盯住两个指标。一个是解码耗时,解码一帧1080p H.264平均应该控制在10到30毫秒,如果超过50毫秒,界面会明显掉帧,这时候要考虑是不是开了太多调试输出,或者丢包导致解码器频繁等待。另一个是内存占用,长期播放时内存应该稳定在300MB以内,如果持续上涨,大概率是队列积压或者QImage没有及时释放。这两条我用后面的代码框架都能实现,只要线程结构不走样。
2. 播放器核心架构与解码流程设计
RTSP播放器最忌讳的就是把解码、渲染、网络操作全塞进UI线程。网络稍有抖动,界面直接卡死,用户第一反应就是“垃圾软件”。所以架构上必须具备三个独立线程:主线程负责UI事件,解码线程负责拉流、解码、输出图像帧,渲染线程(或者定时器)负责把帧画到控件上。三者之间用队列传递数据,队列满了就丢旧帧,保证实时性。
控制关系上,用户点击播放按钮后,主线程启动一个CaptureThread,它内部打开RTSP地址并循环读取AVPacket,解码成AVFrame后转为QImage,塞进一个有限的帧队列。画面控件通过Qt的QTimer以固定频率从队列里取最新一帧并更新,这样即使解码速度偶尔抖动,界面也不会等帧等到卡死。暂停功能其实很简单:解码线程正常跑,但渲染定时器不再取帧,这样再恢复时画面不会跳跃太多,同时也保持了解码器的实时状态。
2.1 模块划分
我把整个工程拆成四个模块:网络会话模块、解码模块、渲染模块、控制模块。网络会话模块负责RTSP的打开、参数设置、网络协议选择(TCP/UDP)和重连逻辑;解码模块负责把AVPacket转成AVFrame,并处理像素格式转换;渲染模块只关心QImage怎么画到控件上,不管数据从哪来;控制模块把按钮事件映射成对线程和队列的操作。这样拆分的好处是,以后如果想支持本地文件播放,只需要新增一个读取本地文件的会话实现,解码和渲染完全不用动。
模块间依赖关系尽量单向:控制模块依赖网络会话和解码,解码产出的帧交给渲染,渲染不知道前两级的存在。这样做还有一个很实际的好处——单测好写。我试过把FFmpeg的打开过程封装成一个虚接口,测试时直接注入一个假的会话模块返回本地测试视频流,整个播放器的逻辑就能在CI环境里跑起来,不用依赖真实摄像头。
2.2 RTSP拉流与解码流程串讲
用FFmpeg打开RTSP时,核心步骤其实比很多人想象中简单:调用avformat_open_input打开地址,avformat_find_stream_info找出视频流索引,然后用对应的解码器参数avcodec_find_decoder找解码器,avcodec_open2打开解码器,最后循环调用av_read_frame拿包、avcodec_send_packet发包、avcodec_receive_frame收帧。这个过程对文件和对RTSP是一样的,区别只在于打开时设置的超时参数和传输协议选项。
一个特别容易忽略的点是,RTSP拉流默认可能走UDP,而UDP在跨网段、公网环境下丢包会直接导致花屏或卡死。我的建议是在avformat_open_input之前,通过AVOption设置打开视频流的超时时间为5秒,传输层强制设为tcp。TCP模式虽然会稍微增加延迟,但稳定性比UDP好太多,尤其是调试阶段,能省掉一大半莫名的“画面不显示”问题。另外在获取流信息后,最好手动把AVStream的avg_frame_rate算一下,用av_q2d转成实际的帧率值,后续做延迟控制和帧同步都用得到。
2.3 音视频同步的简化处理
如果是做视频监控类播放器,音频往往不是第一优先级,很多摄像头默认不带音频流。所以我先不铺开讲复杂的音视频同步算法,只说一个原则:如果不需要音频,就用帧率做基准,渲染定时器按1000 / fps毫秒的间隔刷新一帧;如果需要音频,让视频去贴合音频时钟,因为人耳对声音的敏感程度远高于眼睛。监控场景下,我建议初期直接忽略音频,把精力放在画面质量和延迟控制上,性能压力会小很多。
3. 实操落地:基于FFmpeg的播放器实现
下面进入能跑的代码阶段。我的环境是Windows 10 + Qt 5.15.2(MSVC2019 64位)+ FFmpeg 5.1。Qt安装时选MSVC套件,对应要装好Visual Studio 2019或2022的C++开发工具,否则编译会报找不到编译器的错。FFmpeg我比较推荐去gyan.dev或者BtbN下载编译好的Windows版本,因为自己从源码编译FFmpeg在Windows上不是一般的折腾,还要处理nasm、msys这些依赖,对做应用层开发的人来说性价比太低。
3.1 环境准备与工程配置
下载回来的FFmpeg压缩包解压后,目录结构是bin、include、lib三部分。bin里有avcodec-*.dll、avformat-*.dll、avutil-*.dll、swscale-*.dll这几个关键的动态库,程序运行时必须能找到它们,要么复制到exe旁边,要么把bin目录加到系统PATH。在Qt的.pro文件里,我是这样配置的:
INCLUDEPATH += D:/3rdparty/ffmpeg/include LIBS += -LD:/3rdparty/ffmpeg/lib \ -lavcodec \ -lavformat \ -lavutil \ -lswscale用CMake也类似,关键在于链接库的路径别写错,以及确保编译器的位数和库的位数一致。我见过不少同学报unresolved external symbol,查到最后发现是下载了32位的FFmpeg库,而Qt用的是64位的MSVC套件,两个架构搭不上,自然链接不过。
运行时你会发现光靠Qt的windeployqt不会帮你把FFmpeg的动态库带进去,因为它只识别Qt自身的运行库。我通常会写一个发布脚本,用windeployqt处理完Qt依赖后,再把FFmpeg的bin目录下那几个dll手工复制到发布目录,顺手用dumpbin或者Dependencies工具看一眼动态依赖,避免漏掉某个dll导致用户电脑上启动时弹“找不到avcodec-59.dll”。
3.2 解码线程的代码骨架
我直接给出一个能跑通核心流程的代码框架。首先是头文件里声明的核心类RtspCaptureThread,它继承QThread,暴露startPlay(const QString &url)、pause()、resume()、stop()这几个槽函数,同时通过信号frameReady(const QImage &image)把解码后的图像发出去。注意我这里用的QImage是值传递,表面看多了一次拷贝,实际Qt的隐式共享机制会在这类场景下减少不必要的深拷贝,性能可以接受。
class RtspCaptureThread : public QThread { Q_OBJECT public: explicit RtspCaptureThread(QObject *parent = nullptr); ~RtspCaptureThread() override; void startPlay(const QString &url); void pause(); void resume(); void stop(); signals: void frameReady(const QImage &frame); protected: void run() override; private: void openStream(const QString &url, AVFormatContext **fmtCtx, AVCodecContext **codecCtx, int *videoIndex); void decodeLoop(AVFormatContext *fmtCtx, AVCodecContext *codecCtx, int videoIndex); QString m_url; QAtomicInt m_paused; QAtomicInt m_stopped; };run函数是整个线程的主循环。我一般不在run里直接写死业务,而是拆成openStream和decodeLoop两个私有方法,方便后期替换成本地文件播放或者HLS播放。openStream的职责是打开URL和初始化解码器,decodeLoop则一个包一个包地读、解、转格式并发送信号。
3.3 打开RTSP流与初始化解码器
这是最关键的环节,我把代码贴出来,注释写清楚每一步的作用,特别是几个毁掉无数项目的参数。
void RtspCaptureThread::openStream(const QString &url, AVFormatContext **fmtCtx, AVCodecContext **codecCtx, int *videoIndex) { AVFormatContext *ctx = nullptr; AVDictionary *options = nullptr; // 设置打开超时5s,避免地址不可达时长时间阻塞 av_dict_set(&options, "stimeout", "5000000", 0); // 强制走TCP,公网播放更稳 av_dict_set(&options, "rtsp_transport", "tcp", 0); // 减少内部缓冲,降低延迟 av_dict_set(&options, "buffer_size", "1024000", 0); int ret = avformat_open_input(&ctx, url.toStdString().c_str(), nullptr, &options); av_dict_free(&options); if (ret < 0) { emit errorOccurred(QStringLiteral("打开失败,错误码: %1").arg(ret)); return; } ret = avformat_find_stream_info(ctx, nullptr); if (ret < 0) { avformat_close_input(&ctx); emit errorOccurred(QStringLiteral("获取流信息失败")); return; } *videoIndex = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); if (*videoIndex < 0) { avformat_close_input(&ctx); emit errorOccurred(QStringLiteral("未找到视频流")); return; } AVCodec *decoder = avcodec_find_decoder(ctx->streams[*videoIndex]->codecpar->codec_id); if (!decoder) { avformat_close_input(&ctx); emit errorOccurred(QStringLiteral("找不到对应解码器")); return; } *codecCtx = avcodec_alloc_context3(decoder); avcodec_parameters_to_context(*codecCtx, ctx->streams[*videoIndex]->codecpar); // 部分解码器需要设置线程数以加快解码 (*codecCtx)->thread_count = 4; ret = avcodec_open2(*codecCtx, decoder, nullptr); if (ret < 0) { avcodec_free_context(codecCtx); avformat_close_input(&ctx); emit errorOccurred(QStringLiteral("打开解码器失败")); return; } *fmtCtx = ctx; }注意stimeout的单位是微秒,所以我写了5000000代表5秒。这个参数特别重要,不然设备断电后程序可能会卡在avformat_open_input里几十秒,用户体验极差。另外avformat_find_stream_info有时会比较慢,如果手头RTSP源足够干净,也可以跳过,但我不建议跳过,因为后续很多参数依赖codecpar里的信息,比如宽高、编码格式。
3.4 解码循环与图像格式转换
decodeLoop里的逻辑围绕av_read_frame展开。注意RTSP流里除了视频包还有音频包和字幕包,判断packet.stream_index == videoIndex可以只处理视频包。拿到AVPacket后交给解码器解码,avcodec_send_packet和avcodec_receive_frame是FFmpeg新接口的标准姿势,老接口avcodec_decode_video2已经废弃,网上很多教程还在用,照着写会让编译器报一长串deprecation警告。
void RtspCaptureThread::decodeLoop(AVFormatContext *fmtCtx, AVCodecContext *codecCtx, int videoIndex) { AVPacket *pkt = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); AVFrame *rgbFrame = av_frame_alloc(); SwsContext *swsCtx = sws_getContext( codecCtx->width, codecCtx->height, codecCtx->pix_fmt, codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); int rgbSize = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecCtx->width, codecCtx->height, 1); QByteArray rgbBuffer(rgbSize, Qt::Uninitialized); av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, reinterpret_cast<const uint8_t *>(rgbBuffer.constData()), AV_PIX_FMT_RGB24, codecCtx->width, codecCtx->height, 1); while (!m_stopped.loadRelaxed()) { if (m_paused.loadRelaxed()) { msleep(20); continue; } int ret = av_read_frame(fmtCtx, pkt); if (ret < 0) { // 读到结束或错误,交给外层处理重连 break; } if (pkt->stream_index == videoIndex) { ret = avcodec_send_packet(codecCtx, pkt); if (ret == 0 || ret == AVERROR(EAGAIN)) { while (avcodec_receive_frame(codecCtx, frame) == 0) { sws_scale(swsCtx, frame->data, frame->linesize, 0, codecCtx->height, rgbFrame->data, rgbFrame->linesize); QImage image(rgbBuffer.constData(), codecCtx->width, codecCtx->height, QImage::Format_RGB888); emit frameReady(image.copy()); } } } av_packet_unref(pkt); } sws_freeContext(swsCtx); av_frame_free(&rgbFrame); av_frame_free(&frame); av_packet_free(&pkt); }这里有个小细节,QImage image(...)直接指向rgbBuffer的内存,这个buffer在函数栈上,信号发出时如果在线程内直接消费没有问题,但跨线程传递就必须image.copy()做一次深拷贝,否则接收方看到的是一块已经失效的内存。我之前犯过这个错,画面时常花掉,排查了好久才发现是image生命周期的问题。rgbBuffer用Qt::Uninitialized而不是QByteArray(size, '\0'),是为了避免一次多余的内存清零,视频帧数据马上会被sws_scale覆盖,清零是纯浪费。
3.5 界面渲染与帧率控制
接收端我用了QTimer定时从最新帧队列里取图。队列可以用QQueue<QImage>加锁,或者用QSharedPointer传帧。更简单的做法是让解码线程直接信号发QImage到主线程,在连接信号时用Qt::QueuedConnection,然后在主线程的槽函数里直接更新QLabel或QWidget。但这种方式有一个问题:如果解码速度比显示速度快,队列里堆积的事件会让UI越追越累,内存也会上涨。所以我在实际项目里用了一个带最大长度的环形队列:解码线程往里塞,UI定时器每次取最新一帧,队列满了就淘汰最旧的那帧。这样延迟始终被压制在一两帧以内,UI稳定不膨胀。
void PlayerWidget::showVideo() { QImage frame = m_frameQueue->latestFrame(); if (frame.isNull()) return; if (m_keepAspect) { QPixmap pix = QPixmap::fromImage(frame); pix = pix.scaled(ui->videoLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); ui->videoLabel->setPixmap(pix); } else { ui->videoLabel->setPixmap(QPixmap::fromImage(frame)); } }帧率控制方面,我一般设置QTimer间隔为33毫秒左右,对应约30fps。注意如果是25fps的摄像头,用33毫秒也能覆盖,因为取的是最新帧,所以显示频率略高不会造成重复播放,只是空转几次,性能影响很小。窗口拉伸时,用Qt::KeepAspectRatio防止画面变形,这对16:9的监控画面尤其重要。
4. 常见问题与排查技巧实录
这部分我按真实项目中踩坑频率排序,挑几个最容易让新手崩溃的问题讲。每个问题我都会给出现象、原因和解决方案,方便你当速查表用。
4.1 程序发布后提示“no Qt platform plugin could be initialized”
这个错误常年出现在Windows部署场景,现象是双击exe直接弹窗,或者命令行运行时报no Qt platform plugin could be initialized, reinstalling the application may fix this problem。原因很简单:Qt的platform插件(比如qwindows.dll)没有在exe同级的platforms目录下。
解决办法是运行windeployqt,并确保它与你编译用的Qt版本完全一致。命令大概是windeployqt your_app.exe,它会在exe所在目录自动生成platforms、styles、imageformats等文件夹,并拷贝必需的Qt运行库。如果用了FFmpeg,还要手动把FFmpeg的dll拷过去,前面已经提过。
我这边实际遇到过一个更隐蔽的情况:在开发机上没问题,拷到别人的电脑上就报这个错,最后发现是原来系统里装过别的Qt版本,环境变量PATH里残留了另一个版本的Qt bin目录,程序启动时加载到了错误的qwindows.dll。解决方式是发布时做成免安装绿色版,并且尽量不带依赖系统路径,把发布目录复制到干净环境里验证。
4.2 RTSP地址不对导致画面一直出不来
海康威视摄像头的RTSP地址有标准格式:rtsp://用户名:密码@IP地址:554/Streaming/Channels/101。其中101表示主码流,102表示子码流,大华等品牌也有类似但不同的路径规则。如果地址里密码包含特殊字符,比如@、:,一定要做URL编码,否则地址解析直接错乱。
小米摄像头取流则相对灵活,你可以在米家App里开启RTSP服务,然后把生成的地址配到播放器里。这种源本身没问题,但很多家用摄像头码率不高,主码流可能只有2Mbps左右,解码难度很低。如果你发现播放器连本机(localhost)的RTSP流卡顿,反而应该先检查Wi-Fi信号和码率设置,而不是怀疑FFmpeg。
我建议调试阶段先用公开的RTSP测试地址验通代码,比如网络上常见的海康演示源、大华演示源,或者自己用FFmpeg推一个本地RTSP流服务。先用ffplay rtsp://xxx确认这个地址在电脑上能播放,再放到Qt程序里,能少排查一半的问题。项目刚起步时不要直接用客户的摄像头,因为一旦拉不到流,你分不清是网络问题、摄像头配置问题还是自己代码问题。
4.3 播放花屏、绿屏和崩溃
花屏有很多种诱因。最常见的是编码数据不完整,这往往发生在RTSP走UDP且网络丢包严重的时候,解决方案就是之前在openStream里设置rtsp_transport=tcp,可以让丢包率大幅下降。其次是解码后的像素格式处理错了,比如解码器输出AV_PIX_FMT_YUV420P,但你用sws_scale转到RGB24时参数写错,导致画面错位。这时候可以先用ffprobe或者打印codecCtx->pix_fmt确认实际格式。
崩溃问题要多留个心眼。我遇到过的最典型崩溃是视频宽高为0时调用sws_getContext,直接触发空指针访问。原因是一些特殊视频流在find_stream_info后依然没有填上宽高,或者设备端传过来的偶发错误帧。建议在初始化swsContext之前加一个判断:width <= 0 || height <= 0就跳过解码等待下一帧。另外av_read_frame返回错误后要立刻break而不是继续循环,否则会出现无效包和无限循环打转的坑。
4.4 延迟越来越大,如何优化
监控场景对延迟要求不算苛刻,但要做到低延迟还是有几条路。首先把FFmpeg打开时的buffer_size调小一点,但别太小,否则网络抖动会让画面频繁卡顿。其次在显示端用“取最新帧”而不是“排队按序播放”,即使偶尔跳过几帧老画面,视觉上也比越拖越慢舒服得多。也可以尝试把avcodec_receive_frame循环改成每读1个包就最多解出来1帧,牺牲一点解包效率换更低的缓冲。
如果用的是GStreamer方案,还可以参考它的rtpjitterbuffer属性和sync=false设置,但既然选型已经定了FFmpeg+Qt,就在我这里说的几个参数上找空间。实测下来,普通的1080p摄像头,通过TCP拉流、限制缓冲区、最新帧显示,延迟稳定在300到500毫秒是完全没问题的,肉眼感觉基本是实时的。
4.5 与Qt绘图功能扩展的联动
播放器跑起来之后,很多人会顺手在画面上叠加一些动态曲线或状态信息。比如从摄像头解码出的图像数据本身不做分析,但工控场景里经常要把传感器温度、电工参数以时域波形或者频域频谱的形式叠加到监控界面旁边。这时候Qt自带的QCustomPlot就很好用,我见过一个项目把本课题的播放器再接一路物理量采集,用定时器刷新心电波形,效果很直观。Qt里时域图转频域图一般用kissfft或FFTW做FFT,再把结果灌给QCustomPlot的graph接口,和视频渲染完全是两码事,互不干扰,值得作为播放器成型后的扩展方向。
5. 一些实用的工程建议
最后说几点对实际项目更有价值的经验,不想看详细代码的人可以直接从这里收获。
第一,线程停止一定要用原子变量而不是terminate()。QThread::terminate()会直接终止线程,FFmpeg正在解码时内部可能持有锁,强行终止轻则内存泄漏,重则整个进程崩溃。我这里是设m_stopped标志,并且在av_read_frame阻塞前设置超时,这样即使当前正在等待网络数据,也能在5秒内醒过来判断退出标志。
第二,RTSP断线重连不能简单地在decodeLoop里加个死循环去重连,因为FFmpeg打开失败本身可能有5秒超时,如果断网时间久了,线程会一直阻塞在打开流程里,UI上无法处理退出的请求。我的做法是在解码线程发现av_read_frame返回错误后,发送一个connectionLost信号,由主线程统一决策:是停止播放、弹窗提示,还是启动一个独立的定时器在后台延时重连。真实产品里,我通常用指数退避策略:1秒、2秒、4秒……最多30秒重试一次,避免频繁重连把摄像头端打崩。
第三,FFmpeg日志在Qt里很吵,默认会往stderr打一堆信息。调试时确实有用,但发布时建议用av_log_set_level(AV_LOG_ERROR)关掉大部分输出,或者注册一个回调函数把日志重定向到自己的文件系统。不然客户现场一旦出问题,跨线程打日志还可能引入新的时序问题,排查起来会麻烦很多。
这个项目做到最后,给我的感觉是:RTSP播放器真正的难点不在界面,而在工程细节。你用QTimer刷新一帧图很容易,难的是把解码线程和渲染线程的生命周期管好,把异常路径全都设计到位。按我上面这套结构,功能扩展性也很强,后续接音频、接录像、接AI分析,都能在现有模块上做加法。如果你也正在被“摄像头画面怎么嵌进Qt界面”这个问题困扰,建议先不要急着抄一堆代码,把一个最简单的RTSP地址用FFmpeg命令行ffplay播出来,再一步步挪到你的工程里,踩坑的成本会低很多。
本文还有配套的精品资源,点击获取