视频采集:iOS平台基于AVCaptureDevice的实现

前言

这篇文章简单介绍下移动端iOS系统下利用AVCaptureDevice进行视频数据采集的方法。
按照惯例先上一份源码:iOSVideo
摄像头采集相关核心实现在:NTVideoCapture.m
官方文档可以参考:AVFoundation官方文档

PS:采集部分的逻辑会相对比较简单,后续会在视频的采集基础上面介绍怎么利用OpenGL去绘制采集获取到的数据。

入门知识

AVCaptureSession
在iOS平台开发中只要跟硬件相关的都要从会话开始进行配置,如果我们使用摄像头的话可以利用AVCaptureSession进行视频采集,其可以对输入和输出数据进行管理,负责协调从哪里采集数据,输出到哪里去。

AVCaptureDevice
一个AVCaptureDevice对应的是一个物理采集设备,我们可以通过该对象来获取和识别设备属性。
例如通过AVCaptureDevice.position检测其摄像头的方向。

AVCaptureInput
AVCaptureInput是一个抽象类,AVCaptureSession的输入端必须是AVCaptureInput的实现类。
例如利用AVCaptureDevice构建AVCaptureDeviceInput作为采集设备输入端。

AVCaptureOutput
AVCaptureOutput是一个抽象类,AVCaptureSession的输出端必须是AVCaptureOutput的实现类。
例如AVCaptureVideoDataOutput可以作为一个原始视频数据的输出端。

AVCaptureConnection
AVCaptureConnectionAVCaptureSession用来建立和维护AVCaptureInputAVCaptureOutput之间的连接的,一个AVCaptureSession可能会有多个AVCaptureConnection实例。

采集步骤

  1. 创建AVCaptureSession并初始化。
  2. 通过前后置摄像头找到对应的AVCaptureDevice
  3. 通过AVCaptureDevice创建输入端AVCaptureDeviceInput,并将其添加到AVCaptureSession的输入端。
  4. 创建输出端AVCaptureVideoDataOutput,并进行Format和Delgate的配置,最后添加到AVCaptureSession的输出端。
  5. 获取AVCaptureConnection,并进行相应的参数设置。
  6. 调用AVCaptureSessionstartRunningstopRunning设置采集状态。

配置会话

创建一个AVCaptureSession很简单:

AVCaptureSession *captureSession;
captureSession = [[AVCaptureSession alloc] init];

我们可以在AVCaptureSession来配置指定所需的图像质量和分辨率,可选参数请参考AVCaptureSessionPreset.h
在设置前需要检测是否支持该Preset是否被支持:

//指定采集1280x720分辨率大小格式
AVCaptureSessionPreset preset = AVCaptureSessionPreset1280x720;
//检查AVCaptureSession是否支持该AVCaptureSessionPreset
if ([captureSession canSetSessionPreset:preset]) {
	captureSession.sessionPreset = preset;
}
else {
	//错误处理,不支持该AVCaptureSessionPreset类型值
}

配置输入端

通过AVCaptureDevicedevicesWithMediaType的方法来获取摄像头,由于iOS存在多个摄像头,所以这里一般返回一个设备的数组。
根据业务需要(例如前后置摄像头),我们找到其中对应的AVCaptureDevice,并将其构造成AVCaptureDeviceInput实例。

AVCaptureDevice *device;
AVCaptureDeviceInput *captureInput;
//获取前后置摄像头的标识
AVCaptureDevicePosition position = _isFront ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
//获取设备的AVCaptureDevice列表
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *item in devices) {
	//如果找到对应的摄像头
	if ([item position] == position) {
		device = item;
		break;
	}
}
if (device == nil) {
	//错误处理,没有找到对应的摄像头
}
//创建AVCaptureDeviceInput输入端
captureInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:nil];

配置输出端

如果我们想要获取到摄像头采集到的原始视频数据的话,需要配置一个AVCaptureVideoDataOutput作为AVCaptureSession的输出端,我们需要给其设置采集的视频格式和采集数据回调队列。

AVCaptureVideoDataOutput *captureOutput;
//创建一个输出端AVCaptureVideoDataOutput实例
captureOutput = [[AVCaptureVideoDataOutput new];
//配置输出的数据格式
[captureOutput setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8PlanarFullRange)}];
//设置输出代理和采集数据的队列
dispatch_queue_t outputQueue = dispatch_queue_create("ACVideoCaptureOutputQueue", DISPATCH_QUEUE_SERIAL);
[captureOutput setSampleBufferDelegate:self queue:outputQueue];
// 丢弃延迟的帧
captureOutput.alwaysDiscardsLateVideoFrames = YES;

需要注意的几个点

  • 对于setVideoSettings,虽然AVCaptureVideoDataOutput提供的是一个字典设置,但是现在只支持kCVPixelBufferPixelFormatTypeKey这个key。
  • 像素格式默认使用的是YUVFullRange类型,表示其YUV取值范围是0~255,而还有另外一种类型YUVVideoRange类型则是为了防止溢出,将YUV的取值范围限制为16~235。
  • setSampleBufferDelegate必须指定串行队列来确保视频数据获取委托调用的正确顺序,当然你也可以修改队列来设置视频处理的优先级别。
  • alwaysDiscardsLateVideoFrames = YES可以在你没有足够时间处理视频帧时丢弃任何延迟的视频帧而不是等待处理,如果你设置了NO并不能保证帧不会被丢弃,只是他们不会被提前有意识的丢弃而已。

配置会话的输入和输出

//添加输入设备到会话
if ([captureSession canAddInput:captureInput]) {
    [captureSession addInput:captureInput];
}
//添加输出设备到会话
if ([captureSession canAddOutput:captureOutput]) {
    [captureSession addOutput:captureOutput];
}
//获取连接并设置视频方向为竖屏方向
AVCaptureConnection *conn = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
conn.videoOrientation = AVCaptureVideoOrientationPortrait;
//前置摄像头采集到的数据本来就是镜像翻转的,这里设置为镜像把画面转回来
if (device.position == AVCaptureDevicePositionFront && conn.supportsVideoMirroring) {
    conn.videoMirrored = YES;
}

如果AVCaptureSession已经开启了采集,如果这个时候需要修改分辨率、输入输出等配置。那么需要用到beginConfigurationcommitConfiguration方法把修改的代码包围起来,也就是先调用beginConfiguration启动事务,然后配置分辨率、输入输出等信息,最后调用commitConfiguration提交修改;这样才能确保相应修改作为一个事务组提交,避免状态的不一致性。

AVCaptureSession管理了采集过程中的状态,当开始采集、停止采集、出现错误等都会发起通知,我们可以监听通知来获取AVCaptureSession的状态,也可以调用其属性来获取当前AVCaptureSession的状态,值得注意一点是AVCaptureSession相关的通知都是在主线程的。

开始采集数据和数据回调

当上面的配置搞定后,调用startRunning就可以开始数据的采集了。

if (![captureSession isRunning]) {
	[captureSession startRunning];
}

停止采集只需要调用stopRunning方法即可。

if ([captureSession isRunning]) {
	[captureSession stopRunning];
}

对于采集回调的视频数据,会在[captureOutput setSampleBufferDelegate:self queue:outputQueue]设置的代理方法触发返回,
其中最重要的是CMSampleBufferRef,其中实际存储着摄像头采集到的图像。
方法原型如下:

- (void)captureOutput:(AVCaptureOutput *)output 
		didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer 
		fromConnection:(AVCaptureConnection *)connection 

切换前后摄像头

在视频采集的过程中,我们经常需要切换前后摄像头,这里我们也就是需要把AVCaptureSession的输入端改为对应的摄像头就可以了。
当然我们可以用beginConfigurationcommitConfiguration将修改逻辑包围起来,也可以先调用stopRunning方法停止采集,然后重新配置好输入和输出,再调用startRunning开启采集。

//获取摄像头列表
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
//获取当前摄像头方向
AVCaptureDevicePosition currentPosition = captureInput.device.position;
//转换摄像头
if (currentPosition == AVCaptureDevicePositionBack){
    currentPosition = AVCaptureDevicePositionFront;
}
else{
    currentPosition = AVCaptureDevicePositionBack;
}
//获取到新的AVCaptureDevice
NSArray *captureDeviceArray = [devices filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", currentPosition]];
AVCaptureDevice *device = captureDeviceArray.firstObject;
//开始配置
[captureSession beginConfiguration];
//构造一个新的AVCaptureDeviceInput的输入端
AVCaptureDeviceInput *newInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
//移除掉就的AVCaptureDeviceInput
[captureSession removeInput:captureInput];
//将新的AVCaptureDeviceInput添加到AVCaptureSession中
if ([captureSession canAddInput:newInput]){
    [captureSession addInput:newInput];
    captureInput = newInput;
}
//提交配置
[captureSession commitConfiguration];
//重新获取连接并设置视频的方向、是否镜像
AVCaptureConnection *conn = [captureOutput connectionWithMediaType:AVMediaTypeVideo];
conn.videoOrientation = AVCaptureVideoOrientationPortrait;
if (device.position == AVCaptureDevicePositionFront && conn.supportsVideoMirroring){
    conn.videoMirrored = YES;
}

视频帧率

iOS默认的帧率设置是30帧,如果我们的业务场景不需要用到30帧,或者我们的处理能力达不到33ms(1000ms/30帧)的话,我们可以通过设置修改视频的输出帧率:

NSInteger fps = 15;
//获取设置支持设置的帧率范围
AVFrameRateRange *fpsRange = [captureInput.device.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0];
if (fps > fpsRange.maxFrameRate || fps < fpsRange.minFrameRate) {
	//不支持该fps设置
    return;
}
// 设置输入的帧率
captureInput.device.activeVideoMinFrameDuration = CMTimeMake(1, (int)fps);
captureInput.device.activeVideoMaxFrameDuration = CMTimeMake(1, (int)fps);

简易预览

如果不想通过自己实现OpenGL渲染采集到的视频帧,当然,iOS也提供了一个预览组件AVCaptureVideoPreviewLayer,其继承于CALayer
可以将这个layer添加到UIView上面就可以实现采集到的视频的实时预览。

//创建一个AVCaptureVideoPreviewLayer,并将AVCaptureSession传入
AVCaptureVideoPreviewLayer *previewLayer;
previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession];
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
previewLayer.frame = self.view.bounds;
//将其加载到UIView上面即可
[self.view.layer addSublayer:previewLayer];

PS:如果采用AVCaptureVideoPreviewLayer进行视频预览的话,那么可以不配置AVCaptureSession的输出端相关。

结语

这篇文章简单介绍下移动端iOS系统下利用AVCaptureDevice进行视频数据采集的方法,并提供了相关代码的使用示例。
限于篇幅就不对闪光灯、对焦等展开介绍,详细请参考官方文档。
后续文章将介绍怎么利用OpenGL来渲染摄像头采集到的视频帧。

本文同步发布于简书、CSDN。

End!

你可能感兴趣的:(移动音视频杂谈)