
简介面向想用FFmpeg统一完成摄像头视频与麦克风音频采集、编码、录制或推流的开发者这份资源以完整示例工程演示了从DShow设备枚举到音视频封装输出的全链路方案。作者原本使用DirectShow开发但发现其框架细节多、上手成本高而FFmpeg通过DShow获取摄像头和麦克风数据的公开示例又较少经过一周实践将经验整理为三部分先通过ffmpeg.exe命令行枚举DShow设备并测试采集再介绍自定义采集程序的功能和用法最后解析采集、编码、封装、录制各模块的具体实现。RAR格式压缩包共162个文件22.75MB主要为源码工程含102个头文件、11个cpp源文件以及lib、dll、obj、pdb等编译中间产物和可执行exe工程以VS项目文件组织一并带有界面资源与说明文档可直接打开、编译和二次开发。目前已有1304人学习下载适合需要快速上手FFmpeg设备采集、想对照代码梳理音视频管线实现细节的开发者。1. 用 FFmpeg API 采集摄像头和麦克风为什么我不用命令行做视频会议或者录屏工具时很多人第一反应是直接用 ffmpeg.exe 拉起一条命令行参数一长串就完事了。但等产品真正落地你会发现命令行方案在「设备管理、音画同步、动态切换摄像头」这些场景下根本收不住。我接手过一个会议室采集项目里面就是用 system() 调命令行推流结果用户一切换摄像头整条管线就崩日志里只有一串看不懂的退出码。后来我改用 FFmpeg 的 C API 直接采集摄像头视频和麦克风音频核心逻辑也就 400 行左右但稳定性完全不一样。你可以拿到每一帧画面、每一段 PCM 数据自己做缩放、水印、断线重推也能精确控制编码参数和封装格式。这篇笔记就按我实际拆过的流程写从设备探测到编码推流每个环节的参数怎么设、坑在哪里都会给到可复现的示例代码。适合两类人一是准备把 FFmpeg 集成进自己产品的开发二是被命令行方案坑过、想搞清楚 API 版到底好在哪的熟手。2. 采集前的两台设备版本选择与设备名探测2.1 编译选型用系统包还是自己编先说版本。FFmpeg 的 API 在 4.x 和 5.x 之间有一个明显分水岭4.x 时代很多函数还保留旧签名到了 5.0 之后AVStream 的 codec 字段彻底废弃强制走 AVCodecParameters。我自己最早是从 FFmpeg 4.2 入手的那时候写采集代码还能看到 st-codec-codec_id 这种写法但如果你现在照着老教程抄编译直接报错。我一般建议直接用当前主流版本分支比如 5.1 或者 6.x。Linux 上直接用 apt 装 libavformat-dev、libavcodec-dev、libavdevice-devWindows 上可以用 vcpkg 装 ffmpeg[core,dshow,avfoundation]——注意 Windows 下要选带 dshow 的 feature否则摄像头设备名都枚举不出来。sudo apt-get install -y libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev libswresample-dev装完之后用 pkg-config 验证版本pkg-config --modversion libavformat libavcodec libavdevice这里要强调一点很多刚上手的朋友只装了 libavformat 和 libavcodec唯独漏了 libavdevice。这个库正是负责设备接入的摄像头、麦克风、屏幕录制全靠它。没有 libavdevice你连 avdevice_register_all() 这个函数都找不到更别提打开摄像头了。所以编译链路上如果报 undefined reference to avdevice_register_all先检查是不是漏了这个依赖。2.2 设备名探测三套平台三种玩法FFmpeg 的设备抽象层做得比较统一打开摄像头和打开一个文件在 API 上几乎一样都是 avformat_open_input()。但设备名在各个平台完全不同这一步能劝退一大半新手。Windows 上用 dshowDirectShow作为输入格式设备名可以直接用 ffmpeg 命令行枚举ffmpeg -f dshow -list_devices true -i dummy输出里会列出 video 设备名和 audio 设备名比如 Integrated Camera、Microphone Array (Intel)。这些名字带空格在 C 代码里传参时原样传入就行。macOS 上用 avfoundation设备名类似 FaceTime HD Camera 和 Built-in Microphone。Linux 上就简单了直接用 /dev/video0 和音频设备节点但要注意 Linux 这边视频设备要指定 -f v4l2音频设备要指定 -f alsa。我在做跨平台采集工具的时候给设备枚举单独写了一个函数核心思路是拿 AVFormatContext 的 fmt 去做匹配然后用 avdevice_list_devices() 枚举设备列表而不是靠硬编码设备名。具体代码如下AVFormatContext *ctx avformat_alloc_context(); AVDeviceInfoList *device_list NULL; ctx-iformat av_find_input_format(dshow); // 或 avfoundation / v4l2 if (avdevice_list_devices(ctx, device_list) 0) { fprintf(stderr, 设备枚举失败\n); return; } for (int i 0; i device_list-nb_devices; i) { AVDeviceInfo *dev device_list-devices[i]; printf([%d] %s (%s)\n, i, dev-device_name, dev-device_description); }这段代码枚举出来的设备名就是之后传给 avformat_open_input() 的 URL 参数。注意最后一个括号里的 device_description 往往比 device_name 更友好如果你的产品需要让用户在下拉框里选摄像头展示 description 会好很多。3. 打开采集通道视频帧率和分辨率怎么设才不翻车3.1 avformat_open_input 的 options 细节设备名拿到之后打开采集通道的代码本身不复杂但 options 的设置才是决定采集质量的关键。先看最基本的打开流程AVFormatContext *fmt_ctx NULL; AVDictionary *options NULL; int ret; av_dict_set(options, framerate, 30, 0); av_dict_set(options, video_size, 1280x720, 0); av_dict_set(options, pixel_format, nv12, 0); ret avformat_open_input(fmt_ctx, Integrated Camera, av_find_input_format(dshow), options); if (ret 0) { char errbuf[128]; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, 打开摄像头失败: %s\n, errbuf); return ret; }这里 framerate、video_size、pixel_format 三个参数分别控制帧率、分辨率和像素格式。但有个常见翻车点你设置了 1920x1080摄像头驱动不一定支持这时候 avformat_open_input 可能报错也可能静静地把分辨率降到 640x480 然后继续跑。前者还好排查后者会在录制视频时才发现画面糊得像座机。我习惯在 open 之后加一步校验读流信息的宽高和请求值对比。如果差距太大就直接主动关闭让上层换一套参数重试。这样至少不会产出让用户后期才发现损坏的素材。3.2 buffer 大小与像素格式选择dshow 底层是多线程的摄像头驱动按自己的节奏往回调里吐帧如果 FFmpeg 内部来不及处理帧就会被丢弃。一个有效的缓冲方式是设置 av_dict_set(options, rtbufsize, 64M, 0)这个参数控制实时缓冲的大小。缺省值往往偏小在高分辨率采集时经常出现画面卡顿。像素格式方面摄像头的原生输出一般是 YUYV 或 NV12很少直接给 RGB。在采集阶段尽量保持 YUV 系格式不做无谓的转换。需要显示预览时再转 RGB需要编码时直接用 YUV 递进去一来一回能省很多 CPU。av_dict_set(options, framerate, 30, 0); av_dict_set(options, video_size, 1280x720, 0); av_dict_set(options, rtbufsize, 64M, 0); av_dict_set(options, pixel_format, nv12, 0);还有个容易忽略的选项是 video_input_signal。在某些 USB 采集卡上如果不设置这个参数蓝屏或无信号的黑屏问题会周期性出现。我一般设为 auto 让驱动自动判断如果遇到黑屏再手动改成 composite 或 s-video。3.3 读帧循环AVPacket 的分配和释放设备打开就绪后读帧逻辑和读文件几乎一致核心就是 av_read_frame() 返回数据包。区别在于设备源的 AVPacket 数据是驱动内部分配出来的你得负责把它释放掉否则内存泄漏录十分钟就能涨到上百 MB。AVPacket *pkt av_packet_alloc(); while (!stop_flag) { ret av_read_frame(fmt_ctx, pkt); if (ret 0) { if (ret AVERROR(EAGAIN)) continue; // 设备暂时无数据重试 if (ret AVERROR_EOF) break; // 设备断开 fprintf(stderr, 读帧错误: %d\n, ret); break; } // pkt-data / pkt-size 在这里是有效的 // 编码或推流的逻辑放这里 av_packet_unref(pkt); // 关键释放内部引用 } av_packet_free(pkt);这段代码里两个关键点一是 EAGAIN 错误在设备采集时会频繁出现这是「缓冲区暂时空了」的标志不是致命错误continue 掉就行二是 av_packet_unref 之后 pkt-data 会被置空不要在后面继续访问。如果你用 av_packet_rescale_ts() 去调整时间戳记得在 unref 之前做。4. 音视频编码攻防帧率制、采样率与时间基三件套4.1 视频编码器的选择和参数采集到手的是原始 YUV 帧接下来要编码成 H.264。编码器选择上如果你只要软件编码libx264 是默认选择如果手上机器有 Intel Quick Sync 或者 NVIDIA NVENC用 h264_qsv、h264_nvenc 可以大幅降低 CPU 占用。我这里至少给出 libx264 参数AVCodecContext *video_enc_ctx avcodec_alloc_context3(NULL); video_enc_ctx-codec_id AV_CODEC_ID_H264; video_enc_ctx-width 1280; video_enc_ctx-height 720; video_enc_ctx-time_base (AVRational){1, 30}; video_enc_ctx-framerate (AVRational){30, 1}; video_enc_ctx-pix_fmt AV_PIX_FMT_YUV420P; video_enc_ctx-bit_rate 2000000; // 2Mbps video_enc_ctx-gop_size 60; video_enc_ctx-max_b_frames 2; video_enc_ctx-thread_count 4; ret avcodec_open2(video_enc_ctx, avcodec_find_encoder(video_enc_ctx-codec_id), NULL);bit_rate 这个参数的坑最明显。会议视频和影视流的需求完全不一样电影追求画质细腻2~4Mbps 很合适PPT 演示或者人物说话的镜头运动量不大1Mbps 就够。过高会让视频文件暴涨网络推流时还会占满上行带宽。直播场景我一般降到 1.5Mbps本地录制可以拉到 8Mbps。gop_size 决定每多少帧插入一个关键帧。直播里建议 2 秒一个关键帧60 帧这样观众中途加入直播能在 2 秒内收到画面。本地录像无所谓反而要调大一些以减少体积。4.2 音频重采样不要直接送原始 PCM麦克风拿到的音频数据往往是 48000Hz、浮点格式AV_SAMPLE_FMT_FLT而大多数编码器希望输入是 AAC 标准的 44100Hz 或者 48000Hz、16 位整形。你不做重采样直接送音频编码器轻则编码失败重则音画不同步越来越明显。这里我用 swresample 做重采样逻辑如下SwrContext *swr_ctx swr_alloc(); av_opt_set_int(swr_ctx, in_ch_layout, AV_CH_LAYOUT_MONO, 0); av_opt_set_int(swr_ctx, in_sample_rate, 48000, 0); av_opt_set_int(swr_ctx, in_sample_fmt, AV_SAMPLE_FMT_FLT, 0); av_opt_set_int(swr_ctx, out_ch_layout, AV_CH_LAYOUT_STEREO, 0); av_opt_set_int(swr_ctx, out_sample_rate, 44100, 0); av_opt_set_int(swr_ctx, out_sample_fmt, AV_SAMPLE_FMT_S16, 0); swr_init(swr_ctx); uint8_t *out_buf NULL; int out_samples swr_convert( swr_ctx, out_buf, src_nb_samples, (const uint8_t **)in_buf, src_nb_samples );声音这块容易犯的错是声道布局不匹配。麦克风一般是单声道但你往封装文件里写的时候很多播放器对单声道视频文件的兼容性差。我在对外推流或存档时都强制转成立体声。代价是多一点点 CPU换来的是播放器端的省心。4.3 时间基对齐音画同步的关键音画不同步几乎都是时间基没对齐造成的。视频流和音频流的时间基各自独立视频的 pts 基于 1/30 秒步进音频基于采样率步进。在写进 muxer 之前必须用 av_rescale_q 或 av_packet_rescale_ts 把它们统一到封装层的 time_base。AVStream *video_stream fmt_ctx-streams[video_idx]; AVStream *audio_stream fmt_ctx-streams[audio_idx]; pkt-pts av_rescale_q(pkt-pts, video_enc_ctx-time_base, video_stream-time_base); pkt-dts av_rescale_q(pkt-dts, video_enc_ctx-time_base, video_stream-time_base); pkt-duration av_rescale_q(pkt-duration, video_enc_ctx-time_base, video_stream-time_base);macOS 上 avfoundation 的音频时间基尤其诡异偶尔返回负的 pts。如果你发现录制出来的视频声音位置漂移优先检查 AVStream 的 time_base 是多少然后确认你确实是在往这个 time_base 上对齐。5. 封装推流的避坑记录三条血泪经验5.1 写 MP4 时 flv 扩展名引起的报错现象代码写好之后avformat_write_header() 一直返回 -22对应无效参数。文件路径明明是 output.mp4但错误提示和 FLV 相关。原因我在 open_output 时传的 URL 用了带大写后缀或者路径里夹了奇怪的字符FFmpeg 自动判断封装格式为 FLV。我后来在 avformat_alloc_output_context2 里显式指定了 mp4绕开了这个猜格式的过程。解决avformat_alloc_output_context2(oc, NULL, mp4, output.mp4) 里的 NULL 不要省直接把格式名写上。5.2 B 帧导致的延迟问题现象实时对话里画面延迟越来越大肉眼可见地半秒、一秒地在累积。原因开 B 帧之后编码器是重排序输出的dts 和 pts 不一样。我收到 I 帧之后立刻写文件没问题但到了 DTS 落后时就会卡缓冲表现就是延迟膨胀。解决直播这种场景直接把 max_b_frames 设回 0代价是压缩率低一点但延迟控制住了。如果是本地录像开 2 个 B 帧能省 10% 左右的码率代价是延迟高一些。5.3 摄像头拔掉之后的重连逻辑现象采集过程中用户拔掉摄像头进程直接崩溃日志停在 SEGV。原因av_read_frame() 返回的错误码没有处理设备移除的特殊情况我直接拿负值去访问了流数据。解决在循环里区分错误码AVERROR_EOF 表示设备正常断开此时释放上下文AVERROR(EAGAIN) 表示暂时性无数据继续读其他负错误码统一走「重新枚举设备→重试打开」的流程。if (ret AVERROR_EOF) { printf(设备断开准备重连\n); break; } else if (ret AVERROR(EAGAIN)) { continue; } else { fprintf(stderr, 采集错误: %s\n, av_err2str(ret)); break; }6. 验证采集质量的两种手法pict_type 与 pts 间距代码写完不等于完了。我每次把采集模块交付出去之前会强制跑一遍下面的验证流程拿编码后的 AVPacket 打印它的 pict_type 和 dts 间距确认编码器没有异常丢帧或跳帧。这一个检查项帮我挡掉了不少交付后的低级故障。具体做法是在编码循环里加一小段统计逻辑static int last_dts 0; if (pkt-dts - last_dts 3000) { fprintf(stderr, 警告: pts 跳变 %d - %d\n, last_dts, pkt-dts); } last_dts pkt-dts;正常情况 dts 间距是稳定不变的一旦出现 3000 以上的跳变说明解码端要么有丢帧、要么时间基算错了。这时候先查是不是 EAGAIN 分支被错误当成出口再查编码器的 time_base 配置。再配合 ffprobe 看输出文件的流信息是否符合预期ffprobe -show_streams -select_streams v output.mp4 ffprobe -show_streams -select_streams a output.mp4重点看视频流里面的 avg_frame_rate 是不是 30/1音频流的 sample_rate 和 channel_layout 是否和编码时一致。不一致的就得回去查重采样那段代码了。还有一个我常用的土办法把输出的视频文件放进剪辑软件拖到时间轴上直接看音轨的波形。如果波形起始点和画面第一帧能对上音画同步基本就是达标的。这个方法听着不入流但它能直观暴露 200ms 以上的不同步问题。希望这套验证流程能帮你少踩几个我踩过的坑。本文还有配套的精品资源点击获取