ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FFmpeg API实战:从零构建摄像头音视频采集管线

2026/9/9 23:55:35 拓冰建站 浏览量
FFmpeg API实战:从零构建摄像头音视频采集管线 简介这是一份基于FFmpeg API实现摄像头视频与麦克风音频采集的完整C工程源码包面向希望避开DirectShow繁琐框架、用FFmpeg统一完成采集编码录制或推流的开发者。作者结合一周实战经验先通过ffmpeg.exe命令行演示如何枚举DShow设备并测试采集再给出程序功能与用法最后拆解采集、编码、封装、录制各模块实现能帮助读者快速建立起基于DShow的FFmpeg采集方案。资源共162个文件以102个h头文件和11个cpp源文件为主体同时附带lib/dll依赖库、工程配置与项目文件压缩包22.75MB属于可直接打开查看的VS工程代码中涉及主程序框架、设备选择对话框、视频显示窗口、音视频输入输出封装等模块便于对照学习。目前已有1303人学习下载适合有一定FFmpeg基础、想用DShow源头做音视频采集的开发者参考。 做采集这块我一直有个结论命令行 ffmpeg 适合临时抓个摄像头、录个屏幕但要真正把采集能力嵌进自己的产品比如给实时视频叠加识别结果、按业务逻辑动态切换设备、采集的同时做流媒体分发你必须直接用 FFmpeg API。这篇文章我用一个实际项目的思路把“用 FFmpeg API 采集摄像头视频和麦克风音频”的完整链路讲透设备枚举、打开采集、音视频同步、编码保存以及我在 Windows dshow、Linux v4l2/alsa、树莓派 ov5647 上踩过的那些坑。内容主要面向对 FFmpeg 命令行有基本了解、想往底层 API 走的开发者也适合正在做视频会议、安防监控、图像识别、直播推流的工程师。文章以 Windows 平台 dshow 为主但整体思路可以平移。1. 整体设计思路先搞懂采集链路再写代码1.1 为什么非要 FFmpeg API而不是直接拉命令行命令行做采集很简单一条-f dshow -i videoUSB Camera就能出流。但命令行的本质是一个完整封装好的可执行程序你能做的只有传参拿不到中间每一帧没法在采集同时插入算法设备异常时无法精细控制重试策略。API 调用的价值就是把黑盒拆开av_read_frame读到一个 AVPacket 后你既可以存文件也可以马上解码成 AVFrame 转给 OpenCV、TensorRT还可以把同一个包同时丢给推流器。我之前做过一个防疫测温小工具要用普通 USB 摄像头取流每帧同步喂给人脸检测模型做识别再叠加上温度框推给客户端。命令行方案根本做不了这种“边采集边处理”的流水线API 方案才能真正把“取流—处理—输出”的节奏握在自己手里。1.2 采集链路的四个核心模块很多新手第一次看 FFmpeg源码时被一堆libav*库搞晕其实采集链路只涉及四个模块模块作用生活类比libavdevice设备输入抽象层dshow、v4l2、alsa、avfoundation 都挂在这一层相机镜头libavformat打开输入、探测流信息、读取 AVPacket相机机身libavcodec把压缩数据解码成原始图像/音频修图软件libavutil提供帧结构、像素格式、时间基等基础工具胶卷一句话总结libavdevice 负责“认识设备”libavformat 负责“从设备读数据”libavcodec 负责“把数据变成能用的样子”libavutil 是所有环节的地基。你要做的就是在初始化时把前三者串起来形成一个“镜头—机身—修图软件”的完整管线。1.3 先做减法采集不一定非要解码很多人一上来就准备解码器、编码器、滤镜其实纯采集场景可以非常轻。如果你的需求只是“把摄像头原始流存成文件”或者“把流转发出去”你完全不需要解码av_read_frame拿到的 AVPacket 本身就是压缩后的数据直接写给 muxer 就行。但如果你要“实时分析图像”或“做本地预览”那就一定要解码并且要做像素格式转换。这个取舍会影响整个程序的结构动手写代码前一定先想清楚。2. 环境准备与设备枚举找到摄像头和麦克风2.1 开发库安装与头文件配置Windows 上推荐从 BtbN 或 gyan.dev 下载 FFmpeg 的 shareddev 版本dev 包里包含头文件和导入库。下载后需要做的三件事把bin目录下所有 dll 放到可执行文件旁边或者加入系统 PATH在 IDE 里设置头文件搜索路径为 dev 包里的include目录链接器里加入avformat.lib、avdevice.lib、avcodec.lib、avutil.lib。Linux 上更简单sudo apt install libavdevice-dev libavformat-dev libavcodec-dev libavutil-dev用 vcpkg 也可以vcpkg install ffmpeg:x64-windows但我个人觉得 Windows 上直接下二进制包最省事不用折腾源码编译。源码编译适合要做裁剪的嵌入式环境桌面开发直接用官方构建版本就行。2.2 用 avdevice_list_devices 枚举设备打开设备之前你得先知道设备叫什么名字。命令行时代可以用ffmpeg -list_devices true -f dshow -i dummy查设备列表API 环境下对应的函数是avdevice_list_devices。#include libavdevice/avdevice.h #include libavformat/avformat.h #include libavutil/avutil.h void list_dshow_devices() { avdevice_register_all(); AVFormatContext* fmt_ctx avformat_alloc_context(); const AVInputFormat* ifmt av_find_input_format(dshow); if (!ifmt) { fprintf(stderr, 当前 FFmpeg 不支持 dshow\n); return; } fmt_ctx-iformat ifmt; AVDeviceInfoList* device_list NULL; int ret avdevice_list_devices(fmt_ctx, device_list); if (ret 0) { fprintf(stderr, 枚举设备失败错误码%d\n, ret); avformat_free_context(fmt_ctx); return; } for (int i 0; i device_list-nb_devices; i) { AVDeviceInfo* dev device_list-devices[i]; printf(设备[%d]: %s\n, i, dev-device_name); if (dev-device_description) printf( - 描述%s\n, dev-device_description); } avdevice_free_list_devices(device_list); avformat_free_context(fmt_ctx); }这个函数输出的是设备名而不是完整的video地址。在 dshow 下摄像头设备名通常是Integrated Camera、USB Camera之类的名称麦克风则是Microphone (Realtek High Definition Audio)这种。拿到名字后打开输入时要用video设备名或audio设备名拼成 URL。Linux 下的 v4l2 设备名通常是/dev/video0Alsa 设备名是hw:0open_input 时直接传这些路径即可不需要枚举所以我的实际项目中通常只在 Windows 上保留枚举逻辑。嵌入式平台比如树莓派v4l2 下常见ov5647摄像头设备同样是直接传/dev/video0但要注意格式列表检查这部分我用单独代码演示。2.3 设备命名里的坑dshow 设备名如果包含冒号会导致 URL 解析错乱因为 dshow 的 URL 本身就是videoxxx:audioxxx这种冒号分隔的结构。遇到这种设备名需要在设备名里对冒号做转义用\:或者通过options传但我实际测试下来某些设备驱动对转义的处理很迷更可靠的方式是只开视频不混音分别建两个采集上下文最后在业务层合并时间戳。这个方法后面会细说。3. 核心采集流程从打开设备到拿到每一帧3.1 打开摄像头与麦克风设备枚举做好后就可以正式打开设备了。以 Windows dshow 同时采集视频和音频为例AVFormatContext* create_dshow_ctx(const char* video_dev, const char* audio_dev) { AVFormatContext* fmt_ctx NULL; const AVInputFormat* ifmt av_find_input_format(dshow); if (!ifmt) return NULL; // 拼接视频和音频设备地址 char url[512]; if (video_dev audio_dev) { snprintf(url, sizeof(url), video%s:audio%s, video_dev, audio_dev); } else if (video_dev) { snprintf(url, sizeof(url), video%s, video_dev); } else { snprintf(url, sizeof(url), audio%s, audio_dev); } AVDictionary* options NULL; // 环形缓冲区大小摄像头 1080p 30fps 下建议给足不然容易丢帧 av_dict_set(options, rtbufsize, 128M, 0); // 指定视频分辨率与帧率避免驱动用默认的奇怪参数 av_dict_set(options, video_size, 1280x720, 0); av_dict_set(options, framerate, 30, 0); // 指定采样率如果声卡驱动默认 48k 但你要 44.1k可以在这里定 av_dict_set(options, sample_rate, 48000, 0); av_dict_set(options, channels, 2, 0); int ret avformat_open_input(fmt_ctx, url, ifmt, options); if (ret 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, 打开设备失败%s\n, errbuf); return NULL; } av_dict_free(options); return fmt_ctx; }注意rtbufsize很关键。dshow 内部用环形缓冲保存数据缓冲小了如果主线程来不及读就会直接丢包表现就是画面卡顿、花屏、音频断续。我的经验是 720p 30fps 至少给 64M1080p 直接给 128M别省这一点内存。3.2 定位视频流与音频流设备打开后要拿到内部的流信息再分别找到视频和音频对应的 stream indexvoid find_stream_indexes(AVFormatContext* fmt_ctx, int* video_idx, int* audio_idx) { *video_idx -1; *audio_idx -1; for (unsigned int i 0; i fmt_ctx-nb_streams; i) { AVStream* stream fmt_ctx-streams[i]; if (stream-codecpar-codec_type AVMEDIA_TYPE_VIDEO) { *video_idx (int)i; } else if (stream-codecpar-codec_type AVMEDIA_TYPE_AUDIO) { *audio_idx (int)i; } } }这一步没什么花活但一定要判断返回的 index 是否为 -1因为有些廉价 USB 摄像头只出视频流不带音频直接对 -1 取 stream 会段错误。我见过太多线上事故都是没做这个判断。3.3 读取数据包主循环核心采集循环其实非常朴素int capture_loop(AVFormatContext* fmt_ctx, int video_idx, int audio_idx) { AVPacket pkt; av_init_packet(pkt); int64_t video_count 0; int64_t audio_count 0; while (1) { int ret av_read_frame(fmt_ctx, pkt); if (ret AVERROR(EAGAIN)) { continue; } if (ret AVERROR_EOF) { break; } if (ret 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, 读取数据包失败%s\n, errbuf); break; } if (pkt.stream_index video_idx) { // 这里拿到视频帧。如果只是存文件直接写 muxer // 如果需要分析则解码为 AVFrame printf(视频帧 count%lld, pts%lld\n, video_count, pkt.pts); } else if (pkt.stream_index audio_idx) { printf(音频帧 count%lld, pts%lld\n, audio_count, pkt.pts); } av_packet_unref(pkt); } return 0; }很多新手写的第一个版本会把av_packet_unref漏掉这个东西不调用内存在长时间采集时会上涨到吓人。AVPacket 在av_read_frame内部可能分配了缓冲区处理完不释放下一轮就会覆盖指针导致内存泄漏。我自己的规矩是处理完一包立刻av_packet_unref没有例外。3.4 编码保存为 MP4 文件采集到的裸码流如果直接存文件dshow 输出的往往是原始数据或者 MJPEG体积巨大。正常的做法是解码后重新编码或者用 FFmpeg 的转封装能力直接接一个编码器。我更推荐“解码后重编码”的通用流程因为这样能做更多处理缩放、加水印、降噪。核心步骤是avcodec_find_decoder找解码器avcodec_open2打开avcodec_send_packet和avcodec_receive_frame循环解码建立SwsContext做像素格式转换摄像头常见YUYV422、MJPG编码器常用YUV420P对音频做SwrContext重采样设备常见s16交错编码器常用fltp平面用avformat_write_header、av_interleaved_write_frame、av_write_trailer写文件。这部分代码量比较大我建议按两个函数拆分transcode_video_frame和transcode_audio_frame分别处理视频像素转换和音频重采样最后都统一转成AVFrame送给 muxer。这样主循环保持干净也方便日后扩展滤镜。4. 音视频同步两种时钟怎么对齐4.1 dshow 的时钟机制摄像头和麦克风本质是两个独立的硬件设备dshow 虽然把两者组合成了一个AVFormatContext但内部的时间戳来源并不完全统一。FFmpeg 的 dshow demuxer 默认会把音频设备当作主时钟视频包会基于音频时间戳做校正。对这个机制实操中你的代码不需要管太多把它当成“帧率可能有轻微抖动”即可。真正要命的是有些摄像头驱动不给pts或者给的是从 0 开始的系统启动时间这种情况下录出来的视频在用播放器拖进度条时会抽风。面对这种驱动不给时间戳的情况有一个通用方案用墙上时钟换算。// 在采集循环里拿到包后做一次统一时间基的转换 if (pkt.pts AV_NOPTS_VALUE) { // 没有时间戳就用自己的计数器生成 pkt.pts (video_count * 1000000) / fps; // 单位 us按你设定的帧率 pkt.dts pkt.pts; } // 把所有时间戳统一到 AV_TIME_BASE 微秒 int64_t pts_us av_rescale_q(pkt.pts, fmt_ctx-streams[pkt.stream_index]-time_base, (AVRational){1, AV_TIME_BASE});这里video_count是已采集的视频帧计数fps是你在打开设备时请求的帧率。这样生成的pts是从 0 开始的均匀时间戳虽然丢掉了真实采集时刻的语义但至少保证音视频相对顺序可预期播放器拖进度条也不会乱跳。4.2 音视频不同步的排查思路一次成都客户现场的摄像头录出来声音和画面差距肉眼可见大约有 100ms。我排查后发现不是时间戳的问题而是声卡驱动的缓冲默认较大导致音频流延迟比视频流多出了几十毫秒。解决方法是给 dshow 设置audio_buffer_size参数av_dict_set(options, audio_buffer_size, 50, 0);这个参数单位是毫秒值越大音频缓冲越大延迟越高、越不容易爆音值越小延迟越低但太小时在某些声卡上会出现断续。50ms 是我试出来比较折中的值存文件、做监控都够用。如果你的场景是直播、视频会议这种低延迟要求高的场景可以再往下压到 20ms同时配合后续的av_interleaved_write_frame写入策略。还需要注意一个细节当你在 Windows 上看到音画不同步先别急着改代码先用 ffprobe 看输入流的 parser 时间和解码时间。pkt.pts、pkt.dts、pkt.duration这三者的关系能透露很多驱动底层的行为。我见过一些摄像头驱动把 dts 设得乱糟糟pts 正常如果只按 pts 处理问题不大但如果用av_rescale_q同时算了 dts反而会把音频轨节奏带乱。4.3 视频和音频分开采集的兜底方案有些设备特别是部分安防摄像头、采集卡转接环境在 dshow 下同时videoxxx:audioxxx打开会失败或者打开后音频死活不出流。我的兜底方案是拆成两个独立的AVFormatContext一个开视频一个开音频然后在主循环里用poll或者多线程分别对两个上下文调av_read_frame最后统一按pts_us墙上时钟换算做对齐。这个方法灵活代价是你要自己维护两条流的生命周期代码复杂度翻倍。我建议只有当合路打开方式明确失败时才考虑不要一上来就上多线程不然会把自己绕进去。5. 常见问题与排查技巧实录5.1 设备打不开返回 AVERROR_INVALIDDATA症状是调用avformat_open_input失败errbuf 里提示Invalid data。绝大多数情况是设备名不对。Windows dshow 的设备名是包含空格的比如Integrated Camera你拼 URL 时写成videoIntegratedCamera就会失败。另一个常见原因是设备被占用。Windows 的摄像头默认开启了隐私权限限制或者在调试时你的程序还在跑另一个进程又去打开同一个设备就会拿到权限拒绝。处理办法先确认设备名通过枚举得到不要手打再检查是否有其他进程占用。可以在设备管理器里禁用再启用一次设备来恢复。5.2 有视频流但画面是全黑的多半是像素格式不匹配。dshow 设备默认可能输出 MJPEG 或 YUYV422如果你的下游解码器只认 NV12出来的画面就是黑屏或者花屏。解决方式是在打开设备时指定pixel_formatav_dict_set(options, pixel_format, yuyv422, 0);注意MJPG和YUYV422两种格式在 dshow 中如果都支持驱动会优先选择第一个你指定的。如果你指定了yuyv422带宽会高一倍USB2.0 下 1080p 30fps 可能跑不满若是 USB3.0 则没问题。嵌入式设备像树莓派的 ov5647在 v4l2 下常见格式是YUYV422和JPEG可以用v4l2-ctl --list-formats-ext查看设备支持的所有格式再决定采集参数。5.3 麦克风采集无声程序跑起来没有报错音频包也在走但播放文件没声音。先做二分排除法录出来的文件单独用播放器验证是“整个静音”还是“根本没有音频流”如果根本没有音频流说明audio_idx一直是 -1设备没被识别检查枚举列表里有没有麦克风如果有音频流但静音多半是采样率或通道数不对。针对采样率问题dshow 下可以通过sample_rate和channels强制指定。注意 Windows 的 WASAPI 在某些声卡上会做采样率转换如果你程序里请求 44100Hz 而设备默认是 48000HzFFmpeg 可能拿到的是经过驱动混洗的数据采样率标记却是 44100这时建议统一用 48000Hz 采集避免隐式重采样带来的音质损失。5.4 av_read_frame 长时间阻塞不动这个在 Windows dshow 下算是经典问题。av_read_frame是同步阻塞调用如果设备那边不送数据它会一直等。造成阻塞的原因通常是摄像头被系统相机应用占用、休眠恢复后驱动状态异常、或 USB 带宽不够导致驱动挂死。我总结的排查优先级现象排查思路处理手段打开之后第一帧就卡死设备被占用或驱动异常换 USB 口关闭其他相机应用运行一段时间后卡死USB 带宽不足丢包后驱动卡死调低分辨率、帧率加 rtbufsize休眠唤醒后卡死设备驱动状态未恢复增加设备重开重试逻辑重度使用场景下建议把采集循环放到独立线程主线程通过消息队列取帧。这样即使av_read_frame阻塞也不至于把整个程序的 UI 或网络通信卡死。5.5 树莓派 ov5647 和 Linux 设备的差异在树莓派上用 v4l2 采集 ov5647 模块时有一个容易被忽略的点v4l2 设备节点/dev/video0可能是 CSI 接口的 raw 设备输出格式是SGBRG10这种 Bayer 格式而不是常见的 YUV。如果直接用 FFmpeg 的 v4l2 输入不做格式协商会得到一片紫色的画面。正确的做法是先用v4l2-ctl --list-formats-ext -d /dev/video0查看驱动实际支持的输出格式。树莓派官方驱动通常会注册两个节点一个输出 raw Bayer一个输出 YUYV 合成格式。FFmpeg 里要用输出 YUYV 的那个节点或者用libcamera应用层转换后再交给 FFmpeg。这块的坑比较多我在树莓派上做 ov5647 采集时最终稳定方案是让 libcamera 出 RGB 数据再用 FFmpeg 的 rawvideo 输入封装成流而不是直接用 v4l2 裸抓。Linux 桌面环境下视频用 v4l2音频用 alsa代码结构和 dshow 基本相同只是打开格式名从dshow换成v4l2和alsaURL 从设备名换成/dev/video0和hw:0。核心的av_read_frame主循环完全一致。6. 一些实战经验和收尾最后分享一个我自己反复踩过的坑dshow 采集时不要在拿到 AVPacket 后同步做重量级处理比如人脸检测、目标跟踪否则av_read_frame内部的环形缓冲会被塞满然后驱动开始丢帧画面就会变成“一卡一卡但 CPU 占用率却很低的诡异状态”。正确做法永远是“采集线程只做拷贝和解码把数据丢给工作队列业务分析放别的线程”。另一个细节是异常退出。很多嵌入式设备或者廉价 USB 摄像头在程序不调avformat_close_input直接退出后驱动资源不会立刻释放紧接着下一次打开就会失败。以前做项目时经常遇到“程序崩溃重启后摄像头打不开要重启设备”的怪问题后来规范了退出逻辑先关流再释放上下文情况好了很多。如果你现在还在用命令行做设备采集我建议抽个时间把 API 版本的采集骨架搭起来哪怕只是打印每一帧的 pts 和流索引。这个骨架以后能复用到视频推流、AI 识别、远程监控几乎所有项目里投入产出比非常高。本文还有配套的精品资源点击获取