iOS:音视频开发——视频采集

前言


在直播和短视频行业日益火热的发展形势下,音视频开发(采集、编解码、传输、播放、美颜)等技术也随之成为开发者们关注的重点,本系列文章就音视频开发过程中所运用到的技术和原理进行梳理和总结。

认识 AVCapture 系列

AVCapture 系列是AVFoundation框架为我们提供的用于管理输入设备、采集、输出、预览等一系列接口,其工作原理如下:

1. AVCaptureDevice: 信号采集硬件设备(摄像头、麦克风、屏幕等)

AVCaptureDevice 代表硬件设备,并且为AVCaptureSession 提供 input,要想使用 AVCaptureDevice,应该先将设备支持的 device枚举出来, 根据摄像头的位置( 前置或者后置摄像头 )获取需要用的那个摄像头, 再使用;
如果想要对 AVCaptureDevice 对象的一些属性进行设置,应该先调用 lockForConfiguration: 方法, 设置结束后,调用unlockForConfiguration方法;

    [self.device lockForConfiguration:&error];
    // 设置 ***
    [self.device unlockForConfiguration];

2. AVCaptureInput: 输入数据管理

AVCaptureInput 继承自 NSObject,是向 AVCaptureSession 提供输入数据的对象的抽象超类; 要将 AVCaptureInput 对象与会话 AVCaptureSession 关联,需要 AVCaptureSession实例调用 -addInput: 方法。

由于 AVCaptureInput是个抽象类,无法直接使用,所以我们一般使用它的子类类管理输入数据。我们常用的AVCaptureInput的子类有三个:

AVCaptureDeviceInput:用于从 AVCaptureDevice对象捕获数据;
AVCaptureScreenInput:从 macOS 屏幕上录制的一种捕获输入;
AVCaptureMetadataInput:它为AVCaptureSession 提供AVMetadataItems

3. AVCaptureOutput:输出数据管理

AVCaptureOutput 继承自 NSObject,是输出数据管理,该对象将会被添加到会话AVCaptureSession中,用于接收会话AVCaptureSession各类输出数据; AVCaptureOutput提供了一个抽象接口,用于将捕获输出数据(如文件和视频预览)连接到捕获会话AVCaptureSession的实例,捕获输出可以有多个由AVCaptureConnection对象表示的连接,一个连接对应于它从捕获输入(AVCaptureInput的实例)接收的每个媒体流,捕获输出在首次创建时没有任何连接,当向捕获会话添加输出时,将创建连接,将该会话的输入的媒体数据映射到其输出,调用AVCaptureSession-addOutput:方法将AVCaptureOutputAVCaptureSession关联。

AVCaptureOutput是个抽象类,我们必须使用它的子类,常用的 AVCaptureOutput的子类如下所示:

AVCaptureAudioDataOutput:一种捕获输出,用于记录音频,并在录制音频时提供对音频样本缓冲区的访问;
AVCaptureAudioPreviewOutput :一种捕获输出,与一个核心音频输出设备相关联、可用于播放由捕获会话捕获的音频;
AVCaptureDepthDataOutput :在兼容的摄像机设备上记录场景深度信息的捕获输出;
AVCaptureMetadataOutput :用于处理捕获会话 AVCaptureSession产生的定时元数据的捕获输出;
AVCaptureStillImageOutput:在macOS中捕捉静止照片的捕获输出。该类在 iOS 10.0 中被弃用,并且不支持新的相机捕获功能,例如原始图像输出和实时照片,在 iOS 10.0 或更高版本中,使用 AVCapturePhotoOutput类代替;
AVCapturePhotoOutput :静态照片、动态照片和其他摄影工作流的捕获输出;
AVCaptureVideoDataOutput :记录视频并提供对视频帧进行处理的捕获输出;
AVCaptureFileOutput:用于捕获输出的抽象超类,可将捕获数据记录到文件中;
AVCaptureMovieFileOutput :继承自 AVCaptureFileOutput,将视频和音频记录到 QuickTime 电影文件的捕获输出;
AVCaptureAudioFileOutput :继承自AVCaptureFileOutput,记录音频并将录制的音频保存到文件的捕获输出。

4. AVCaptureSession:用来管理采集数据和输出数据,它负责协调从哪里采集数据,输出到哪里,它是整个Capture的核心,类似于RunLoop,它不断的从输入源获取数据,然后分发给各个输出源

AVCaptureSession 继承自NSObject,是AVFoundation的核心类,用于管理捕获对象AVCaptureInput的视频和音频的输入,协调捕获的输出AVCaptureOutput

5. AVCaptureConnection:用于AVCaptureSession来建立和维护 AVCaptureInputAVCaptureOutput之间的连接

AVCaptureConnectionSessionOutput 中间的控制节点,每个 OutputSession 建立连接后,都会分配一个默认的 AVCpatureConnection

6. AVCapturePreviewLayer:预览层,AVCaptureSession的一个属性,继承自 CALayer,提供摄像头的预览功能,照片以及视频就是通过把 AVCapturePreviewLayer添加到 UIViewlayer上来显示

开始视频采集

1、创建并初始化输入AVCaptureInput: AVCaptureDeviceInput 和输出AVCaptureOutput: AVCaptureVideoDataOutput;
2、创建并初始化 AVCaptureSession,把 AVCaptureInputAVCaptureOutput添加到 AVCaptureSession中;
3、调用 AVCaptureSessionstartRunning开启采集

初始化输入

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

        /**************************  设置输入设备  *************************/
        // ---  获取所有摄像头  ---
        NSArray *cameras = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
        // ---  获取当前方向摄像头  ---
        NSArray *captureDeviceArray = [cameras filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"position == %d", _capturerParam.devicePosition]];
        
        if (captureDeviceArray.count == 0) {
            return nil;
        }
        
        // ---  转化为输入设备  ---
        AVCaptureDevice *camera = captureDeviceArray.firstObject;
        self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:camera
                                                                        error:&error];

设置视频采集参数

@implementation VideoCapturerParam

- (instancetype)init {
    self = [super init];
    if (self) {
        _devicePosition = AVCaptureDevicePositionFront;    // 摄像头位置,默认为前置摄像头
        _sessionPreset = AVCaptureSessionPreset1280x720;   // 视频分辨率 默认 AVCaptureSessionPreset1280x720
        _frameRate = 15;  // 帧 单位为 帧/秒,默认为15帧/秒
        _videoOrientation = AVCaptureVideoOrientationPortrait;   // 摄像头方向 默认为当前手机屏幕方向
        
        switch ([UIDevice currentDevice].orientation) {
            case UIDeviceOrientationPortrait:
            case UIDeviceOrientationPortraitUpsideDown:
                _videoOrientation = AVCaptureVideoOrientationPortrait;
                break;
                
            case UIDeviceOrientationLandscapeRight:
                _videoOrientation = AVCaptureVideoOrientationLandscapeRight;
                break;
                
            case UIDeviceOrientationLandscapeLeft:
                _videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
                break;
                
            default:
                break;
        }
    }
    
    return self;
}

初始化输出

初始化视频输出 AVCaptureVideoDataOutput,并设置视频数据格式,设置采集数据回调线程,这里视频输出格式选的是 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,YUV 数据格式

        /**************************  设置输出设备  *************************/
        // ---  设置视频输出  ---
        self.captureVideoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
        
        NSDictionary *videoSetting = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange], kCVPixelBufferPixelFormatTypeKey, nil];   // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange 表示输出的视频格式为NV12
        [self.captureVideoDataOutput setVideoSettings:videoSetting];
        
        // ---  设置输出串行队列和数据回调  ---
        dispatch_queue_t outputQueue = dispatch_queue_create("VideoCaptureOutputQueue", DISPATCH_QUEUE_SERIAL);
        // ---  设置代理  ---
        [self.captureVideoDataOutput setSampleBufferDelegate:self queue:outputQueue];
        // ---  丢弃延迟的帧  ---
        self.captureVideoDataOutput.alwaysDiscardsLateVideoFrames = YES;

初始化 AVCaptureSession 并设置输入输出

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

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

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

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

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

/**************************  初始化会话  *************************/
        self.captureSession = [[AVCaptureSession alloc] init];
        self.captureSession.usesApplicationAudioSession = NO;
        
        // ---  添加输入设备到会话  ---
        if ([self.captureSession canAddInput:self.captureDeviceInput]) {
            [self.captureSession addInput:self.captureDeviceInput];
        }
        else {
            NSLog(@"VideoCapture:: Add captureVideoDataInput Faild!");
            return nil;
        }
        
        // ---  添加输出设备到会话  ---
        if ([self.captureSession canAddOutput:self.captureVideoDataOutput]) {
            [self.captureSession addOutput:self.captureVideoDataOutput];
        }
        else {
            NSLog(@"VideoCapture:: Add captureVideoDataOutput Faild!");
            return nil;
        }
        
        // ---  设置分辨率  ---
        if ([self.captureSession canSetSessionPreset:self.capturerParam.sessionPreset]) {
            self.captureSession.sessionPreset = self.capturerParam.sessionPreset;
        }
        
        /**************************  初始化连接  *************************/
        self.captureConnection = [self.captureVideoDataOutput connectionWithMediaType:AVMediaTypeVideo];
        
        // ---  设置摄像头镜像,不设置的话前置摄像头采集出来的图像是反转的  ---
        if (self.capturerParam.devicePosition == AVCaptureDevicePositionFront && self.captureConnection.supportsVideoMirroring) { // supportsVideoMirroring 视频是否支持镜像
            self.captureConnection.videoMirrored = YES;
        }
        
        self.captureConnection.videoOrientation = self.capturerParam.videoOrientation;
        
        self.videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
        self.videoPreviewLayer.connection.videoOrientation = self.capturerParam.videoOrientation;
        self.videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;

调用 / 获取数据

调用很简单,初始化视频采集参数VideoCapturerParam 和 视频采集器VideoVapturer, 设置预览图层videoPreviewLayer , 调用 startCpture就可以开始采集了,然后实现数据采集回调的代理方法videoCaptureOutputDataCallback 获取数据

    // --- 初始化视频采集参数  ---
    VideoCapturerParam *param = [[VideoCapturerParam alloc] init];
    
    // ---  初始化视频采集器  ---
    self.videoCapture = [[VideoVapturer alloc] initWithCaptureParam:param error:nil];
    self.videoCapture.delagate = self;
    
    // ---  开始采集  ---
    [self.videoCapture startCpture];
    

    // ---  初始化预览View  ---
    self.recordLayer = self.videoCapture.videoPreviewLayer;
    self.recordLayer.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds));
    [self.view.layer addSublayer:self.recordLayer];
#pragma mark ————— VideoCapturerDelegate —————  视频采集回调
- (void)videoCaptureOutputDataCallback:(CMSampleBufferRef)sampleBuffer {
    NSLog(@"%@ sampleBuffer : %@ ", kLOGt(@"视频采集回调"), sampleBuffer);
}

至此,我们就完成了视频的采集,在采集前和过程中,我们可能会对采集参数、摄像头方向、帧率等进行修改,具体的实现附上 Demo 地址:

github.com/G-Jayson/JX…

小编这呢,给大家推荐一个优秀的iOS学习平台,平台里的伙伴们都是非常优秀的iOS开发人员,我们专注于技术的分享、学习和交流,大家可以在平台上讨论技术,交流学习。欢迎大家的加入(想学习小伙伴的可加小编微信15673450590)。

转载于作者:G-Jayson
链接:https://juejin.im/post/5d26b47e6fb9a07ea5681868

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342