
简介一份基于FFmpeg与Qt的Windows桌面及摄像头推流源码工程主要面向需要实现实时音视频采集、编码、推流与录制功能的开发者。项目将推流和视频保存统一交给FFmpeg库完成支持音视频同步推流与录制由于FFmpeg和Qt均支持跨平台在Android、Linux、Windows上只需编译对应版本的FFmpeg库即可复用同一套逻辑代码。包内共201个文件约16.55MB以120个h头文件、5个cpp源码、8个dll运行库为核心同时包含30个html、19个png及qrc资源文件便于快速理解工程结构、界面配置与编译依赖。当前已有2920人学习下载源码涉及音频视频编码封装、颜色格式转换等关键实现细节适合有一定C与Qt基础、希望搭建推流Demo或深入研究FFmpeg调用流程的开发者参考。 先说个场景你要把自己电脑的桌面画面和摄像头画面同时推到内网或公网的流媒体服务器上给其他端做直播、多机位录制或者设备可视化监控。用现成的OBS能搞定但它终究是给别人用的工具一旦你想把这套推流能力嵌进自己的业务系统动态控制启停、切换画面来源、自动重连、拿到每路的实时状态OBS那条路就会让你越走越难受。所以我花了些时间用ffmpeg的libav*系列接口在Windows下做了一整套可供二次开发的推流源码覆盖了桌面采集gdigrab、摄像头采集dshow、H.264编码和RTMP推流到流媒体服务器这条完整链路。这篇文章不吹原理直接把这套源码的模块划分、选型逻辑、编译方式和几个让我折腾到半夜的坑一次讲透。这篇内容适合两类人一类是准备在自己的Windows产品里集成推流能力、又不甘心直接调外部exe的开发另一类是刚接触ffmpeg API、想拿一个能跑通的项目当学习样本的初学者。前者能直接拿走框架后者能顺着文章把采集怎么接、编码怎么配、时间戳怎么对、断了怎么重连这几个关键点一次理清。1. 这套源码存在的理由为什么不是OBS也不是命令行你在网上随手一搜推流的现成方案至少有四五种。我一开始也偷过懒直接写脚本调ffmpeg.exe -f gdigrab -i desktop -f dshow -i videoUSB Camera ...串几条命令看起来挺省事但真正用起来全是问题。命令行的核心痛点是不可控。你想让推流中途关掉桌面这一路、只留摄像头得杀掉整个进程再来一遍你想在对方断网时自动重连得自己在外面包一层守护脚本你想把每一帧的码率、丢包信息回传给业务系统几乎无从下手。进程级控制太粗不适合做产品集成。OBS的核心痛点是不可嵌入。OBS确实强场景切换、滤镜、混流全都有但它的本体是独立应用。你也许会用obs-websocket走HTTP/WebSocket指令远程遥控可一旦涉及深度定制比如采集前做图像处理、按业务逻辑动态切换多路源、将画面叠加自定义渲染依然绕不开改OBS源码那是另一个量级的工程。GStreamer在Windows上又太折腾。它的插件模型确实优美但Windows下跑起来依赖链比ffmpeg长得多出了问题排查成本高。对绝大多数做Windows桌面推流的人来说ffmpeg是生态最成熟、资料最多、坑也最透明的一条路。所以在这套源码里我直接用libavformat、libavcodec、libswscale这一层API把采集—编码—封装—推送完整封装成一个可在业务里调用的模块。推流端不再是一个黑盒exe而是你自己可控的代码。适用范围包括远程教学的双画面推流、门店/机房设备多路监控、活动多机位采集以及一切想拥有OBS能力但必须集成进自家软件的场景。2. Windows端两个关键采集源gdigrab与dshow的底层逻辑2.1 gdigrab桌面采集为什么用它又有哪些坑Windows下桌面采集的常见手段有GDI、DXGI Desktop Duplication、Mirror Driver和WGC。ffmpeg内置的gdigrab走的是GDI这套老路径也就是循环GetDC配合BitBlt把屏幕内容拷出来。它没有DXGI的高效率和硬件光标合成胜在兼容性好、实现简单——从Windows 7到Win11只要系统能显示桌面它就能采。API层面的用法相当直白AVInputFormat* ifmt av_find_input_format(gdigrab); AVDictionary* opts NULL; av_dict_set(opts, framerate, 30, 0); av_dict_set(opts, offset_x, 0, 0); av_dict_set(opts, offset_y, 0, 0); av_dict_set(opts, video_size, 1920x1080, 0); ret avformat_open_input(fmt_ctx, desktop, ifmt, opts);这里有一个容易被误会的点gdigrab的输入URL填desktop表示整屏采集但如果你想只抓屏幕上的某个区域靠的不是改URL而是offset_x、offset_y、video_size这三个私有选项的组合。带区域抓屏在只推某个软件窗口、不暴露桌面其他内容的场景里非常实用。GDI采集的效率不高尤其在高分辨率屏幕上抓全屏CPU开销明显。实测下来1080p30大约会占一个中等桌面CPU的5%-10%这个数做产品方案时可以接受但如果你要求的是游戏级的高速抓屏建议把通路换成DXGI再喂给编码器那是另一套思路。另外两个经验值一是HDR显示模式下gdigrab抓出来的帧在转换像素格式时容易偏色后文避坑章节会展开二是当桌面画面长时间静止不动时GDI采集并不会自动降帧CPU空转令人心疼。如果想省资源得在采集循环里对相邻帧做变化检测画面无变化时直接跳过编码和推流只维持心跳。2.2 dshow摄像头采集的设备枚举与参数细节摄像头在Windows下最常见的采集接口是DirectShowffmpeg里对应的输入格式名就叫dshow。相比桌面采集它的差异主要在先拿设备列表再按设备名打开。先用一条命令列出机器上所有采集设备这一步几乎每个接入摄像头的人都该先做ffmpeg -list_devices true -f dshow -i dummy返回结果里会列出video和audio两类设备名。拿到准确的名称后再构造输入URLchar url[256]; snprintf(url, sizeof(url), video%s, device_name); AVInputFormat* ifmt av_find_input_format(dshow); AVDictionary* opts NULL; av_dict_set(opts, video_size, 1280x720, 0); av_dict_set(opts, framerate, 30, 0); ret avformat_open_input(fmt_ctx, url, ifmt, opts);两个容易踩坑的细节第一设备名里如果带冒号、空格或中文URL构造时要注意。dshow解析URL时以video和audio为前缀标记理论上设备名里大部分字符都能直接传但你最好在拿到用户选择的设备名后做一次安全检查至少别把换行符之类的东西带进去。第二摄像头被别的进程占用时avformat_open_input会返回错误。很多产品第一个版本都没处理这个分支结果就是推流线程直接崩溃。正确做法是在打开失败时返回明确的业务错误码让上层UI提示用户关闭正在使用摄像头的应用。dshow同样支持音频采集用audio设备名指定。不过我这套源码里最初只做了视频因为把音频和视频两路采集、编码、同步叠在一起出问题的概率会成倍上升。第一版先把画面链路跑稳再扩展音频是最务实的节奏。3. 推流链路的工程细节编码、时间戳、封装与重连3.1 低延迟编码参数怎么配采集到的原始帧一般是BGR0/BGRA编码器不认识这种格式得先通过sws_scale转成YUV420P这一步用SwsContext完成属于标准操作。编码器选CPU上的libx264还是GPU上的h264_nvenc/h264_qsv取决于你的部署环境。桌面推流这类场景我推荐默认先走libx264不是因为它最强而是它参数调整空间大、问题容易排查。AVCodecContext* enc_ctx avcodec_alloc_context3(codec); enc_ctx-width width; enc_ctx-height height; enc_ctx-time_base (AVRational){1, 30}; enc_ctx-framerate (AVRational){30, 1}; enc_ctx-pix_fmt AV_PIX_FMT_YUV420P; enc_ctx-codec_type AVMEDIA_TYPE_VIDEO; // 码率按分辨率和场景确定1080p桌面建议2~4Mbps enc_ctx-bit_rate 3000000; enc_ctx-gop_size 60; // 关闭B帧降低延迟避免播放器缓存过多 enc_ctx-max_b_frames 0; // 这些参数通过私有选项传给libx264 av_opt_set(enc_ctx-priv_data, preset, ultrafast, 0); av_opt_set(enc_ctx-priv_data, tune, zerolatency, 0);三个关键选择的理由max_b_frames 0B帧能省码率但会引入额外的解码延迟和乱序。低延迟直播场景里B帧是第一个需要牺牲的东西。preset ultrafasttune zerolatency桌面内容变化大、编码压力高ultrafast换速度zerolatency让编码器不要为了提升压缩率而缓存帧从源头控制延迟。gop_size生成关键帧间隔60代表2秒一个关键帧兼顾拖进度条快速定位和关键帧太大导致卡顿的平衡。如果对延迟要求更极致可以缩到30甚至更小代价是码率升高。如果你用NVIDIA显卡且想压CPU占用h264_nvenc是很好的备选。它有一个需要特别注意的点部分硬编实现里SPS/PPS信息只在关键帧开头带一次有些播放器或者流媒体服务器中转之后会出现画面花一下才能恢复的问题必要时需要手动在每个关键帧前插入SPS/PPS。我在这套源码里做了处理统一在write_header时把extradata保存下来再按关键帧编号注入。3.2 时间戳处理推流中最容易被忽略的隐形杀手桌面和摄像头推流源码跑起来后最常见的故障就是画面卡住但CPU还在动或者声音正常但画面一帧一帧跳。十次有九次是时间戳没有处理好。核心规则只有一条每一帧交给编码器的时间戳必须是单调递增的并且要和编码器的time_base对齐。GDI采集和dshow采集不保证你以恒定的30fps拿到帧特别是桌面静止或摄像头自动曝光调整时帧间隔可能突然变大变小。如果你直接把系统当前时间塞进pts轻度情况是播放端时间轴抖动严重情况是编码器输出异常包。我的做法是维护一个独立的时间基准int64_t pts av_rescale_q( frame_index * (int64_t)1000000, // 按固定帧节奏推进 (AVRational){1, 1000000}, enc_ctx-time_base );也就是不管采集多快或多慢推给编码器的pts始终按固定帧率递增。这样从源头掐死了时间戳反弹的问题。画面确有跳帧的话对直播场景来说完全可接受总比整个流时间轴错乱强。3.3 RTMP封装与断线重连推流输出侧封装格式固定选FLV因为RTMP传输层使用的就是FLV容器。在ffmpeg里不需要特意设置AVOutputFormat的格式名直接按RTMP URL打开输出上下文它会自动匹配到对应的协议和封装。真正要设计的是推流的线程模型和重连机制。av_interleaved_write_frame是阻塞式调用网络状况差时会卡住线程所以绝对不要把它放在采集线程里跑。我这套源码里按三线程模型来组织采集线程负责从gdigrab/dshow读取帧做像素转换后放入帧队列编码线程从帧队列取帧编码得到编码后的包放入包队列推流线程从包队列取包写入输出上下文处理发送。断网是流媒体场景的常态。重连的第一版逻辑很粗糙任何一次写包失败就退出整个推流线程。后来改成检测到写失败→重置输出上下文→等待3秒→重新打开输入、重新配置编码器、重新推流。注意重连后不能继续沿用之前的pts计数否则播放端会出现几秒的音画不同步因为解码器的时间轴已经重新开始了。重连成功后把pts归零重算是我实测最稳的一种做法。4. 源码结构、编译与运行拿到代码后怎么跑起来4.1 目录结构与核心模块划分整套源码按功能拆成四个目录意图是让换一个输入源或换一种封装协议时只动局部代码src/ capture/ gdigrab_source.c # 基于gdigrab的Windows桌面采集 dshow_camera.c # 基于dshow的摄像头采集 encode/ h264_encoder.c # x264/nvenc统一封装 push/ rtmp_pusher.c # 输出管理、断线重连、pts维护 app/ main.c # 演示入口配置解析、循环控制 CMakeLists.txt核心结构上我定义了一个push_session贯穿全流程内部持有五个上下文对象输入上下文、解码/转换上下文、编码上下文、输出上下文和线程句柄。上层业务只需要push_session* session push_session_create(...); push_session_start(session); // 业务运行中... push_session_stop(session);这个接口设计参考了直播SDK的常见形态方便你后续替换成自己的业务逻辑。4.2 拿到并集成ffmpeg开发库Windows下最省事的一条路是用BtbN的GitHub自动构建产物里的dev包里面已经包含了include、lib和必要的DLL。注意选择与你的编译环境匹配的版本Visual Studio配x64架构的win64包MinGW配win64-gpl或对应的共享库版本。用CMake搭建工程时把dev包的路径整理好set(FFMPEG_ROOT D:/deps/ffmpeg-dev) include_directories(${FFMPEG_ROOT}/include) link_directories(${FFMPEG_ROOT}/lib) target_link_libraries(push_demo avformat avcodec avdevice avfilter avutil swscale )编译链接的坑主要在静态库还是动态库上。Windows下用静态库要注意每个库的额外依赖链比如libx264、libz等不如直接用DLL对应导入库来得干净。开发阶段我建议用DLL版本出了运行时问题也好用Dependencies Walker这类工具排查。运行时如果提示找不到avformat-*.dll这类错误解决方法是把ffmpeg dev包里的bin目录加入系统PATH或者把DLL拷到exe同目录。网上很多人说ffmpeg不是内部或外部命令通常是没把bin加入PATH跟开发引用两码事别混着处理。4.3 先在本地搭个流媒体服务器验证推流总得有地方接收。本地验证最简单的方案是装一个Nginx-RTMP或者SRS服务端我也用过ZLMediaKit对RTMP、WebRTC、SRT这些协议支持都比较完整做流媒体服务器搭建和测试都很方便。以SRS为例一条命令就能把RTMP服务跑起来./objs/srs -c conf/rtmp.conf然后再启动我的推流demo带三个参数桌面推流地址、摄像头推流地址、摄像头设备名push_demo.exe --screen rtmp://127.0.0.1:1935/live/screen --camera rtmp://127.0.0.1:1935/live/camera --device USB Camera打开VLC播放器拉流ffplay rtmp://127.0.0.1:1935/live/screen ffplay rtmp://127.0.0.1:1935/live/camera能在本地看到两路画面就说明采集—编码—推流—服务端转发这条路已经完全跑通接下来再往公网或内网环境扩展就有底了。5. 实测效果与踩坑记录5.1 实测数据参考教室内一台i5-12400、16GB内存的Windows 11主机上同时跑1080p30桌面推流和720p30摄像头推流的实测情况项目桌面推流(gdigrablibx264)摄像头推流(dshowlibx264)分辨率/帧率1920x1080 / 301280x720 / 30码率设置3Mbps1.5Mbps解码观察延迟1-2s1-2sCPU占用约8%-12%约3%-5%长时间运行稳定无内存持续增长稳定偶发丢帧可自恢复把编码器换成NVENC之后CPU占用能再降一半但画质在文字边缘上比x264要毛一点点看你更在意哪一头。5.2 重连策略的实测补充断网重连能不能成功还取决于一个细节服务端对流断开的判定超时。RTMP协议本身没有心跳保活机制如果推流端直接拔网线服务端可能要等几十秒才发现流断了这期间你去重连新连接会被旧session挡住。我在这套源码里做了两层处理一是在推流线程里每5秒检查一次网络状态主动发现异常就退出当前连接、等待重试二是重连前发一个FCUnpublish指令给服务端尽量让旧session尽快释放。实测下来断网30秒再恢复40秒内能自动回到推流状态对大多数非实时强控场景是够用的。5.3 几个具体的坑和解决办法HDR偏色问题。用gdigrab抓屏再转YUV420P时如果系统开启了HDR出来的画面会整体发灰、颜色不对。原因是gdigrab输出的像素格式和sws_scale默认的转换矩阵不一致。排查时先用命令行确认一下采集源的像素格式再做针对性转换。我最终的处理是判断输入帧的AV_PIX_FMT如果带BGR0之外的特殊格式先统一转成BGR24再做后续操作。设备名特殊字符导致dshow打开失败。摄像头名称里有括号或特殊符号时直接拼接URL偶尔会失败。我建议用列表接口枚举后由用户选择而不是让用户手输设备名同时在构造URL时对video后的名称部分做一次trim。双路采集共用同一个输入format context。最开始时偷懒想把桌面和摄像头塞进同一个输入上下文结果一路流结束了另一路也崩。后来老老实实为每路单独创建完整的capture—encode—push链路各管各的资源运行起来就清爽了。直播隐私合规。摄像头画面比桌面更敏感在工具里务必加上明确的推流前提示让使用者确认自己有权推送当前画面。合规意识和功能本身同等重要别在产品上线之后才想起这层。一路从命令行脚本调到这里我对推流这件事最深的体会是会调ffmpeg.exe只是入门能把采集、编码、推流做成一个可控、可嵌、可维护的模块才算真正掌握这条路。在Windows下用ffmpeg做桌面与摄像头双路推流坑基本都藏在demo里不会告诉你的环节——设备名特殊字符、像素格式转换、时间戳单调性、断线重连后的状态重置——希望这套源码和这篇文章能帮你少走几段我走过的弯路。本文还有配套的精品资源点击获取