这篇文章简单介绍下移动端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
AVCaptureConnection
是AVCaptureSession
用来建立和维护AVCaptureInput
和AVCaptureOutput
之间的连接的,一个AVCaptureSession
可能会有多个AVCaptureConnection
实例。
AVCaptureSession
并初始化。AVCaptureDevice
。AVCaptureDevice
创建输入端AVCaptureDeviceInput
,并将其添加到AVCaptureSession
的输入端。AVCaptureVideoDataOutput
,并进行Format和Delgate的配置,最后添加到AVCaptureSession
的输出端。AVCaptureConnection
,并进行相应的参数设置。AVCaptureSession
的startRunning
和stopRunning
设置采集状态。创建一个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类型值
}
通过AVCaptureDevice
的devicesWithMediaType
的方法来获取摄像头,由于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
已经开启了采集,如果这个时候需要修改分辨率、输入输出等配置。那么需要用到beginConfiguration
和commitConfiguration
方法把修改的代码包围起来,也就是先调用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
的输入端改为对应的摄像头就可以了。
当然我们可以用beginConfiguration
和commitConfiguration
将修改逻辑包围起来,也可以先调用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!