采集设备与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 的对应关系:

采集设备与API全链路架构 采集设备与API全链路架构 摄像头 麦克风 屏幕捕获 Windows: DirectShow macOS: AVFoundation Linux: V4L2 Windows: WASAPI macOS: AudioUnit Linux: PulseAudio/ALSA Windows: DXGI Duplication macOS: SCStream Linux: PipeWire/X11 统一采集接口(抽象层) 原始数据:YUV/RGB 帧 · PCM 音频 · 屏幕纹理 下游:编码 → 封装 → 传输 / 存储 说明 各平台API差异大,抽象层统一接口是关键

2.8 采集参数选择建议

选采集参数时,我一般遵循几个原则:

  • 分辨率:能低不高。1080p 够用就别上 4K,带宽和计算压力小很多。
  • 帧率:视频通话 15-30fps 足够,游戏直播才需要 60fps。
  • 色彩空间:优先 NV12 或 I420,编码器原生支持,省去色彩转换。
  • 音频采样率:48000Hz 是行业标准,44100Hz 也行,但别用 8000Hz。
  • 声道:单声道省带宽,立体声体验好。看场景选。

总结:采集是整个链路的起点,选对 API 和参数能省掉后面很多麻烦。Windows 用 DirectShow + DXGI,macOS 用 AVFoundation + SCStream,Linux 用 V4L2 + PulseAudio。记住这个组合,跨平台开发能少踩一半坑。