iOS音视频开发

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

2018-12-24  本文已影响2人  码农叔叔

前言

这篇文章简单介绍下移动端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;

需要注意的几个点

配置会话的输入和输出

//添加输入设备到会话
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!

上一篇下一篇

猜你喜欢

热点阅读