FFmpeg与Qt实战:RTSP摄像头拉流解码并封装AVI落盘
2026/9/13 11:07:51 网站建设 项目流程

简介:一套基于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_transport=tcp 让 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_scale,YUV420P 原样交给编码器,避免一次无谓的色彩转换。

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 而你给编码器声明 YUV420P,avcodec_open2 直接报 invalid pixel format。gop_size 是两帧关键帧之间最多间隔多少帧,值越大文件越小,但播放器拖动条定位越不准,监控录像一般取帧率的 1 到 2 倍。max_b_frames=0 是为了让时间戳严格单调,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 的 extradata(SPS/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 继承 QThread,run 里跑 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绘制:等比例缩放代替setScaledContents

VideoPlayer 控件拿到 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_frames=0 和 tune=zerolatency 配合,录制链路端到端延迟控制在几百毫秒;如果还有更苛刻的实时性要求,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_transport=tcp 基本能解决。0 字节文件大概率是路径问题,注意 Windows 下路径里的反斜杠要转义,Qt 里用正斜杠更安全。时长不对则几乎都是时间戳问题,逐帧打印 pts 前后的值就能定位。

5.3 启停接口和global header的细节

录制结束时不能在 run 里直接 return,AVI 的索引段还没写。stop 方法的正确实现是置位一个原子标志,解码循环退出后依次调用 av_write_trailer、avio_closep、avcodec_free_context,最后再 QThread::wait 回收线程。用 terminate 强杀线程会让 AVI 文件缺少索引尾,播放器打不开。

如果录像拿到别的播放器里提示找不到 H.264 参数集,原因是 SPS/PPS 没有进 AVI 头。给解码器打开前加一行设置,让 x264 把参数集写入 extradata:

enc_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,最后调写入函数。

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

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

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

立即咨询