ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FFmpeg API实战:从零实现摄像头视频与麦克风音频采集

FFmpeg API实战:从零实现摄像头视频与麦克风音频采集 简介希望用FFmpeg替代DirectShow完成Windows音视频采集的开发者可以直接获得一套完整VC工程内含摄像头视频与麦克风音频的采集、编码、封装录制实现方案。内容围绕三条主线展开先用FFmpeg命令行枚举DShow设备并采集图像快速验证硬件可用性再介绍自研NSPlayer程序的功能与用法最后解析采集、编码、封装、录制各模块的源码实现。包内含162个文件其中h/cpp源文件逾110个便于阅读改造lib/dll为运行依赖库exe为可直接运行的示例程序另有Visual Studio工程文件、资源文件与说明文档压缩包约22.75MB。已有1303人学习下载。通过学习可掌握FFmpeg在音视频采集中的核心API调用、DShow设备枚举与参数配置并能基于所附工程快速搭建自己的采集录制工具避免从零研究DirectShow框架细节。用FFmpeg API采集摄像头视频和麦克风音频我最早做视频采集这块图省事直接在程序里调系统命令去跑ffmpeg指定摄像头设备名和麦克风设备名凑合能出文件。但项目一复杂就难受了——你要实时拿到每一帧做算法处理要动态切换分辨率要同步多路音视频流靠命令行根本没法跟应用逻辑打通。后来花了两天把采集模块整个重写成调用FFmpeg的libavdevice API这才算彻底舒坦。这篇文章就把这套方案完整讲清楚为什么用API而不是命令行、环境怎么配、核心流程长什么样、实际代码怎么写以及我踩过的那些坑。如果你正好在做一个需要从摄像头和麦克风实时取流的工具、录屏软件、或者类似监控类的客户端这篇可以直接抄作业。1. 整体思路为什么必须走API这条路先说清楚一个概念问题。很多人在搜“FFmpeg API”的时候找到的是像ffmpeg-python这类库但那只是对命令行的封装本质上还是起子进程。真正的FFmpeg API指的是libavformat、libavcodec这些C库你直接把FFmpeg的功能链接进程序里用自己的代码调用函数完成采集、解码、转码、封装。用命令行和用API区别就像是“喊一个工人干活”和“自己亲自上手操作”。命令行适合一次性转个文件但如果你要在软件里内置采集能力有三个绕不开的坎实时获取数据命令行默认把编码后的数据写文件你想在编码前拿到原始帧做分析、加水印、推流都得靠额外的管道或协议往里塞绕路。精细控制参数命令行参数是全局的遇到需要按帧处理、按设备动态调整曝光、采集过程中切换音视频参数非常别扭。多设备管理一台机器接多个摄像头、多个麦克风时命令行每次都是一锤子买卖程序中没法统一调度。而libavdevice这条路相当于FFmpeg已经帮你把各个平台Windows的dshow、Linux的v4l2、macOS的avfoundation的采集逻辑写好了你只需要用统一的AVFormatContext接口去操作底层的平台差异它全给你抹平。2. 环境准备先确保FFmpeg库是完整版启动之前先确认你机器上的FFmpeg带不带libavdevice。很多人下的精简版FFmpeg只包含转码核心功能用API采集的时候一调用就报“undefined reference to avdevice_register_all”一头雾水。2.1 检查FFmpeg版本和编译选项在终端跑一下这个命令ffmpeg -version看输出里面有没有--enable-libavdevice和--enable-avdevice。正常情况下官方编译版的FFmpeg默认都会带上但如果你是自己从源码编译的记得在configure的时候显式加上这些选项./configure --enable-libavdevice --enable-libavformat --enable-libavcodec --enable-libavutil我遇到过最坑的情况是某发行版为了精简体积把libavdevice单独拆成了libavdevice-dev这样一个包如果你只装了主包没装dev包编译时缺头文件。以Ubuntu为例需要确认这些包都已经装齐sudo apt install ffmpeg libavformat-dev libavcodec-dev libavdevice-dev libavutil-dev2.2 不同操作系统的采集后端FFmpeg的libavdevice层对应不同的平台选错输入格式名会直接报错这点新手特别容易忽略操作系统输入格式名设备标识方式常见坑Windowsdshowvideo摄像头名:audio麦克风名设备名带特殊字符冒号要转义Linuxv4l2视频、alsa音频/dev/video0、hw:0没有读写权限时打不开设备macOSavfoundation0:0索引或用设备名需要麦克风权限触发系统弹窗在Windows上先花十秒钟列出当前机器上可用的设备名这个操作能省掉后面至少一小时的排查时间ffmpeg -list_devices true -f dshow -i dummy输出里能看到类似“Integrated Camera”这样的名字如果你机器上还挂了USB摄像头这里也会列出来。Linux下直接看/dev/video*或者用v4l2-ctl --list-devices。3. 核心流程注册、打开、读包、关闭四步走整个采集过程比我预想中要简洁得多核心只有几步。但每一步背后都有值得展开的细节很多人写出来的程序要么半天打不开设备要么拿到手的帧数据是花的大概率就是这几步里的参数没设对。3.1 全局初始化注册设备和网络协议代码第一件事是调用avdevice_register_all()把FFmpeg内置的所有采集设备驱动注册进来。同时建议调用avformat_network_init()虽然这个主要跟网络流有关但有些采集后端底层会复用网络组件顺手做了不亏。#include libavformat/avformat.h #include libavdevice/avdevice.h int main() { avdevice_register_all(); avformat_network_init(); // ... }这里有个容易忽略的点avdevice_register_all()只需要调用一次。如果你的程序会反复打开、关闭设备比如做设备管理器界面不需要每次打开前重新注册。3.2 打开采集设备输入格式的选择与参数注入打开设备的接口是avformat_open_input()但这个函数的第三个参数AVInputFormat直接决定命运。你需要根据操作系统指定对应的输入格式然后通过AVDictionary往里面塞采集参数。Windows下同时采集摄像头和麦克风你可以把设备名拼成一个字符串让dshow一次性打开两路AVFormatContext *fmt_ctx NULL; AVDictionary *options NULL; AVInputFormat *input_fmt av_find_input_format(dshow); // 设置采集参数 av_dict_set(options, video_size, 1920x1080, 0); av_dict_set(options, framerate, 30, 0); av_dict_set(options, pixel_format, yuyv422, 0); av_dict_set(options, rtbufsize, 1024000, 0); // 增大缓冲减少丢帧 // 视频和音频同时采集用冒号分隔拼接 char device_name[256]; snprintf(device_name, sizeof(device_name), videoIntegrated Camera:audioMicrophone Array); int ret avformat_open_input(fmt_ctx, device_name, input_fmt, options); if (ret 0) { char errbuf[128] {0}; av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, 打开设备失败: %s\n, errbuf); return -1; }Linux下则是分开打开视频和音频两个输入流因为v4l2只处理视频alsa只处理音频// 视频 AVFormatContext *video_ctx NULL; AVInputFormat *vfmt av_find_input_format(v4l2); av_dict_set(vopts, input_format, mjpeg, 0); avformat_open_input(video_ctx, /dev/video0, vfmt, vopts); // 音频 AVFormatContext *audio_ctx NULL; AVInputFormat *afmt av_find_input_format(alsa); avformat_open_input(audio_ctx, hw:0, afmt, NULL);我对参数注入这块比较有心得尤其是av_dict_set。它就像一个“零活的配置袋”后面随时往里面塞键值对底层驱动按需读取。比较常用的一组参数video_size采集分辨率能设多大看摄像头硬件支持不是越高越好。framerate采集帧率。设太高而摄像头实际输出不了FFmpeg可能会自动降帧别被它糊弄。pixel_format指定像素格式。dshow下建议用yuyv422这个兼容性最好部分摄像头支持mjpeg直接输出压缩帧带宽占用小很多。rtbufsize设置内部缓冲区大小单位字节默认值往往偏小采集高分辨率视频时容易丢帧我一般会设大。3.3 读取数据包采集循环的核心逻辑设备打开成功后就进入了最核心的循环反复调用av_read_frame()读取音视频数据包。这个接口会从设备驱动那边拿原始帧以AVPacket的形式交给你。循环结束的条件是用户主动退出或者设备被拔了导致返回值出错。AVPacket pkt; av_init_packet(pkt); while (1) { int ret av_read_frame(fmt_ctx, pkt); if (ret 0) { // 读取失败或设备断开 break; } // 在这里你能拿到一帧一帧的原始数据 // pkt.stream_index 区分音视频流 // pkt.pts 时间戳 // pkt.size 数据大小 // pkt.data 数据内容 printf(读到一帧stream%d大小%dpts%lld\n, pkt.stream_index, pkt.size, pkt.pts); // 用完必须释放不然内存涨到飞起 av_packet_unref(pkt); }这段代码看着简单但有两个关键点极容易出错。第一个是包引用释放。av_read_frame每次返回你一个有效packet后内部存储是引用计数的必须调用av_packet_unref()把引用减掉否则内存会持续上涨。新手最容易犯这个错跑上一分钟内存就到几个G。第二个是时间戳的处理。采集设备的pts是基于设备自己的时钟初始值不是0你如果直接拿它来写文件或者做音视频同步经常会出现开头几秒对不齐的情况。比较稳妥的做法是记录第一个包的pts作为基准后面每个包的pts都减去这个基准值。3.4 关闭资源别只关一半采集结束要释放的资源并不复杂但顺序有讲究。我先调avformat_close_input把设备关掉再释放字典最后才退出。代码看起来枯燥却是我实际踩过“设备被占用下次打开失败”的坑之后总结出来的。av_write_trailer(out_fmt_ctx); // 如果写了文件 avformat_close_input(fmt_ctx); av_dict_free(options);更隐蔽的一个坑是如果你一次性打开了视频和音频两个context忘记关任意一个那么对应的摄像头或麦克风就会一直处于被占用状态。表现就是采集程序退出了但你再用别的软件打开摄像头会提示“设备被其他程序占用”。4. 完整示例采集摄像头音视频并存成MP4文件把上面的流程串起来就能得到一套可直接使用的采集程序。我把这个逻辑封装成一个完整的项目功能是采集摄像头和麦克风保存成MP4文件。你在自己的机器上把设备名换成实际的就行。#include stdio.h #include libavformat/avformat.h #include libavdevice/avdevice.h #include libavutil/timestamp.h int main(int argc, char *argv[]) { AVFormatContext *input_ctx NULL; AVFormatContext *output_ctx NULL; AVDictionary *options NULL; AVInputFormat *input_fmt NULL; const char *output_file output.mp4; const char *device_video Integrated Camera; const char *device_audio Microphone Array; int ret; char errbuf[128]; char device_name[256]; avdevice_register_all(); avformat_network_init(); // 1. 拼接dshow设备名 snprintf(device_name, sizeof(device_name), video%s:audio%s, device_video, device_audio); // 2. 配置采集参数 input_fmt av_find_input_format(dshow); av_dict_set(options, video_size, 1920x1080, 0); av_dict_set(options, framerate, 30, 0); av_dict_set(options, pixel_format, yuyv422, 0); av_dict_set(options, rtbufsize, 1024000, 0); // 3. 打开设备 ret avformat_open_input(input_ctx, device_name, input_fmt, options); if (ret 0) { av_strerror(ret, errbuf, sizeof(errbuf)); fprintf(stderr, 打开视频设备失败: %s\n, errbuf); return 1; } // 4. 查找流信息 ret avformat_find_stream_info(input_ctx, NULL); if (ret 0) { fprintf(stderr, 获取流信息失败\n); return 1; } // 5. 创建输出上下文 ret avformat_alloc_output_context2(output_ctx, NULL, NULL, output_file); if (ret 0) { fprintf(stderr, 创建输出上下文失败\n); return 1; } // 6. 将输入流复制到输出 AVStream *stream; for (int i 0; i input_ctx-nb_streams; i) { stream avformat_new_stream(output_ctx, NULL); if (!stream) { fprintf(stderr, 创建输出流失败\n); return 1; } ret avcodec_parameters_copy(stream-codecpar, input_ctx-streams[i]-codecpar); if (ret 0) { fprintf(stderr, 复制编码参数失败\n); return 1; } stream-codecpar-codec_tag 0; } // 7. 写入文件头 ret avformat_write_header(output_ctx, NULL); if (ret 0) { fprintf(stderr, 写文件头失败\n); return 1; } // 8. 采集循环 AVPacket pkt; int64_t base_pts[2] {AV_NOPTS_VALUE, AV_NOPTS_VALUE}; int64_t frame_count 0; while (frame_count 300) { // 采集10秒 ret av_read_frame(input_ctx, pkt); if (ret 0) { break; } // 时间戳归一化减去第一帧的时间戳 int stream_index pkt.stream_index; if (base_pts[stream_index] AV_NOPTS_VALUE) { base_pts[stream_index] pkt.pts; } pkt.pts - base_pts[stream_index]; pkt.dts - base_pts[stream_index]; // 按输出时间基转换 av_packet_rescale_ts(pkt, input_ctx-streams[stream_index]-time_base, output_ctx-streams[stream_index]-time_base); pkt.stream_index stream_index; av_interleaved_write_frame(output_ctx, pkt); av_packet_unref(pkt); frame_count; } // 9. 写完文件尾清理 av_write_trailer(output_ctx); avformat_free_context(output_ctx); avformat_close_input(input_ctx); av_dict_free(options); printf(采集完成共 %lld 帧输出文件%s\n, frame_count, output_file); return 0; }编译命令Linux可以按需去掉dshowgcc -o capture capture.c -lavformat -lavcodec -lavdevice -lavutilWindows下用MSVC的话链接库名类似avformat.lib、avdevice.lib。这段代码在Windows dshow下能直接跑核心逻辑在其他平台也通用只需换掉输入格式和设备名。5. 参数怎么取舍分辨率、帧率与格式的选择逻辑很多人拿到API后第一反应是“我全都拉最高”。我劝你冷静摄像头采集不只是“取流”这一个环节整个处理链路才是真正吃资源的地方。参数设太高后面编码、显示、存储全部要跟着烧性能。5.1 分辨率不是越高越好如果你的应用只是在界面上显示预览720p基本够用如果是做识别算法1080p对算力要求会直线上升。更关键的是摄像头有自己的原生分辨率模式比如很多笔记本摄像头虽然标称支持1920x1080但插值上去的画质还不如原生1280x720清晰。我自己的习惯是先列出摄像头支持的所有分辨率和像素格式再根据需求挑原生模式。列出设备能力可以用FFmpeg命令行ffmpeg -f dshow -list_options true -i videoIntegrated Camera这个命令会打印出摄像头支持的video_size、framerate、pixel_format组合。照着列表选比凭空猜稳得多。5.2 像素格式的选型会直接影响性能摄像头采集出来的原始格式大头是yuyv422YUV 4:2:2也有少部分支持mjpeg。如果你的程序后续要做编码或显示建议统一转成yuv420pYUV 4:2:0因为这是大多数编码器比如H.264和渲染管线默认支持的格式。如果你用的是MJPEG摄像头可以直接要求它以mjpeg格式输出。这种模式下摄像头内部已经完成JPEG压缩数据量小带宽占用低对USB 2.0接口的老设备来说优势特别明显但前提是你后续能解压JPEG——好在FFmpeg的libavcodec解MJPEG很快。5.3 音频参数的选择逻辑音频这边主要关注采样率和声道。麦克风阵列通常是双声道采样率一般是48000Hz或44100Hz。我建议统一设成48000Hz双声道如果用立体声或单声道原因很简单后续要跟视频封装进MP4音频重采样越少越好。要想拿更高品质的音频dshow还提供了sample_size等参数可以指定16位或24位深度的PCM。默认16位足够24位主要给录音后期留空间。6. 常见问题与排查技巧实录采集类程序踩的坑五花八门但高频问题其实就那几类。我把它们整理成速查表每个都是我实际遇到过或帮朋友排查过的确实能省不少时间。症状可能原因解决办法avformat_open_input返回-2文件不存在设备名写错或设备被占用先用命令行列出设备名检查其他软件是否占用摄像头打开摄像头成功但画面全黑分辨率或像素格式摄像头不支持用list_options查看实际支持的组合逐项匹配帧率不稳一会快一会慢缓冲区太小导致丢帧或USB带宽不足调大rtbufsize降低分辨率或帧率音频有严重杂音采样率与实际设备不符强制设为48000Hz检查麦克风采样率是否匹配程序内存持续上涨调用av_read_frame后忘了av_packet_unref每个包用完立即释放引用退出程序后设备仍被占用只关了输出没关输入确保调用avformat_close_input关闭输入上下文Windows下设备名含冒号拼接出错设备名里有冒号跟dshow的语法冲突设备名用引号包起来或检查是否确实有冒号6.1 解决设备占用的小技巧排查设备占用问题时我通常先用系统的设备管理器或任务管理器看该设备是否被其他进程锁定。Windows下还可以用PowerShell查一下Get-Process | Where-Object {$_.ProcessName -like *ffmpeg*}如果跑过命令行版本的ffmpeg采集进程没退干净API程序也会跟着打不开设备。杀掉残留进程就正常了。6.2 时间戳乱跳的处理采集设备传上来的pts并不是一个稳定递增的序列尤其在Windows下dshow和Linux下v4l2有的驱动会在某些时刻重置时间戳。硬拿这种时间戳写文件可能造成MP4文件没声音或画面跳帧。我的处理策略很简单不信任设备的绝对时间戳只信任增量。每读到一个packet记录它与上一个packet的时间差再叠加到自己维护的“本地时钟”上。这相当于给采集数据重新统一打点音视频同步性会稳很多。7. 最后一个避坑技巧不要和设备驱动硬刚前面说的都是API层面的问题最后说一个纯经验层面的。很多人写采集程序遇到对不上的格式比如你的程序要YUV420摄像头只给MJPEG第一反应是换参数、翻驱动设置折腾半天。其实没必要跟设备驱动硬刚。FFmpeg的架构里输入层拿到的原始数据本来就允许跟最终使用格式不一致你完全可以在读取到packet之后用libswscale和libavcodec做一层转换。拿MJPEG原始帧解成YUV420几百行代码的事灵活度却大幅提升。我现在的通用做法是采集层一律按摄像头最稳定的模式来处理层再做格式转换。这样即使换了摄像头型号采集层顶多变个设备名和格式核心逻辑完全不用动。就分享到这里。如果你正在接这个项目把文章里的示例跑通一遍再结合你自己的设备特性微调参数应该很快就能上手。本文还有配套的精品资源点击获取
返回列表