采集设备与API:摄像头、麦克风、屏幕捕获
做音视频开发这么多年,我始终觉得采集是整个链路里最「接地气」的一环。你想想看,无论后面编解码多花哨、传输协议多复杂,源头要是没采好,后面全是白搭。这一章咱们就聊聊摄像头、麦克风和屏幕捕获,以及它们背后的操作系统API。
2.1 三大操作系统的采集生态
不同平台有各自的「方言」。Windows 用 DirectShow,macOS 靠 AVFoundation,Linux 则是 V4L2 的天下。我刚开始跨平台开发时,被这些 API 折腾得不轻——同一个摄像头,在三个系统上要写三套代码。
| 平台 | 核心API | 适用设备 | 特点 |
|---|---|---|---|
| Windows | DirectShow | 摄像头、麦克风 | 基于 COM,Filter Graph 架构 |
| macOS | AVFoundation | 摄像头、麦克风 | 现代、Swift/ObjC 友好 |
| Linux | V4L2 | 摄像头 | 内核级接口,ioctl 驱动 |
| 跨平台 | ALSA / PulseAudio | 麦克风 | Linux 音频采集 |
核心观点:采集 API 的本质是「枚举设备 → 打开设备 → 设置参数 → 启动流 → 读取数据 → 停止流 → 关闭设备」这个循环。不同平台的差异主要在参数格式和回调机制上。
2.2 Windows DirectShow 实战
DirectShow 是 Windows 上最经典的采集框架。它用 Filter Graph 把设备、处理、渲染串起来。我个人习惯用 IMoniker 枚举设备,然后用 ICaptureGraphBuilder2 构建采集图。
先看枚举摄像头的代码:
// 枚举视频采集设备
ICreateDevEnum *pDevEnum = NULL;
IEnumMoniker *pEnum = NULL;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL,
CLSCTX_INPROC_SERVER, IID_ICreateDevEnum,
(void**)&pDevEnum);
pDevEnum->CreateClassEnumerator(
CLSID_VideoInputDeviceCategory, &pEnum, 0);
IMoniker *pMoniker = NULL;
while (pEnum->Next(1, &pMoniker, NULL) == S_OK) {
IPropertyBag *pPropBag;
pMoniker->BindToStorage(0, 0,
IID_IPropertyBag, (void**)&pPropBag);
VARIANT varName;
VariantInit(&varName);
pPropBag->Read(L"FriendlyName", &varName, 0);
// varName.bstrVal 就是设备名称
VariantClear(&varName);
pPropBag->Release();
pMoniker->Release();
}
小技巧:DirectShow 的 Filter Graph 是 COM 组件,记得调用 CoInitializeEx(NULL, COINIT_MULTITHREADED) 初始化。我曾经忘了这步,调试了一下午才发现是 COM 没初始化。
设置采集参数时,我建议用 IAMStreamConfig 接口。它能枚举所有支持的格式,包括分辨率、帧率、色彩空间。举个例子:
IAMStreamConfig *pConfig = NULL;
pCaptureFilter->QueryInterface(
IID_IAMStreamConfig, (void**)&pConfig);
int iCount = 0, iSize = 0;
pConfig->GetNumberOfCapabilities(&iCount, &iSize);
VIDEO_STREAM_CONFIG_CAPS scc;
AM_MEDIA_TYPE *pmt = NULL;
for (int i = 0; i < iCount; i++) {
pConfig->GetStreamCaps(i, &pmt,
(BYTE*)&scc);
// pmt->formattype == FORMAT_VideoInfo
// 可以读取分辨率、帧率
DeleteMediaType(pmt);
}
避坑指南:DirectShow 的 AM_MEDIA_TYPE 需要手动释放,否则会内存泄漏。我曾经在生产环境跑了一周,内存涨了 2GB,排查后发现是这里没释放。
2.3 macOS AVFoundation 实战
macOS 上我推荐用 AVFoundation。它比老的 QTKit 现代得多,而且 Swift 和 ObjC 都支持。核心类是 AVCaptureSession 和 AVCaptureDevice。
枚举设备很简单:
// 枚举摄像头
NSArray *devices = [AVCaptureDevice
devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
NSLog(@"设备名称: %@", device.localizedName);
// 检查是否支持 1080p 30fps
for (AVCaptureDeviceFormat *fmt
in device.formats) {
CMFormatDescriptionRef desc =
fmt.formatDescription;
CMVideoDimensions dims =
CMVideoFormatDescriptionGetDimensions(desc);
if (dims.width == 1920 &&
dims.height == 1080) {
// 找到了 1080p 格式
}
}
}
启动采集流时,我习惯用 AVCaptureVideoDataOutput 获取原始帧。设置代理后,每帧都会回调:
AVCaptureVideoDataOutput *output =
[[AVCaptureVideoDataOutput alloc] init];
[output setSampleBufferDelegate:self
queue:dispatch_queue_create(
"capture.queue", NULL)];
// 设置像素格式
output.videoSettings = @{
(id)kCVPixelBufferPixelFormatTypeKey :
@(kCVPixelFormatType_32BGRA)
};
// 回调方法
- (void)captureOutput:(AVCaptureOutput *)output
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
// 这里拿到 CMSampleBufferRef
// 可以转成 CVPixelBufferRef 处理
}
经验之谈:AVFoundation 的回调队列建议用串行队列,别用主队列。主队列会卡 UI,串行队列能保证帧顺序。我遇到过帧乱序的问题,就是因为用了并发队列。
2.4 Linux V4L2 实战
Linux 上采集摄像头,V4L2(Video for Linux 2)是标准方案。它通过 ioctl 系统调用操作设备文件 /dev/video0。说实话,V4L2 的 API 比较底层,但胜在稳定。
打开设备并查询能力:
int fd = open("/dev/video0", O_RDWR);
if (fd < 0) {
perror("打开设备失败");
return -1;
}
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);
// cap.card 是设备名称
// cap.capabilities 包含 V4L2_CAP_VIDEO_CAPTURE
// 设置格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
ioctl(fd, VIDIOC_S_FMT, &fmt);
V4L2 的采集模式有两种:read/write 和 mmap。我推荐用 mmap,性能更好:
// 请求缓冲区
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 4; // 4 个缓冲区
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);
// 映射缓冲区
struct v4l2_buffer buf;
for (int i = 0; i < 4; i++) {
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
buffers[i].start = mmap(NULL,
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd, buf.m.offset);
buffers[i].length = buf.length;
}
// 启动采集
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
避坑指南:V4L2 的 VIDIOC_S_FMT 不一定会精确设置你想要的格式。它可能「就近」选择一个支持的格式。所以设置后一定要读回 fmt.fmt.pix 确认实际值。我曾经设了 1920x1080,结果设备只支持 1280x720,没检查就继续跑了,画面全是黑的。
2.5 屏幕捕获方案
屏幕捕获比摄像头采集复杂一些。不同平台差异很大,而且涉及权限问题。
| 平台 | API | 特点 | 帧率 |
|---|---|---|---|
| Windows | DXGI Desktop Duplication | 高性能,支持多显示器 | 60fps+ |
| Windows | BitBlt / GDI | 兼容性好,性能差 | 15-30fps |
| macOS | CGDisplayStream / SCContentSharingPicker | 支持 Retina,需权限 | 60fps |
| Linux | X11 / PipeWire / KMS | 碎片化严重 | 30-60fps |
Windows 上我强烈推荐 DXGI Desktop Duplication。它直接访问 GPU 的桌面副本,性能远好于 GDI。核心代码:
// 创建 DXGI 设备
ID3D11Device *device;
ID3D11DeviceContext *context;
D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE,
NULL, 0, NULL, 0, D3D11_SDK_VERSION,
&device, NULL, &context);
// 获取输出副本
IDXGIOutputDuplication *dup;
IDXGIOutput1 *output1;
output->QueryInterface(IID_IDXGIOutput1,
(void**)&output1);
output1->DuplicateOutput(device, &dup);
// 循环采集
IDXGIResource *resource;
DXGI_OUTDUPL_FRAME_INFO info;
dup->AcquireNextFrame(500, &info, &resource);
// resource 就是桌面纹理
// 可以映射到 CPU 内存处理
dup->ReleaseFrame();
经验之谈:DXGI 采集时要注意 AcquireNextFrame 的超时时间。设太短会频繁返回超时错误,设太长会卡住 UI 线程。我一般设 100ms,配合事件循环处理。
macOS 上,从 macOS 10.15 开始,屏幕捕获需要用户授权。用 SCContentSharingPicker 可以让用户选择要捕获的窗口或屏幕:
SCContentSharingPicker *picker =
[SCContentSharingPicker sharedPicker];
picker.delegate = self;
// 弹出选择界面
[picker show];
// 回调
- (void)contentSharingPicker:(SCContentSharingPicker *)picker
didSelectStreams:(NSArray<SCStream *> *)streams {
SCStream *stream = streams.firstObject;
// 配置输出
SCStreamConfiguration *config =
[[SCStreamConfiguration alloc] init];
config.width = 1920;
config.height = 1080;
// 添加视频输出
[stream addStreamOutput:self
type:SCStreamOutputTypeScreen
sampleHandlerQueue:dispatch_queue_create(
"screen.capture", NULL)];
}
// 帧回调
- (void)stream:(SCStream *)stream
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
ofType:(SCStreamOutputType)type {
// 处理屏幕帧
}
2.6 音频采集:麦克风与系统音频
音频采集相对简单,但要注意采样率和声道数。我一般用 48000Hz、16bit、立体声作为通用配置。
Windows 上用 WASAPI(Windows Audio Session API)采集麦克风:
// 初始化音频客户端
IMMDeviceEnumerator *enumerator;
IMMDevice *device;
IAudioClient *audioClient;
IAudioCaptureClient *captureClient;
CoCreateInstance(CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&enumerator);
enumerator->GetDefaultAudioEndpoint(
eCapture, eConsole, &device);
device->Activate(IID_IAudioClient,
CLSCTX_ALL, NULL, (void**)&audioClient);
// 设置格式
WAVEFORMATEX *format;
audioClient->GetMixFormat(&format);
// format->nSamplesPerSec = 48000
// format->wBitsPerSample = 16
// format->nChannels = 2
audioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
0, 10000000, 0, format, NULL);
audioClient->GetService(
IID_IAudioCaptureClient,
(void**)&captureClient);
audioClient->Start();
// 循环读取
UINT32 packetLength;
captureClient->GetNextPacketSize(&packetLength);
BYTE *data;
UINT32 framesAvailable;
DWORD flags;
captureClient->GetBuffer(&data,
&framesAvailable, &flags, NULL, NULL);
// 处理 data 中的音频数据
captureClient->ReleaseBuffer(framesAvailable);
避坑指南:WASAPI 的 GetMixFormat 返回的格式可能不是整数采样率。我遇到过返回 44100Hz 但实际设备只支持 48000Hz 的情况。建议用 IsFormatSupported 验证一下。
Linux 上我推荐用 PulseAudio 的 simple API,比 ALSA 更上层:
pa_simple *s = NULL;
pa_sample_spec ss;
ss.format = PA_SAMPLE_S16LE;
ss.rate = 48000;
ss.channels = 2;
s = pa_simple_new(NULL, "capture_app",
PA_STREAM_RECORD, NULL, "capture",
&ss, NULL, NULL, &error);
uint8_t buf[4096];
while (1) {
pa_simple_read(s, buf, sizeof(buf), &error);
// 处理 buf 中的 PCM 数据
}
pa_simple_free(s);
2.7 全链路采集架构图
下面这张图展示了采集层在整个音视频链路中的位置,以及各平台 API 的对应关系:
2.8 采集参数选择建议
选采集参数时,我一般遵循几个原则:
- 分辨率:能低不高。1080p 够用就别上 4K,带宽和计算压力小很多。
- 帧率:视频通话 15-30fps 足够,游戏直播才需要 60fps。
- 色彩空间:优先 NV12 或 I420,编码器原生支持,省去色彩转换。
- 音频采样率:48000Hz 是行业标准,44100Hz 也行,但别用 8000Hz。
- 声道:单声道省带宽,立体声体验好。看场景选。
总结:采集是整个链路的起点,选对 API 和参数能省掉后面很多麻烦。Windows 用 DirectShow + DXGI,macOS 用 AVFoundation + SCStream,Linux 用 V4L2 + PulseAudio。记住这个组合,跨平台开发能少踩一半坑。