拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FFmpeg与Qt实战:RTSP摄像头拉流解码并封装AVI落盘

简介一套基于FFmpeg与Qt的摄像头视频采集与存储系统源码面向音视频开发入门及中级学习者解决摄像头RTSP流实时预览与本地录制问题。整体转换思路清晰视频流经RTSP→YUV→H.264→AVI覆盖从网络拉流、解码显示到重新编码封装的完整链路。压缩包共21个文件主要有6个C源文件、6个头文件、2个UI界面文件、2个ico图标以及Qt工程配置和README说明源码结构简明便于对照学习。包体大小仅26KB不涉及大体积依赖库聚焦核心逻辑。目前已有1187人学习下载。借助这份资源读者可以快速搭建一个可运行的Qt音视频采集Demo并掌握FFmpeg在界面程序中的集成方式、RTSP流处理与AVI封装的基本写法还能理清视频帧在Qt界面中的刷新机制与AVI文件写入的流程适合作为课程设计或实际项目的前期参考。1. FFmpeg与Qt的摄像头采集存储为什么rtsp要转成avi再落盘这套源码是一个完整的 Windows MSVC Qt 工程解决摄像头视频采集与本地存储里最典型的需求把网络摄像头的 RTSP 流拉下来实时显示在 Qt 界面上并按 AVI 格式存到本地。数据链路是 rtsp→yuv→h264→avi不是直接把流复制进容器而是先解码成原始图像再重新编码封装。很多人用 ffmpeg 命令行一条命令就能抓流但换成 Qt 音视频开发就不一样了得自己管理解码器、编码器、封装器和界面线程的关系光调 ffmpeg.exe 做不了界面联动和可控启停。这个工程把读流到封装拆成可复用的 C 类再用 Qt 信号槽把视频帧送到界面适合正在做 Qt 音视频开发、或者会调 ffmpeg 命令但想升级成源码实现的人。下文按解码、编码封装、界面协作、排错四条线展开每段都给可抄的代码和参数依据。2. FFmpeg拉流解码链路AVFormatContext到YUV帧的获取与转换2.1 打开RTSP流rtsp_transport与stimeout必须显式设置摄像头 RTSP 流默认走 UDP包一丢画面就花。这个工程在 Convert 类里做采集第一步就必须在打开输入时固定传输协议为 TCP顺便给一个超时值否则摄像头掉线后 av_read_frame 会一直阻塞界面看着像卡死实际上挂在网络层。AVFormatContext *fmt_ctx avformat_alloc_context(); AVDictionary *opts nullptr; av_dict_set(opts, rtsp_transport, tcp, 0); // 强制 TCP 交错传输 av_dict_set(opts, stimeout, 5000000, 0); // 5 秒超时单位是微秒 if (avformat_open_input(fmt_ctx, url, nullptr, opts) ! 0) { qDebug() open rtsp failed: url; return -1; } avformat_find_stream_info(fmt_ctx, nullptr);rtsp_transporttcp 让 RTP 包走 TCP 交错通道丢包率明显下降代价是延迟略高局域网摄像头完全可接受。stimeout 的单位和 avio 的 timeout 不一样这里填 5000000 表示 5 秒摄像头断电或者网线断开时av_read_frame 会在这段时间内返回错误而不是永远等下去。拿到 fmt_ctx 后再用 avformat_find_stream_info 探测流信息这一步会解析 SDP 里的分辨率、帧率、编码格式后续解码器初始化依赖这些参数。找到视频流索引后用 codecpar 初始化解码器。FFmpeg 4.0 以后 AVStream 里的 codec 字段已经废弃网上老教程那种stream-codec-codec_id的写法在新版 SDK 里编译不过正确做法是先拿 codecpar 再拷到解码上下文。int video_idx -1; for (unsigned int i 0; i fmt_ctx-nb_streams; i) { if (fmt_ctx-streams[i]-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { video_idx i; break; } } AVCodec *decoder avcodec_find_decoder(fmt_ctx-streams[video_idx]-codecpar-codec_id); AVCodecContext *dec_ctx avcodec_alloc_context3(decoder); avcodec_parameters_to_context(dec_ctx, fmt_ctx-streams[video_idx]-codecpar); if (avcodec_open2(dec_ctx, decoder, nullptr) ! 0) { qDebug() open decoder failed; return -1; }RTSP 流里经常同时有音频轨所以必须用 video_idx 过滤直接对每个包解码会撞上音频包导致报错。avcodec_parameters_to_context 会把 SPS/PPS 这类 extradata 一并拷进去少了这一步 H.264 解码器会报参数缺失。2.2 解码主循环av_read_frame与send/receive模型解码循环是典型的 send_packet / receive_frame 两段式写法。新版本 FFmpeg 里 avcodec_decode_video2 已经删掉必须用 avcodec_send_packet 喂包、avcodec_receive_frame 取帧这个模型支持解码器内部缓冲多帧对存在 B 帧的 RTSP 流尤其重要。AVPacket *pkt av_packet_alloc(); AVFrame *frame av_frame_alloc(); while (av_read_frame(fmt_ctx, pkt) 0) { if (pkt-stream_index video_idx) { int ret avcodec_send_packet(dec_ctx, pkt); if (ret ! 0) { av_packet_unref(pkt); continue; } while (avcodec_receive_frame(dec_ctx, frame) 0) { handleYuvFrame(dec_ctx, frame); // 编码存储 界面显示 av_frame_unref(frame); } } av_packet_unref(pkt); }av_read_frame 每次只返回一个 AVPacket调用后必须 av_packet_unref 释放引用否则内存会随录制时长线性增长。receive_frame 返回 AVERROR(EAGAIN) 表示解码器还需要更多包直接继续读下一包即可。handleYuvFrame 里做两件事把 YUV420P 交给编码器写文件同时转成 RGB 给 Qt 显示。注意 frame 用完后马上 unref因为下一帧会复用同一块缓冲。2.3 YUV420P 的内存排布显示与编码的分叉点解码器吐出来的 AVFrame 是 YUV420P三个平面分别放在 data[0]、data[1]、data[2]。这里最容易踩的坑是 linesize 不等于 width解码器为了对齐会把每行补 padding直接按 width 去复制会得到斜的或者带绿边的图。平面data 下标1920x1080 数据量说明Ydata[0]1920 * 1080亮度完整分辨率Udata[1]960 * 540色度水平垂直各减半Vdata[2]960 * 540色度同上实际内存里每个平面的行大小是 linesize[i]可能比逻辑宽度多几十字节。处理时统一用 linesize 做步长不要自己按 width 算。显示路径需要把 YUV 转成 RGB24常用做法是初始化一个 SwsContext每帧复用而不是每帧都创建销毁。SwsContext *sws sws_getContext( dec_ctx-width, dec_ctx-height, AV_PIX_FMT_YUV420P, dec_ctx-width, dec_ctx-height, AV_PIX_FMT_RGB24, SWS_BILINEAR, nullptr, nullptr, nullptr); uint8_t *rgb_data new uint8_t[dec_ctx-width * dec_ctx-height * 3]; uint8_t *dst[4] { rgb_data, nullptr, nullptr, nullptr }; int dst_linesize[4] { dec_ctx-width * 3, 0, 0, 0 }; sws_scale(sws, frame-data, frame-linesize, 0, dec_ctx-height, dst, dst_linesize); QImage img(rgb_data, dec_ctx-width, dec_ctx-height, dec_ctx-width * 3, QImage::Format_RGB888); emit frameReady(img.copy());sws_scale 的入参 src_linesize 传 frame-linesize目标行的步长传 width*3整数倍长宽的转换不会出现缩放误差。SWS_BILINEAR 是质量和速度的均衡点追求帧率可以换 SWS_FAST_BILINEAR。img.copy()这步不能省QImage 从外部缓冲区构造时默认不持有数据直接传出去等 rgb_data 被 delete 后界面拿到的就是悬空指针。录制路径不走 sws_scaleYUV420P 原样交给编码器避免一次无谓的色彩转换。3. H.264编码与AVI封装编码器参数与时间戳重标定3.1 编码器初始化pix_fmt、gop_size、max_b_frames怎么填解码得到 YUV 帧后存储路径要重新编码成 H.264。这个工程里 Convert 类负责整条链路编码器的初始化参数直接决定录像文件的大小、拖拽精度和 CPU 占用。AVCodec *encoder avcodec_find_encoder(AV_CODEC_ID_H264); if (!encoder) { qDebug() H.264 encoder not found; return; } AVCodecContext *enc_ctx avcodec_alloc_context3(encoder); enc_ctx-width src_w; enc_ctx-height src_h; enc_ctx-pix_fmt AV_PIX_FMT_YUV420P; enc_ctx-time_base (AVRational){1, fps}; // pts 使用的基础时间单位 enc_ctx-framerate (AVRational){fps, 1}; // 告知编码器目标帧率 enc_ctx-gop_size fps * 2; // 每 2 秒一个关键帧 enc_ctx-max_b_frames 0; // 禁用 B 帧 enc_ctx-bit_rate 2 * 1000 * 1000; // 2 Mbps 起步 av_opt_set(enc_ctx-priv_data, preset, veryfast, 0); av_opt_set(enc_ctx-priv_data, tune, zerolatency, 0); if (avcodec_open2(enc_ctx, encoder, nullptr) ! 0) { ... }pix_fmt 必须和解码输出一致解码器吐 NV12 而你给编码器声明 YUV420Pavcodec_open2 直接报 invalid pixel format。gop_size 是两帧关键帧之间最多间隔多少帧值越大文件越小但播放器拖动条定位越不准监控录像一般取帧率的 1 到 2 倍。max_b_frames0 是为了让时间戳严格单调AVI 是老容器对 B 帧重排的支持很差去掉 B 帧后 pts 顺序和显示顺序完全一致封装逻辑简单得多。preset 和 tune 是 x264 的私有参数必须用 av_opt_set 设置到 priv_data。veryfast 在压缩率和 CPU 占用之间最平衡zerolatency 关掉 lookahead 缓冲录像时界面延迟不会持续累积。不调这两个参数编码器延迟可能到 1 秒以上录出来的视频时间轴对不上。3.2 AVI封装alloc_output_context2与codecpar拷贝编码器就绪后创建输出上下文。格式名直接传 avi不要依赖文件后缀推断万一保存路径没有后缀会创建失败。AVFormatContext *ofmt_ctx nullptr; if (avformat_alloc_output_context2(ofmt_ctx, nullptr, avi, save_path) 0) { qDebug() alloc avi muxer failed; return; } AVStream *out_stream avformat_new_stream(ofmt_ctx, nullptr); avcodec_parameters_from_context(out_stream-codecpar, enc_ctx); out_stream-time_base enc_ctx-time_base; if (avio_open(ofmt_ctx-pb, save_path, AVIO_FLAG_WRITE) 0) { qDebug() open output file failed: save_path; return; } avformat_write_header(ofmt_ctx, nullptr);avcodec_parameters_from_context 会把 H.264 的 extradataSPS/PPS写进 AVI 头播放器打开文件时先读这段数据才能初始化解码器。out_stream-time_base 直接复用编码器的时间基后面写包时少一层换算。avio_open 失败必须提前 return否则 write_header 会往一个空文件指针上写出现 0 字节文件。AVI 封装器默认按 OpenDML 扩展格式处理大文件录制超过 1GB 不会像老式 AVI 那样卡索引上限。3.3 写入循环时间戳重标定与interleaved写入每拿到一帧 YUV先送给编码器再从编码器取回 AVPacket最后写进 muxer。这一节是整条链路里报错最多的地方。int ret avcodec_send_frame(enc_ctx, frame); while (ret 0) { ret avcodec_receive_packet(enc_ctx, out_pkt); if (ret AVERROR(EAGAIN) || ret AVERROR_EOF) break; out_pkt-pts av_rescale_q(out_pkt-pts, enc_ctx-time_base, out_stream-time_base); out_pkt-dts av_rescale_q(out_pkt-dts, enc_ctx-time_base, out_stream-time_base); out_pkt-duration av_rescale_q(out_pkt-duration, enc_ctx-time_base, out_stream-time_base); out_pkt-stream_index out_stream-index; av_interleaved_write_frame(ofmt_ctx, out_pkt); av_packet_unref(out_pkt); }av_rescale_q 把编码器的时间基换算成 muxer 流的时间基。这一步漏掉AVI 文件的 duration 会是错的表现是播放器里进度条总长度不对、视频快进。av_interleaved_write_frame 会内部缓冲包并按显示顺序交错写入索引直接 av_write_frame 写出来的 AVI 很可能无法拖动进度条。每次循环末尾 unref 是必须的interleaved 写入会拿走包内部数据不释放的话内存持续上涨。4. Qt界面显示与线程模型VideoPlayer和Convert类怎么分工4.1 从v1.0.sln和v10.qrc看工程结构拿到压缩包先看文件清单v1.0.sln 说明是 MSVC 工程资源文件齐全属于能直接打开编译的状态。按命名约定界面和逻辑分得比较清楚文件职责main.cppQApplication 入口创建并显示主窗口V20.ui / V20.cpp / V20.h主窗口包含视频显示区域和录制控制按钮v10.ui / v10.cpp / v10.h早期版本窗口或参数配置面板VideoPlayer.cpp / vidoeplayer.cpp视频显示控件负责 QImage 渲染Convert.cpp / Convert.hFFmpeg 读流、解码、编码、封装核心类v10.qrcQt 资源打包一般放图标和样式v1.0.rc / resource.h / v1.0.ico / window.icoWindows 资源和程序图标VideoPlayer 和 vidoeplayer 两套播放器文件从命名看是一个控件的两个版本编译时看工程文件里实际包含哪一组。Convert 是纯逻辑类不依赖界面这种拆分的好处是命令行工具和 GUI 可以共用同一套采集录制代码也方便单独做单元测试。4.2 采集线程与UI线程解耦信号槽和帧队列的配合摄像头采集不能放在 UI 线程里av_read_frame 阻塞一次就卡住整个界面刷新。常见做法是让 Convert 继承 QThreadrun 里跑 2.2 节的解码循环每拿到一帧用信号发给 VideoPlayer。class Convert : public QThread { Q_OBJECT public: void stop() { stop_flag true; } signals: void frameReady(const QImage img); void errorOccurred(const QString msg); protected: void run() override; }; // 主窗口或 VideoPlayer 里建立跨线程连接 connect(convert, Convert::frameReady, videoPlayer, VideoPlayer::slotShowFrame, Qt::QueuedConnection);参数用 Qt::QueuedConnection信号在采集线程 emit槽函数在 UI 线程异步执行这样 QImage 的构造发生在工作线程绘制发生在 UI 线程两边不抢同一块内存。QImage 是隐式共享的跨线程传递时 copy-on-write成本可控。如果采集线程每帧都发信号UI 线程刷新不过来队列里会积压大量 QImage延迟越来越大。常见做法是加一个丢弃策略VideoPlayer 里记录上次刷新时间距离上一次不足 33 毫秒的直接忽略新帧保证显示刷新不超过 30fps录制线程不受影响。摄像头运动场景下这个策略最实用画面依然连续内存不会爆。4.3 paintEvent绘制等比例缩放代替setScaledContentsVideoPlayer 控件拿到 QImage 后重绘。QLabel 加 setScaledContents 虽然省事但会拉伸变形监控画面里车牌、人脸的宽高比错了就没法看。自定义控件的 paintEvent 里做等比例缩放才是这行的常规做法。void VideoPlayer::paintEvent(QPaintEvent *) { QPainter p(this); if (current.isNull()) { p.fillRect(rect(), Qt::black); return; } QImage scaled current.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); p.drawImage((width() - scaled.width()) / 2, (height() - scaled.height()) / 2, scaled); }SmoothTransformation 在 1080p 缩到控件大小时 CPU 开销明显录制状态下如果发现界面刷新吃 CPU可以把缩放模式换成立即模式 FastTransformation画面没有黑边清晰度损失在监控场景可接受。这个显示控件只负责画图不碰 FFmpeg 对象职责单一换成 OpenGL 渲染时也能平滑迁移。5. 录制参数调优与故障定位黑屏、0字节文件和时长不对5.1 按场景调整的三组参数第一组是码率。固定 bit_rate 在画面静止时浪费流量复杂场景又不够用。更推荐给 enc_ctx 设置 rc_max_rate 和 rc_buffer_size限制单帧波动或者直接用 x264 的 crf 模式用 av_opt_set 把 crf 设为 23画质由量化步长决定文件大小随场景自动浮动。第二组是关键帧间隔。监控录像需要回放拖动gop_size 越小定位越准文件越大。25fps 的流设置 50 就是 2 秒一个 I 帧回放时最长等 2 秒出清晰画面。如果只做证据留存、不常回看gop_size 拉到 100 也能接受。第三组是延迟相关。max_b_frames0 和 tunezerolatency 配合录制链路端到端延迟控制在几百毫秒如果还有更苛刻的实时性要求preset 换 ultrafast画质下降但编码速度翻倍。5.2 故障定位最常见的四种问题现象可能原因排查手段界面一直黑屏RTSP 走 UDP 丢包严重av_dict_set 强制 tcp抓包看 RTP 序号连续性avcodec_open2 报 encoder not foundFFmpeg 编译时没开 libx264命令行执行 ffmpeg -encoders 验证输出文件 0 字节avio_open 失败或写入循环没进入检查保存路径权限逐段打印返回值AVI 时长不对pts/dts 没有 av_rescale_q检查编码器与输出流 time_base 是否一致黑屏问题在局域网摄像头里最常见。udp 模式丢包后解码器花屏甚至卡在等待关键帧换成 rstp_transporttcp 基本能解决。0 字节文件大概率是路径问题注意 Windows 下路径里的反斜杠要转义Qt 里用正斜杠更安全。时长不对则几乎都是时间戳问题逐帧打印 pts 前后的值就能定位。5.3 启停接口和global header的细节录制结束时不能在 run 里直接 returnAVI 的索引段还没写。stop 方法的正确实现是置位一个原子标志解码循环退出后依次调用 av_write_trailer、avio_closep、avcodec_free_context最后再 QThread::wait 回收线程。用 terminate 强杀线程会让 AVI 文件缺少索引尾播放器打不开。如果录像拿到别的播放器里提示找不到 H.264 参数集原因是 SPS/PPS 没有进 AVI 头。给解码器打开前加一行设置让 x264 把参数集写入 extradataenc_ctx-flags | AV_CODEC_FLAG_GLOBAL_HEADER; ret avcodec_open2(enc_ctx, encoder, nullptr);最后提醒一个评审时必查的坑写入 muxer 前必须手动给 AVPacket 赋 stream_index编码器返回的包不认识 muxer 的流编号。漏掉这一行av_interleaved_write_frame 会静默丢包文件正常关闭大小也正常但里面没有任何画面。这个赋值和 av_rescale_q 放在一起顺序是先重标定时间戳再设 stream_index最后调写入函数。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门