iOS视频开发(一):视频采集

作者: GenoChen | 来源:发表于2018-07-05 10:10 被阅读356次

    前言

    作为iOS音视频开发之视频开发的第一篇,本文介绍iOS视频采集的相关概念及视频采集的工作原理,后续将对采集后的视频数据进行硬编码、硬解码、播放等流程进行分析讲解。


    基本概念

    AVCaptureSession

    苹果为了管理从摄像头、麦克风等设备捕获到的信息,整了一个叫做AVCaptureSession的东西来对输入和输出数据流进行管理。AVFoundation官方文档

    单个AVCaptureSession管理多个输入输出

    AVCaptureSession对象是用来管理采集数据和输出数据的,它负责协调从哪里采集数据,输出到哪里。

    AVCaptureDevice

    一个AVCaptureDevice对象代表一个物理采集设备,我们可以通过该对象来设置物理设备的属性。

    AVCaptureInput

    AVCaptureInput是一个抽象类,AVCaptureSession的输入端必须是AVCaptureInput的实现类。
    这里我们用到的是AVCaptureDeviceInput,作为采集设备输入端。

    AVCaptureOutput

    AVCaptureOutput是一个抽象类,AVCaptureSession的输出端必须是AVCaptureOutput的实现类。
    这里我们用到的是AVCaptureVideoDataOutput,作为视频数据的输出端。

    AVCaptureConnection

    AVCaptureConnection是AVCaptureSession用来建立和维护AVCaptureInput和AVCaptureOutput之间的连接的。

    AVCaptureVideoPreviewLayer

    这是AVCaptureSession的一个属性,集成自CALayer,通过类名我们可以知道这个layer是用来预览采集到的视频图像的,直接把这个layer加到UIView上面就可以实现采集到的视频实时预览了。


    AVCaptureConnection表示输入和输出之间的连接

    视频采集的步骤

    1、创建并初始化输入(AVCaptureInput)和输出(AVCaptureOutput)
    2、创建并初始化AVCaptureSession,把AVCaptureInput和AVCaptureOutput添加到AVCaptureSession中
    3、调用AVCaptureSession的startRunning开启采集

    初始化输入(摄像头)

    通过AVCaptureDevice的devicesWithMediaType:方法获取摄像头,iPhone都是有前后摄像头的,这里获取到的是一个设备的数组,要从数组里面拿到我们想要的前摄像头或后摄像头,然后将AVCaptureDevice转化为AVCaptureDeviceInput,一会儿添加到AVCaptureSession中。

    // 获取所有摄像头
    NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    // 获取前置摄像头
    NSArray *captureDeviceArray = [cameras filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", AVCaptureDevicePositionFront]];
    if (!captureDeviceArray.count)
    {
        NSLog(@"获取前置摄像头失败");
        return;
    }
    // 转化为输入设备
    AVCaptureDevice *camera = captureDeviceArray.firstObject;
    NSError *errorMessage = nil;
    self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&errorMessage];
    if (errorMessage)
    {
        NSLog(@"AVCaptureDevice转AVCaptureDeviceInput失败");
        return;
    }
    
    初始化输出

    初始化视频输出并设置视频数据格式,设置采集数据回调线程。
    这里视频输出格式选的是kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,YUV数据格式,不理解YUV数据的概念的话可以先这么写,后面编解码再深入了解YUV数据格式

    // 设置视频输出
    self.captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    
    // 设置视频数据格式
    NSDictionary *videoSetting = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], kCVPixelBufferPixelFormatTypeKey, nil];
    [self.captureVideoDataOutput setVideoSettings:videoSetting];
    
    // 设置输出代理、串行队列和数据回调
    dispatch_queue_t outputQueue = dispatch_queue_create("ACVideoCaptureOutputQueue", DISPATCH_QUEUE_SERIAL);
    [self.captureVideoDataOutput setSampleBufferDelegate:self queue:outputQueue];
    // 丢弃延迟的帧
    self.captureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES;
    
    初始化AVCaptureSession并设置输入输出

    1、初始化AVCaptureSession,把上面的输入和输出加进来,在添加输入和输出到AVCaptureSession先查询一下AVCaptureSession是否支持添加该输入或输出端口。

    2、设置视频分辨率及图像质量(AVCaptureSessionPreset),设置之前同样需要先查询一下AVCaptureSession是否支持这个分辨率。

    3、如果在已经开启采集的情况下需要修改分辨率或输入输出,需要用beginConfiguration和commitConfiguration把修改的代码包围起来。在调用beginConfiguration后,可以配置分辨率、输入输出等,直到调用commitConfiguration了才会被应用。

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

    前置摄像头采集到的画面是翻转的,若要解决画面翻转问题,需要设置AVCaptureConnection的videoMirrored为YES。

    self.captureSession = [[AVCaptureSession alloc] init];
    // 不使用应用的实例,避免被异常挂断
    self.captureSession.usesApplicationAudioSession = NO;
    // 添加输入设备到会话
    if ([self.captureSession canAddInput:self.captureDeviceInput])
    {
        [self.captureSession addInput:self.captureDeviceInput];
    }
    // 添加输出设备到会话
    if ([self.captureSession canAddOutput:self.captureVideoDataOutput])
    {
        [self.captureSession addOutput:self.captureVideoDataOutput];
    }
    // 设置分辨率
    if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720])
    {
        self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
    }
    // 获取连接并设置视频方向为竖屏方向
    self.captureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
    self.captureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    // 设置是否为镜像,前置摄像头采集到的数据本来就是翻转的,这里设置为镜像把画面转回来
    if (camera.position == AVCaptureDevicePositionFront && self.captureConnection.supportsVideoMirroring)
    {
        self.captureConnection.videoMirrored = YES;
    }
    // 获取预览Layer并设置视频方向,注意self.videoPreviewLayer.connection跟self.captureConnection不是同一个对象,要分开设置
    self.videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
    self.videoPreviewLayer.connection.videoOrientation = self.captureParam.videoOrientation;
    self.videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    
    开始视频数据采集

    伺候好AVCaptureSession之后,直接调用startRunning就可以开始采集了,开启采集前最好判断一下摄像头权限有没有开启。采集到的数据会通过上面设置的代理方法captureOutput:didOutputSampleBuffer:fromConnection回调出来。若要停止采集,只需调用stopRunning即可。

    AVCaptureSession的startRunning方法是个耗时操作,如果在主线程调用的话会卡UI。

    /** 开始采集 */
    - (BOOL)startCapture
    {
        if (self.isCapturing)
        {
            return NO;
        }
        // 摄像头权限判断
        AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
        if (videoAuthStatus != AVAuthorizationStatusAuthorized)
        {
            return NO;
        }
        [self.captureSession startRunning];
        self.isCapturing = YES;
        return YES;
    }
    /**
     摄像头采集的数据回调
     @param output 输出设备
     @param sampleBuffer 帧缓存数据,描述当前帧信息
     @param connection 连接
     */
    - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
    {
        if ([self.delegate respondsToSelector:@selector(videoCaptureDataCallback:)])
        {
            [self.delegate videoCaptureDataCallback:sampleBuffer];
        }
    }
    
    切换前后摄像头

    采集视频数据的过程中,我们可能需要切换前后摄像头,这时候我们只需要把AVCaptureSession中的输入端改成前置摄像头就可以了。这里是修改了输入端口,也就是需要调用beginConfiguration和commitConfiguration包起来。当然也可以选择先调用stopRunning方法停止采集,然后重新配置好输入和输出,再调用startRunning开启采集。

    // 获取所有摄像头
    NSArray *cameras= [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    // 获取当前摄像头方向
    AVCaptureDevicePosition currentPosition = self.captureDeviceInput.device.position;
    AVCaptureDevicePosition toPosition = AVCaptureDevicePositionUnspecified;
    if (currentPosition == AVCaptureDevicePositionBack || currentPosition == AVCaptureDevicePositionUnspecified)
    {
        toPosition = AVCaptureDevicePositionFront;
    }
    else
    {
        toPosition = AVCaptureDevicePositionBack;
    }
    NSArray *captureDeviceArray = [cameras filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", toPosition]];
    if (captureDeviceArray.count == 0)
    {
        return;
    }
    NSError *error = nil;
    AVCaptureDevice *camera = captureDeviceArray.firstObject;
    // 开始配置
    [self.captureSession beginConfiguration];
    AVCaptureDeviceInput *newInput = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
    [self.captureSession removeInput:self.captureDeviceInput];
    if ([_captureSession canAddInput:newInput])
    {
        [_captureSession addInput:newInput];
        self.captureDeviceInput = newInput;
    }
    // 提交配置
    [self.captureSession commitConfiguration];
    
    // 重新获取连接并设置视频的方向、是否镜像
    self.captureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
    self.captureConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
    if (camera.position == AVCaptureDevicePositionFront && self.captureConnection.supportsVideoMirroring)
    {
        self.captureConnection.videoMirrored = YES;
    }
    
    设置视频的帧率

    iOS默认输出的视频帧率为30帧/秒,在某些应用场景下我们可能用不到30帧,我们也可以设置或修改视频的输出帧率

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

    踩坑及总结

    用的时候我们是有设置AVCaptureVideoPreviewLayer的connection的videoOrientation属性来确定画面的方向的。刚开始的时候出现了很奇怪的现象,AVCaptureVideoPreviewLayer的画面是正常的竖屏方向,但拿采集出来的视频流去播放的时候发现画面是横屏方向。后来发现,这里AVCaptureVideoPreviewLayer的connection属性跟我们通过connectionWithMediaType方法获取到的connection不是同一个对象,需要分别设置。像我们上面分析看到,connection是维护一个输入设备和一个输出的,也就是说AVCaptureSession有多个connection。找到对应需要的connection设置videoOrientation,问题解决。

    iOS视频采集这一块内容不多,难度也比较低,应该还是比较好上手的。本文限于篇幅没有详细说明设置摄像头的相关内容,例如闪光灯、对焦等。建议阅读AVFoundation官方文档,少走弯路。

    下一篇将介绍H264硬编码的实现,最后附上Demo地址:https://github.com/GenoChen/MediaService

    相关文章

      网友评论

        本文标题:iOS视频开发(一):视频采集

        本文链接:https://www.haomeiwen.com/subject/lzieuftx.html