AVFoundation开发秘籍笔记-06捕捉媒体

一、捕捉功能

1、捕捉会话 AVCaptureSession

AVFoundation捕捉栈的核心类是AVCaptureSession。一个捕捉会话相当于一个虚拟的“插线板”,用于连接输入和输出的资源。

捕捉会话管理从屋里设备得到的数据流,比如摄像头和麦克风设备,输出到一个或多个目的地。可以动态配置输入和输出的线路,可以再会话进行中按需配置捕捉环境。

捕捉会话还可以额外配置一个会话预设值(session preset),用来控制捕捉数据的格式和质量。会话预设值默认为AVCaptureSessionPresetHigh,适用于大多数情况。还有很多预设值,可以根据需求设置。

2、捕捉设备 AVCaptureDevice

AVCaptureDevice为摄像头或麦克风等物理设备定义了一个接口。对硬件设备定义了大量的控制方法,如对焦、曝光、白平衡和闪光灯等。

AVCaptureDevice定义大量类方法用用访问系统的捕捉设备,最常用的是defaultDeviceWithMediaType:,根据给定的媒体类型返回一个系统指定的默认设备

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];请求的是一个默认的视频设备,在包含前置和后置摄像头的iOS系统,返回后置摄像头。

3、捕捉设备的输入 AVCaptureInput

AVCaptureInput是一个抽象类,提供一个连接接口将捕获到的输入源连接到AVCaptureSession

抽象类无法直接使用,只能通过其子类满足需求:AVCaptureDeviceInput-使用该对象从AVCaptureDevice获取设备数据(摄像头、麦克风等)、AVCaptureScreenInput-通过屏幕获取数据(如录屏)、AVCaptureMetaDataInput-获取元数据

  • 以 AVCaptureDeviceInput 为例

使用捕捉设备进行处理前,需要将它添加为捕捉会话的输入。通过将设备(AVCaptureDevice)封装到AVCaptureDeviceInput实例中,实现将设备插入到AVCaptureSession中。

AVCaptureDeviceInput在设备输出数据和捕捉会话间,扮演接线板的作用。

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];

4、捕捉的输出 AVCaptureOutput

AVCaptureOutput是一个抽象基类,用于从捕捉会话得到的数据寻找输出目的地。

框架定义一些这个基类的高级扩展类,比如
AVCaptureStillImageOutput用来捕捉静态图片,AVCaptureMovieFileOutput捕捉视频

还有一些底层扩展,如AVCaptureAudioDataOutputAVCaptureVideoDataOutput使用它们可以直接访问硬件捕捉到的数字样本。使用底层输出类需要对捕捉设备的数据渲染有更好的理解,不过这些类可以提供更强大的功能,比如对音频和视频流进行实时处理。

5、捕捉连接 AVCaptureConnection

AVCaptureConnection 连接

捕捉会话首先确定有给定捕捉设备输入渲染的媒体类型,并自动建立其到能够接收该媒体类型的捕捉输出端的连接。

对连接的访问可以对信号流进行底层的空值,比如禁用某些特定的连接,或者再音频连接中访问单独的音频轨道(一些高级用法,不纠结)。

  • 附加AVCaptureConnection解决一个图像旋转90°的问题:(setVideoOrientation:方法)
AVCaptureConnection *stillImageConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
AVCaptureVideoOrientation  avcaptureOrientation = [self avOrientationForDeviceOrientation:UIDeviceOrientationPortrait];
    
[stillImageConnection setVideoOrientation:avcaptureOrientation];

6、捕捉预览 AVCaptureVideoPreviewLayer

AVCaptureVideoPreviewLayer是一个CoreAnimationCALayer的子类,对捕捉视频数据进行实时预览。

类似于AVPlayerLayer,不过针对摄像头捕捉的需求进行了定制。他也支持视频重力概念setVideoGravity:

  • AVLayerVideoGravityResizeAspect --在承载层范围内缩放视频大小来保持视频原始宽高比,默认值,适用于大部分情况
  • AVLayerVideoGravityResizeAspectFill --保留视频宽高比,通过缩放填满层的范围区域,会导致视频图片被部分裁剪。
  • AVLayerVideoGravityResize --拉伸视频内容拼配承载层的范围,会导致图片扭曲,funhouse effect效应。

二、创建简单捕捉会话

当如库文件 #import <AVFoundation/AVFoundation.h>

  • 1、创建捕捉会话 AVCaptureSession,可以设置为成员变量,开始会话以及停止会话都是用到实例对象。
    AVCaptureSession *session = [[AVCaptureSession alloc] init];

  • 2、创建获取捕捉设备 AVCaptureDevice
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

  • 3、创建捕捉输入 AVCaptureDeviceInput

NSError *error;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
  • 4、将捕捉输入加到会话中
if ([session canAddInput:input]) {
    //首先检测是否能够添加输入,直接添加可能会有crash
    [session addInput:input];
}
  • 5、创建一个静态图片输出 AVCaptureStillImageOutput

AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];

  • 6、将捕捉输出添加到会话中
if ([session canAddOutput:imageOutput]) {
    //检测是否可以添加输出
    [session addOutput:imageOutput];
}
  • 7、创建图像预览层AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
previewLayer.frame = self.view.frame;
[self.view.layer addSublayer:previewLayer];
  • 8、开始会话

[session startRunning];

开始之前先获取设备摄像头权限。info.plist中添加Privacy - Camera Usage Description

这里只是实现捕捉流程,梳理核心组件的关系,没有任何操作。典型的会话创建过程会更复杂,这是毋庸置疑的。当开始运行会话,视频数据流就可以再系统中传输。

三、创建一个简单的拍照视频项目

整个的逻辑依旧是上面的几步,更多的是一些新的属性设置,因为是简单项目,所以,只是实现了功能,并没有作具体的优化。怎么简单怎么来,主要是熟悉一下主要功能。

1、创建捕捉会话

项目里不只是要实现静态图片捕捉,还会有视频拍摄,所以还有视频和音频输入。

就是前面说的【创建简单会话】流程的升级版,可以同时给会话添加多个输入和多个输出,然后分别单独处理。

self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
    
    //获取设备摄像头
    AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    // 得到一个指向默认视频捕捉设备的指针。
    
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
    //将设备添加到Session之前,先封装到AVCaptureDeviceInput对象
    
    if (videoInput) {
        if ([self.captureSession canAddInput:videoInput]) {
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        }
    } else {
        return NO ;
    }
    
    //获取设备麦克风功能
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
    AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
    if (audioInput) {
        if ([self.captureSession canAddInput:audioInput]) {
            //对于有效的input,添加到会话并给它传递捕捉设备的输入信息
            [self.captureSession addInput:audioInput];
        }
    } else {
        return NO ;
    }
    
    //设置 静态图片输出
    self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
    
    self.stillImageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
    //配置字典表示希望捕捉JPEG格式图片
    
    if ([self.captureSession canAddOutput:self.stillImageOutput]) {
        // 测试输出是否可以添加到捕捉对话,然后再添加
        [self.captureSession addOutput:self.stillImageOutput];
    }
    
    
    //设置视频文件输出
    
    self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];
    
    if ([self.captureSession canAddOutput:self.movieOutput]) {
        [self.captureSession addOutput:self.movieOutput];
        NSLog(@"add movie output success");
    }

2、开始和结束会话

- (dispatch_queue_t)globalQueue {
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}

//开始捕捉会话
- (void)startSession {
    if (![self.captureSession isRunning]) {
        dispatch_async([self globalQueue], ^{
            //开始会话 同步调用会消耗一定时间,所以用异步方式在videoQueue排队调用该方法,不会阻塞主线程。
            [self.captureSession startRunning];
        });
    }
}

//停止捕捉会话
- (void)stopSession {
    if ([self.captureSession isRunning]) {
        dispatch_async([self globalQueue], ^{
            [self.captureSession stopRunning];
        });
    }
}

3、切换摄像头

切换前置和后置摄像头需要重新配置捕捉回话,可以动态重新配置AVCaptureSession,不必担心停止会话和重新启动会话带来的开销。
对会话进行的任何改变,都要通beginConfigurationcommitConfiguration,进行单独的、原子性的变化。

- (BOOL)switchCameras { //验证是否有可切换的摄像头
    if (![self canSwitchCameras]) {
        return NO;
    }
    NSError *error;
    AVCaptureDevice *videoDevice = [self inactiveCamera];
    
    AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
    if (videoInput) {
        [self.captureSession beginConfiguration];
        // 标注源自配置变化的开始
        
        [self.captureSession removeInput:self.activeVideoInput];
        if ([self.captureSession canAddInput:videoInput]) {
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        } else if (self.activeVideoInput) {
            [self.captureSession addInput:self.activeVideoInput];
        }
        [self.captureSession commitConfiguration];
    } else {
        [self.delegate deviceConfigurationFailedWithError:error];       
        return NO;
    }
    return YES;
}
// 返回指定位置的AVCaptureDevice 有效位置为 AVCaptureDevicePositionFront 和AVCaptureDevicePositionBack,遍历可用视频设备,并返回position参数对应的值
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device  in devices) {
        if (device.position  == position) {
            return device;
        }
    }
    return nil;
}

// 当前捕捉会话对应的摄像头,返回激活的捕捉设备输入的device属性
- (AVCaptureDevice *)activeCamera {
    return self.activeVideoInput.device;
}

// 返回当前未激活摄像头
- (AVCaptureDevice *)inactiveCamera {
    AVCaptureDevice *device = nil;
    if (self.cameraCount > 1) {
        if ([self activeCamera].position == AVCaptureDevicePositionBack) {
            device = [self cameraWithPosition:AVCaptureDevicePositionFront];
        } else {
            device = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }
    }
    return device;
}

- (BOOL)canSwitchCameras {
    return self.cameraCount > 1;
}

// 返回可用视频捕捉设备的数量
- (NSUInteger)cameraCount {
    return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}

4、捕获静态图片

AVCaptureConnection,当创建一个会话并添加捕捉设备输入和捕捉输出时,会话自动建立输入和输出的链接,按需选择信号流线路。访问这些连接,可以更好地对发送到输出端的数据进行控制。

CMSampleBuffer是有CoreMedia框架定义的CoreFoundation对象。可以用来保存捕捉到的图片数据。图片格式根据输出对象设定的格式决定。

- (void)captureStillImage {
    NSLog(@"still Image");
    AVCaptureConnection *connection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
    if (connection.isVideoOrientationSupported) {
        connection.videoOrientation = [self currentVideoOrientation];
    }
    id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error) {
        if (sampleBuffer != NULL) {
            NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
            UIImage *image = [UIImage imageWithData:imageData];
            //这就得到了拍摄到的图片,可以做响应处理。
            
            
        } else {
            NSLog(@"NULL sampleBuffer :%@",[error localizedDescription]);
        }
    };
    [self.stillImageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];
}

处理图片方向问题。

- (AVCaptureVideoOrientation)currentVideoOrientation {
    AVCaptureVideoOrientation orientation;
    
    switch ([[UIDevice currentDevice] orientation]) {
        case UIDeviceOrientationPortrait:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationLandscapeRight:
            orientation = AVCaptureVideoOrientationLandscapeLeft;
            break;
        case UIDeviceOrientationPortraitUpsideDown:
            orientation = AVCaptureVideoOrientationPortraitUpsideDown;
            break;

        default:
            orientation = AVCaptureVideoOrientationLandscapeRight;
            break;
    }
    
    return orientation;
}

5、录制视频

视频内容捕捉,设置捕捉会话,添加名为AVCaptureMovieFileOutput的输出。将QuickTime影片捕捉大磁盘,这个类的大多数核心功能继承与超类AVCaptureFileOutput

通常当QuickTime应聘准备发布时,影片头的元数据处于文件的开始位置,有利于视频播放器快速读取头包含的信息。录制的过程中,知道所有的样本都完成捕捉后才能创建信息头。

- (void)startRecording {
    if (![self isRecording]) {
        
        AVCaptureConnection *videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        if ([videoConnection isVideoOrientationSupported]) {
            videoConnection.videoOrientation = [self currentVideoOrientation];
        }
        if ([videoConnection isVideoStabilizationSupported]) {
            videoConnection.preferredVideoStabilizationMode = YES;
            
        }
        
        //如果支持preferredVideoStabilizationMode,设置为YES。支持视频稳定可以显著提升捕捉到的视频质量。
        // 只在录制视频文件时才会涉及。
        
        AVCaptureDevice *device = [self activeCamera];
        if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            } else {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
            //摄像头可以进行平滑对焦模式的操作,减慢摄像头镜头对焦的速度。
            //通常情况下,用户移动拍摄时摄像头会尝试快速自动对焦,这会在捕捉视频中出现脉冲式效果。
            //当平滑对焦时,会较低对焦操作的速率,从而提供更加自然的视频录制效果。
        }
        
        self.outputURL = [self uniqueURL];
        NSLog(@"url %@",self.outputURL);
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
        // 查找写入捕捉视频的唯一文件系统URL。保持对地址的强引用,这个地址在后面处理视频时会用到
        // 添加代理,处理回调结果。
        
    }
}

// 获取录制时间
- (CMTime)recordedDuration {
    return self.movieOutput.recordedDuration;
}

// 设置存储路径
- (NSURL *)uniqueURL {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *directionPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"camera_movie"];
  
    NSLog(@"unique url :%@",directionPath);
    if (![fileManager fileExistsAtPath:directionPath]) {
        [fileManager createDirectoryAtPath:directionPath withIntermediateDirectories:YES attributes:nil error:nil];
    }

    NSString *filePath = [directionPath stringByAppendingPathComponent:@"camera_movie.mov"];
    if ([fileManager fileExistsAtPath:filePath]) {
        [fileManager removeItemAtPath:filePath error:nil];
    }
    return [NSURL fileURLWithPath:filePath];
    
    return nil;
}

// 停止录制
- (void)stopRecording {
    if ([self isRecording]) {
        [self.movieOutput stopRecording];
    }
}

// 验证录制状态
- (BOOL)isRecording {
    return self.movieOutput.isRecording;
}

代理回调,拿到录制视频的地址。

#pragma mark -- AVCaptureFileOutputRecordingDelegate

// 录制完成
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
{
    NSLog(@"capture output");
    if (error) {
        NSLog(@"record error :%@",error);
        [self.delegate mediaCaptureFailedWithError:error];
    } else {
        // 没有错误的话在存储响应的路径下已经完成视频录制,可以通过url访问该文件。
                    
    }
    self.outputURL = nil;
}

6、将图片和视频保存到相册

将拍摄到的图片和视频可以通过这个系统库保存到相册。

不过AssetsLibrary在iOS9.0之后就被弃用了,可以使用从iOS8.0支持的Photos/Photos.h库来实现图片和视频的保存。

- (void)writeImageToAssetsLibrary:(UIImage *)image {
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    [library writeImageToSavedPhotosAlbum:image.CGImage orientation:(NSInteger)image.imageOrientation completionBlock:^(NSURL *assetURL, NSError *error) {
        if (!error) {

        } else {
            NSLog(@"Error :%@",[error localizedDescription]);
        }
        
    }];
    
}
- (void)writeVideoToAssetsLibrary:(NSURL *)videoUrl {
    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoUrl]) {
        //检验是否可以写入
        
        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
        completionBlock = ^(NSURL *assetURL, NSError *error) {
            if (error) {
                [self.delegate asssetLibraryWriteFailedWithError:error];
            } else {

            }
        };
        [library writeVideoAtPathToSavedPhotosAlbum:videoUrl completionBlock:completionBlock];
        
    }
}


Photos/Photos.h实现图片和视频保存

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"success :%d ,error :%@",success,error);
        if (success) {
                // DO: 
                
        }
    }];

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoUrl];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
    if (success) {
        // DO:
        [self generateThumbnailForVideoAtURL:videoUrl];
    } else {
        [self.delegate asssetLibraryWriteFailedWithError:error];
        NSLog(@"video save error :%@",error);
    }
}];

7、关于闪光灯和手电筒的设置

设备后面的LED灯,当拍摄静态图片时作为闪光灯,当拍摄视频时用作连续灯光(手电筒).捕捉设备的flashMode和torchMode。

  • AVCapture(Flash|Torch)ModeAuto:基于周围环境光照情况自动关闭或打开
  • AVCapture(Flash|Torch)ModeOff:总是关闭
  • AVCapture(Flash|Torch)ModeOn:总是打开

修改闪光灯或手电筒设置的时候,一定要先锁定设备再修改,否则会挂掉。

- (BOOL)cameraHasFlash {
    return [[self activeCamera] hasFlash];
}

- (AVCaptureFlashMode)flashMode {
    return [[self activeCamera] flashMode];
}

- (void)setFlashMode:(AVCaptureFlashMode)flashMode {
    AVCaptureDevice *device = [self activeCamera];
    if ([device isFlashModeSupported:flashMode]) {
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.flashMode = flashMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

- (BOOL)cameraHasTorch {
    return [[self activeCamera] hasTorch];
}

- (AVCaptureTorchMode)torchMode {
    return [[self activeCamera] torchMode];
}

- (void)setTorchMode:(AVCaptureTorchMode)torchMode {
    AVCaptureDevice *device = [self activeCamera];
    if ([device isTorchModeSupported:torchMode]) {
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.torchMode = torchMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

8、其他一些设置

还有许多可以设置的属性,比如聚焦、曝光等等,设置起来差不多,首先要检测设备(摄像头)是否支持相应功能,锁定设备,而后设置相关属性。

再以对焦为例

// 询问激活中的摄像头是否支持兴趣点对焦
- (BOOL)cameraSupportsTapToFocus {
    return [[self activeCamera] isFocusPointOfInterestSupported];
}

// 点的坐标已经从屏幕坐标转换为捕捉设备坐标。
- (void)focusAtPoint:(CGPoint)point {
    AVCaptureDevice *device = [self activeCamera];
    if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
        // 确认是否支持兴趣点对焦并确认是否支持自动对焦模式。
        // 这一模式会使用单独扫描的自动对焦,并将focusMode设置为AVCaptureFocusModeLocked
        
        NSError *error;
        if ([device lockForConfiguration:&error]) {
            //锁定设备准备配置
            device.focusPointOfInterest = point;
            device.focusMode = AVCaptureFocusModeAutoFocus;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

关于屏幕坐标与设备坐标的转换

captureDevicePointOfInterestForPoint:--获取屏幕坐标系的CGPoint数据,返回转换得到的设备坐标系CGPoint数据

pointForCaptureDevicePointOfInterest:--获取社小偷坐标系的CGPoint数据,返回转换得到的屏幕坐标系CGPoint数据

推荐阅读更多精彩内容