FFmpeg API音视频采集:v4l2/dshow设备协商与编码同步实践 简介压缩包提供一套基于FFmpeg API的摄像头视频与麦克风音频采集工程面向熟悉DirectShow但希望简化采集流程的C开发者。作者先用ffmpeg.exe命令行演示如何枚举DShow设备并采集图像再给出自研采集程序的功能与用法最后按采集、编码、封装、录制四个模块拆解实现思路。整个工程涵盖设备选择对话框、视频显示窗口、音视频输入输出流、统计视图等完整界面与逻辑压缩包内含162个文件以102个头文件和11个cpp源文件为核心9个lib与9个dll支撑运行时链接另含sln、vcproj等VS工程配置、exe演示程序及pdb调试信息整体约22.75MB。已有1304人学习浏览。对于希望绕过DShow框架细节、直接通过FFmpeg完成音视频采集并录制或推流的开发者这份可编译、可跟读的完整案例能减少踩坑快速上手扩展自己的项目且代码结构清晰便于二次开发。1. 用 FFmpeg API 采集摄像头视频和麦克风音频命令行之外的实时音视频开发市面上多数开发者接触 FFmpeg 是从命令行开始的一条ffmpeg -f v4l2 -i /dev/video0就能出图。但走进采集类项目时命令行那套就撑不住了你说不清设备枚举失败是哪个参数造成的没法在帧到达时插入自己的处理回调更别提在音频和视频两个流之间做统一时钟校准。这份资源把 FFmpeg 采集链路从命令行搬到了 API 层核心是 libavdevice 家族配合 libavformat、libavcodec 的调用方式源码包是 C 语言工程覆盖 Linux 与 Windows 两套采集后端。它解决的是「设备侧实时取流 → 音视频同步 → 编码封装」这条硬链路适合正在做安防、在线教育、取证类桌面工具或嵌入式终端的开发者。看懂这套代码之后你会发现边角问题大多出在设备参数协商和 PTS 映射上而不是协议本身。2. 设备层选型avdevice 体系下的 v4l2 与 dshow 取舍2.1 先理解采集后端的形态差异FFmpeg 命令行能读摄像头本质上是 libavdevice 提供的输入格式在工作。Linux 下是v4l2Windows 下是dshowmacOS 下走avfoundation。这份资源重点落在 v4l2 与 dshow 双端实现目录里分别有input_v4l2.c和input_dshow.c两个入口文件。从 API 视角来看这三者都遵循同一个抽象AVInputFormat提供read_header和read_packet回调你的任务就是通过avformat_open_input把它激活。v4l2 输入格式对应的名称是v4l2dshow 对应的是dshow。别用av_find_input_format(video4linux2)那是早期版本的别名现在的库版本会返回空指针。AVInputFormat *fmt av_find_input_format(v4l2); if (!fmt) { fprintf(stderr, v4l2 input format not found\n); return -1; }这段代码的语义很简单av_find_input_format按字符串匹配注册表里的输入格式匹配不到就返回 NULL。常见做法是紧接着打印av_version_info()确认链接到的库版本很多采集问题其实是本地装了多个 FFmpeg 版本导致的头文件与动态库错配。2.2 设备名与 AVDictionary 参数协商在 API 层打开 Linux 设备设备名直接传/dev/video0之类的路径Windows 的 dshow 设备名要写成videoUSB Camera或audioMicrophone。打开之前最关键的操作是把参数塞进AVDictionaryAVDictionary *opts NULL; av_dict_set(opts, video_size, 640x480, 0); av_dict_set(opts, pixel_format, yuyv422, 0); av_dict_set(opts, framerate, 30, 0); av_dict_set(opts, fflags, nobuffer, 0); int ret avformat_open_input(ctx, dev_name, fmt, opts); av_dict_free(opts);这里video_size和framerate是摄像头驱动层直接消费的参数v4l2 后端会通过VIDIOC_S_FMT系统调用写入驱动。pixel_format很关键多数廉价摄像头默认输出 MJPG但你需要原始帧做算法处理时必须协商为yuyv422或nv12前提是驱动支持。fflagsnobuffer是一个通用读包标志让av_read_frame尽量少积压对实时显示场景很重要。参数协商失败时avformat_open_input会返回负的 AVERROR常见错误码是EINVAL与EBUSY。2.3 枚举设备的两种做法v4l2 在 Linux 下可以通过v4l2-ctl --list-devices查设备列表但代码里更通用的做法是用 FFmpeg 自带的avdevice_list_input_sources枚举。这个 API 的好处是跨平台——Windows 下用 dshow 后端枚举时返回的不只是视频设备还会列出音频输入AVDeviceInfoList *dev_list NULL; int ret avdevice_list_input_sources(fmt, NULL, NULL, dev_list); for (int i 0; i dev_list-nb_devices; i) { AVDeviceInfo *dev dev_list-devices[i]; printf(device[%d]: %s (%s)\n, i, dev-device_name, dev-device_description); } avdevice_free_list_devices(dev_list);AVDeviceInfoList内部会填充device_name和device_description两个字段前者用于传给avformat_open_input后者是给人看的描述文本。Windows 上如果枚举结果为空多半是 DirectShow 注册表里没有可用设备这也是虚拟摄像头软件装完需要重启才能被识别的原因。3. 采集主循环落地从 av_read_frame 到编码输出3.1 视频帧的读取与格式转换打开设备成功之后主循环的核心是反复调用av_read_frame。这个函数从设备驱动读出原始数据包音频和视频都走同一个出口通过pkt-stream_index区分流类型while (running) { AVPacket *pkt av_packet_alloc(); int ret av_read_frame(ctx, pkt); if (ret 0) { av_packet_free(pkt); break; } if (pkt-stream_index video_stream_idx) { // 对 pkt-data 做处理 } else if (pkt-stream_index audio_stream_idx) { // 音频包处理 } av_packet_unref(pkt); av_packet_free(pkt); }注意av_read_frame返回的是原始压缩包v4l2 设备如果协商成 MJPG 输出pkt-data里是一帧 JPEG 数据要拿去显示或做算法就得交给avcodec解码。如果协商的是yuyv422那pkt-data里是未压缩的原始图像帧可以直接做后续处理这也是低延迟场景里常见的选择——省掉了解码环节代价是 USB 带宽占用高。3.2 音视频编码的接入点采集的原始音频大概率是 PCM f32le 或 s16le视频是 yuyv422 或 nv12。要输出成 MP4就必须分别编码为 AAC 与 H.264。编码器的创建是套路化的AVCodecContext *video_enc_ctx NULL; const AVCodec *encoder avcodec_find_encoder(AV_CODEC_ID_H264); video_enc_ctx avcodec_alloc_context3(encoder); video_enc_ctx-width 640; video_enc_ctx-height 480; video_enc_ctx-pix_fmt AV_PIX_FMT_YUV420P; video_enc_ctx-time_base (AVRational){1, 30}; video_enc_ctx-framerate (AVRational){30, 1};上面的代码里pix_fmt用的是AV_PIX_FMT_YUV420P这说明从设备直接读到的 yuyv422 数据要经过sws_scale转一次格式再喂给编码器。硬件编码器可能只接受 NV12务必用avcodec_get_supported_config或直接看编码器封装里的pix_fmt数组确认。time_base是编码器打分用的时间精度一般设置为帧率的倒数。音频编码端的参数相对隐蔽一些编码器名称是aac采样格式要指定为AV_SAMPLE_FMT_FLTP而不是设备原始输出格式。下面这段是在音频编码器设置里容易翻车的点audio_enc_ctx-sample_fmt AV_SAMPLE_FMT_FLTP; audio_enc_ctx-sample_rate 44100; audio_enc_ctx-channel_layout AV_CH_LAYOUT_STEREO; audio_enc_ctx-bit_rate 128000; audio_enc_ctx-time_base (AVRational){1, 44100};采样格式FLTP是平面浮点格式AAC 编码器的内部输入约定就是这个麦克风采集到的 s16 交错数据不能直接送进去必须先用swr_convert做重采样与格式转换。这一步漏掉的话编码器返回的 AVERROR 会直接告诉你invalid argument但不会告诉你缺了格式转换。3.3 输出封装MP4 的时间基映射把编码后的音视频包写进 MP4靠avformat_alloc_output_context2创建封装器。MP4 不需要整体重编码脚本里只需要做一件事写入AVPacket时让pts从编码器时间基换算到封装器时间基AVRational enc_time_base video_enc_ctx-time_base; AVRational mux_time_base mux_ctx-streams[video_stream_idx]-time_base; pkt-pts av_rescale_q(pkt-pts, enc_time_base, mux_time_base); pkt-dts pkt-pts; pkt-duration av_rescale_q(pkt-duration, enc_time_base, mux_time_base);av_rescale_q是 FFmpeg 里做时间基换算的万能公式它内部采用有理数运算不存在浮点误差。这里已经把dts强制与pts对齐因为实时摄像头采集场景里不方便做 B 帧重排用baseline等级的 H.264 编码器通常不会出现dts滞后属于安全的默认策略。4. 音视频同步与设备占用常见踩坑排查4.1 摄像头打开失败返回 EBUSY 或 EACCES现象无论传/dev/video0还是/dev/video1avformat_open_input都返回-16或-13命令行下用 ffplay 打开也是黑屏。原因v4l2 设备被某个进程独占。常见的是浏览器测试页或抓拍软件没有释放设备v4l2 本身不允许多个进程同时打开同一节点。另外/dev/video0的用户权限不足时非 root 用户会得到 EACCES。解决先执行v4l2-ctl --list-devices确认哪个节点对应物理摄像头然后用fuser -v /dev/video0找出并释放占用进程。如果是权限问题usermod -a -G video 用户名后重新登录不要用 chmod 777 临时改权限重启后会被重置。4.2 图像卡顿和延迟越来越大现象程序跑了几分钟后画面延迟从几百毫秒涨到数秒内存占用持续增长。原因av_read_frame循环里没有及时释放AVPacket的引用计数。很多人只写了av_packet_alloc却忘了av_packet_unref或av_packet_freeFFmpeg 在老版本里并不会自动清理一个包里的引用。另一个原因是编码器没有开启delay模式控制编码器内部积压了过多帧。解决主循环里每次读完包立即av_packet_unref在while循环结束后统一av_packet_free。编码端在avcodec_send_frame之后循环avcodec_receive_packet直到返回值是EAGAIN为止避免编码器缓存堆积。4.3 音画不同步先快后慢现象录制出来的 MP4 里口型对不上前十几秒同步越往后音频越滞后。原因视频的pts和音频的pts是基于不同时钟源递增的。v4l2 摄像头的时间戳来源于内核系统时钟麦克风的 dshow 时间戳来源于 Windows 多媒体时钟。直接拼接等于拿两根不同频率的尺子量同一段布。解决用av_gettime_relative()作为统一时钟源。读取到视频包时记录当前时间把pts重写为av_gettime_relative() - start_time音频包也走同样的逻辑。重新采样成同一时钟后音画才能在容器层对齐。这样做之后仍然有轻微偏移就检查音频帧大小是否是编码器的frame_size整数倍麦克风驱动一次送过来 512 个样本但 AAC 一次要 1024 个必须走swr_convert或缓冲拼接后再avcodec_send_frame。4.4 编码器报错 AVERROR_INVALIDDATA且只发生在推流时现象本地采集正常投到 RTMP 服务后播放器提示无法解码。原因本地录制时 MP4 封装器会帮你写入 SPS/PPS 头但裸流推给 RTMP 时服务端需要你在每个关键帧前带上这两个参数。很多采集程序只avformat_write_header一次后续编码器更新参数就丢掉了。解决在avformat_write_header之后主动从avcodec_parameters里取出extradata推流前在每个关键帧包上附加这段数据。FFmpeg 5.x 之后推荐用法是通过av_packet_side_data_add把AV_PKT_DATA_NEW_EXTRADATA附到关键帧包上。4.5 高帧率下 GPU 编码器延迟反而变大现象同样的摄像头 30fps软编码延迟 200ms切到 NVIDIA 硬件编码后延迟反而到了 1s。原因硬件编码器默认有较大的surfaces队列为了吞吐量牺牲了流水线延迟。开发工具里看到的参数叫async_depth它表示驱动层最多提前缓冲多少帧等待编码完成默认值可能高达 16。这在视频剪辑场景没问题实况直播场景就不可接受了。解决在编码器私有选项里把该参数压到最小。FFmpeg 的 H.264 NVDEC 编码器提供-async_depth 2选项建议测试时从4起步。同时总延迟还受解码器端影响采集端如果设备输出 MJPG 还叠加了解码时间也必须一并考虑。5. 进阶优化基于 AVBufferRef 的环形缓冲与零拷贝取流完成基础链路之后多数人下一页面对的性能瓶颈集中在拷贝上。设备驱动把帧送到 FFmpeg 内部是第一次拷贝av_read_frame把AVPacket递给你是第二次编码前的sws_scale是第三次。三次拷贝足够让 CPU 占用率停留在高位。一种成熟的做法是用AVBufferRef持有缓冲区的生命周期不搬数据只搬引用。改造后的代码里摄像头后端直接把pkt-buf塞进环形缓冲区的节点消费线程取出引用交给编码器而不是复制pkt-datatypedef struct RingItem { AVBufferRef *buf_ref; int64_t pts; int stream_idx; } RingItem; int push_to_ring(AVPacket *pkt) { AVBufferRef *ref av_buffer_ref(pkt-buf); RingItem item { .buf_ref ref, .pts pkt-pts, .stream_idx pkt-stream_index }; ring_buffer_push(item); return 0; }这段代码的思路是让AVBufferRef成为帧数据的所有权凭证。av_buffer_ref的操作本质上是引用计数加一内部数据内存不会被复制只有计数变化。消费端在编码完成时调用av_buffer_unref释放引用。整套流程下来数据在整个链路里只存在一份物理内存配合 v4l2 的 MMAP 模式甚至可以做到用户态零拷贝。用AVBufferRef方案有个前置条件设备的每个AVPacket必须携带独立的buf字段v4l2 后端默认满足这个条件但某些硬件平台的驱动会复用同一个缓冲区此时av_buffer_ref后共享计数一旦减到零另一路还在访问的线程就会踩到释放后的内存。遇到平台相关翻车事故回退方案是改为av_packet_move_ref转移所有权彻底放弃多线程共享同一帧的思路。整套资源的代码里主循环线程只负责av_read_frame和把包送入环形缓冲编码线程独立消费线程间通过无锁队列传递RingItem耦合度降到最低。这样一个架构的好处是后期换硬件编码器或接入 GPU 推理管线时不需要改设备层代码。我在自己的项目里曾经为音画同步砸进去了整整两周后来发现问题是驱动层 USB 控制器把音频和视频时间戳分配了重叠的序号不是代码问题。从那以后我每次做采集前都强制走一遍 v4l2-ctl 检查设备能力、确认像素格式、验证帧率再做代码层开发。这套流程帮我提前排掉了大半的低级问题。希望这份资源也能帮你绕开那些我已经踩平的坑。本文还有配套的精品资源点击获取