美文网首页
AVFoundation 拍照/录制视频

AVFoundation 拍照/录制视频

作者: Maji1 | 来源:发表于2020-09-17 23:53 被阅读0次

    首先介绍下实现拍照和录制视频需要用到的类:

    • AVCaptureVideoPreviewLayer:捕获视频预览层。
    • AVCaptureSession:捕获会话类。
    • AVCaptureDevice:捕获设备类。
    • AVCaptureDeviceInput:捕获设备输入类。
    • AVCapturePhotoOutput:捕获照片输出类。
    • AVCaptureMovieFileOutput:捕获电影文件输出类。
    • AVCaptureConnection:捕获连接类。
    • AVCapturePhotoSettings:捕获照片设置类。
    • AVAsset:资产类。
    • AVAssetImageGenerator:资产图片生成器类。

    首先来看下AVCaptureSession初始化的流程:

    AVCaptureSession初始化配置

    通过该流程图可以看出,AVCaptureSession的初始化配置需要:
    1、视频输入设备 。
    2、音频输入设备。
    3、照片输出对象 。
    3、电影文件输出对象。

    看核心代码:

    - (BOOL)setupSession:(NSError **)error {
        self.captureSession = [[AVCaptureSession alloc] init];
        self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
        //视频输入设备
        AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
        AVCaptureDeviceInput *videoDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:videoDevice error:error];
        if (videoDeviceInput) {
            if ([self.captureSession canAddInput:videoDeviceInput]) {
                [self.captureSession addInput:videoDeviceInput];
                self.activeVideoInput = videoDeviceInput;
            } else {
                return NO;
            }
        } else {
            return NO;
        }
        
        //音频输入设备
        AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
        AVCaptureDeviceInput *audioDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioDevice error:error];
        if (audioDeviceInput) {
            if ([self.captureSession canAddInput:audioDeviceInput]) {
                [self.captureSession addInput:audioDeviceInput];
            } else {
                return NO;
            }
        } else {
            return NO;
        }
        
        //从实例摄像头中捕捉静态图片
        self.photoOutput = [[AVCapturePhotoOutput alloc] init];
        if ([self.captureSession canAddOutput:self.photoOutput]) {
            [self.captureSession addOutput:self.photoOutput];
        }
        
        
        //用于将电影录制到文件系统
        self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];
        if ([self.captureSession canAddOutput:self.movieOutput]) {
            [self.captureSession addOutput:self.movieOutput];
        } 
        self.videoQueue = dispatch_queue_create("CQCamera.Video.Queue", NULL);
        return YES;
    }
    

    这段代码最后我们还创建了个全局的串行队列videoQueue,在后面开始捕获和录制时需要使用。

    从上图中我们还看到,在AVCaptureSession初始化配置结束后又做了两个操作。
    1、我们将AVCaptureVideoPreviewLayersession设置为AVCaptureSession

    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
    
    • 将捕捉数据直接输出到图层中,并确保与会话状态同步。

    2、开始捕获

    - (void)startSession {
        if (![self.captureSession isRunning]) {
            dispatch_async(self.videoQueue, ^{
              [self.captureSession startRunning];
            });
        }
    }
    

    下面看下如何将捕获的内容生产图片。

    一、拍照

    同样先看流程图:


    AVCapturePhotoOutput

    很明显拍照我们需要使用AVCapturePhotoOutput 捕获照片输出对象。

    看代码:

    - (void)captureStillImage {
        AVCaptureConnection *connection = [self.photoOutput connectionWithMediaType:AVMediaTypeVideo];
        if (connection.isVideoOrientationSupported) {
              connection.videoOrientation = [self currentVideoOrientation];
          }
        self.photoSettings = [AVCapturePhotoSettings photoSettingsWithFormat:@{AVVideoCodecKey:AVVideoCodecTypeJPEG}];
        [self.photoOutput capturePhotoWithSettings:self.photoSettings delegate:self];
    }
    
    • 拍照时我们需要拿到捕获连接对象(AVCaptureConnection),设置视频的方向,否则在横竖屏切换时会出现问题。
    • 在代理方法中我们利用捕获连接对象(AVCaptureConnection)调用fileDataRepresentation方法获取二进制图片。

    获取到图片后需要利用Photos库将图片保存到相册。

    Photos

    将图片保存到相册我们首先要判断是否有权限,这个需要在plist文件中配置在这就不多说了。

    下面我们来看下将图片添加到指定相册的流程:

    • 第一步:添加图片到【相机胶卷】。
      1.1: UIImageWriteToSavedPhotosAlbum函数
      1.2: AssetsLibrary框架(已过期,一般不用了)
      1.3: Photos框架(推荐)

    • 第二步:拥有一个【自定义相册】
      2.1: AssetsLibrary框架
      2.2: Photos框架(推荐)

    • 第三步:将刚才添加到【相机胶卷】的图片,引用(添加)到【自定义相册】
      3.1: AssetsLibrary框架
      3.2: Photos框架(推荐)

    Photos框架相关类须知:
    1、PHAsset:一个PHAsset对象代表一张图片或者一个视频文件。
    负责查询一堆的图片或者视频文件(PHAsset对象)。

    2、PHAssetCollection:一个PHAssetCollection对象代表一个相册。
    负责查询一堆的相册(PHAssetCollection对象)。

    3、PHAssetChangeRequest: 负责执行对PHAsset(照片或视频)的【增删改】操作。
    这个类只能放在-[PHPhotoLibrary performChanges:completionHandler:]或者 -[PHPhotoLibrary performChangesAndWait:error:]方法的block中使用。

    4、PHAssetCollectionChangeRequest:负责执行对PHAssetCollection(相册)的【增删改】操作。
    这个类只能放在-[PHPhotoLibrary performChanges:completionHandler:] 或者 -[PHPhotoLibrary performChangesAndWait:error:]方法的block中使用。

    • 保存图片到 相机胶卷:
    + (PHFetchResult<PHAsset *> *)savePhoto:(UIImage *)image {
        __block NSString *createdAssetId = nil;
        // Synchronously 同步执行操作
        NSError *error;
        [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
            createdAssetId = [PHAssetChangeRequest creationRequestForAssetFromImage:image].placeholderForCreatedAsset.localIdentifier;
        } error:&error];
        if (error == nil) {
            NSLog(@"保存成功");
        } else {
            NSLog(@"保存图片Error: %@", error.localizedDescription);
            return nil;
        }
    //    // Asynchronously 异步执行操作
    //    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
    //        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    //    } completionHandler:^(BOOL success, NSError * _Nullable error) {
    //        if (success) {
    //            NSLog(@"保存成功");
    //        } else {
    //            NSLog(@"保存图片Error: %@", error.localizedDescription);
    //        }
    //    }];
        
        //PHAsset:查询图片/视屏
        PHFetchOptions *options = nil;
        PHFetchResult<PHAsset *> *createdAssets = [PHAsset fetchAssetsWithLocalIdentifiers:@[createdAssetId] options:options];
        return createdAssets;
    }
    
    • 获取指定相册:
    + (PHAssetCollection *)getAlbumWithTitle:(NSString *)title {
        __block PHAssetCollection *createdCollection = nil;// 已经创建的自定义相册
        //PHAssetCollection: 查询所有的自定义相册
        PHFetchResult<PHAssetCollection *> *collections = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
        [collections enumerateObjectsUsingBlock:^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([collection.localizedTitle isEqualToString:title]) {
                createdCollection = collection;
                *stop = YES;
            }
        }];
    
        if (!createdCollection) { // 没有创建过相册
            __block NSString *createdCollectionId = nil;
            [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
                //PHAssetCollectionChangeRequest:【增】相册
                createdCollectionId = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:title].placeholderForCreatedAssetCollection.localIdentifier;
            } error:nil];
            
            //PHAssetCollection:【查】出相册
            createdCollection = [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[createdCollectionId] options:nil].firstObject;
        }
        
        return createdCollection;
    }
    
    • 保存图片到 指定相册:
    + (BOOL)addAssetsToAlbumWithAssets:(id<NSFastEnumeration>)assets withAlbum:(PHAssetCollection *)assetCollection {
        // 将刚才添加到【相机胶卷】的图片,引用(添加)到【自定义相册】
        NSError *errorCollection = nil;
        [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
            PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:assetCollection];
            // 自定义相册封面默认保存第一张图,所以使用以下方法把最新保存照片设为封面
            [request insertAssets:assets atIndexes:[NSIndexSet indexSetWithIndex:0]];
        } error:&errorCollection];
        
        // 保存结果
        if (errorCollection) {
            NSLog(@"保存到指定 相册 失败!");
            return NO;
        } else {
            NSLog(@"保存到指定 相册 成功!");
            return YES;
        }
    }
    

    二、录频

    看下录频的操作:


    AVCaptureMovieFileOutput

    录频核心代码:

    - (void)startRecording {
        if (self.isRecording) return;
        AVCaptureDevice *device = [self activeCamera];
        //平滑对焦,减缓摄像头对焦速度。移动拍摄时,摄像头会尝试快速对焦
        if (device.isSmoothAutoFocusEnabled) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.smoothAutoFocusEnabled = YES;
                [device unlockForConfiguration];
            } else {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
        }
        
        AVCaptureConnection *connection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
        if (connection.isVideoOrientationSupported) {
            connection.videoOrientation = [self currentVideoOrientation];
        }
        //判断是否支持视频稳定。提高视频的质量。
        if (connection.isVideoStabilizationSupported) {
            connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
        }
        
        self.outputURL = [self uniqueURL];
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
    }
    
    • 1、我们首先需要拿到设置AVCaptureSession是创建的 视频捕获设备输入(AVCaptureDeviceInput),然后取出设备(AVCaptureDevice),配置设备的平滑对焦属性。
    • 2、然后同样需要拿到捕获连接对象(AVCaptureConnection),设置视频的方向,否则在横竖屏切换时会出现问题。并且需要设置preferredVideoStabilizationMode属性提高视频的质量。
    • 3、调用startRecordingToOutputFileURL:recordingDelegate:方法开始录屏。
    • 4、停止录屏。
    • 5、录屏结束后在代理方法中获取到我们的视频地址。

    录屏结束后我们可能需要获取视频的某一帧图片,用来显示到UI上。看下操作步骤:


    流程图很简单,看下代码:

    //生成视频缩略图
    - (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL {
        dispatch_async(self.videoQueue, ^{
            AVAsset *asset = [AVAsset assetWithURL:videoURL];
            AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
            //设置maximumSize 宽为100,高为0 根据视频的宽高比来计算图片的高度
            imageGenerator.maximumSize = CGSizeMake(100.0, 0.0);
            //捕捉视频缩略图会考虑视频的变化(如视频的方向变化),如果不设置,缩略图的方向可能出错.
            imageGenerator.appliesPreferredTrackTransform = YES;
            NSError *error;
            CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:&error];
            if (imageRef == nil) {
                NSLog(@"imageRefError: %@", error);
            }
            UIImage *image = [UIImage imageWithCGImage:imageRef];
            CGImageRelease(imageRef);
        });
    }
    

    到此我们的拍照和录屏的核心功能已经实现了。下面介绍一下跟拍照录屏相关的一些功能:切换摄像头、聚焦、曝光、闪光灯、手电筒。

    切换摄像头

    我们现在的手机设备一般都有前置和后置摄像头,所以我们这里就是对前置和后置摄像头的切换。

    - (BOOL)switchCameras {
        AVCaptureDevice *currentDevice = [self activeCamera];
        AVCaptureDevice *device;
        if (currentDevice.position == AVCaptureDevicePositionBack) {
            device = [self cameraWithPosition:AVCaptureDevicePositionFront];
        } else {
            device = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }
        if (device == nil) { return NO; }
        
        NSError *error;
        AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
        if (deviceInput) {
            [self.captureSession beginConfiguration];
            [self.captureSession removeInput:self.activeVideoInput];
            if ([self.captureSession canAddInput:deviceInput]) {
                [self.captureSession addInput:deviceInput];
                self.activeVideoInput = deviceInput;
            } else {
                [self.captureSession addInput:self.activeVideoInput];
            }
            //配置完成后. 会分批的将所有变更整合在一起。
            [self.captureSession commitConfiguration];
            return YES;
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
            return NO;
        }
    }
    
    • 1、先拿到摄像头设备device
    • 2、将摄像头包装到AVCaptureDeviceInput类型的对象中。
    • 3、一定要先调用beginConfiguration方法,准备配置。
    • 4、removeInput:移除原来的捕获设备输入对象(`AVCaptureDeviceInput )。
    • 5、判断能否添加canAddInput:新的捕获设备输入对象(`AVCaptureDeviceInput )。
    • 6、如果可以就添加addInput:,设置为当前正在使用的捕获设备输入对象。
    • 7、如果不可以添加,再将原来的捕获设备输入对象(`AVCaptureDeviceInput )添加进去。
    • 8、最后调用commitConfiguration方法,分批的将所有变更整合在一起。

    获取前置或后置摄像头的代码:

    - (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
           //AVCaptureDeviceTypeBuiltIn Microphone:话筒
            //AVCaptureDeviceTypeBuiltIn WideAngleCamera:广角照相机
            //AVCaptureDeviceTypeBuiltIn TelephotoCamera:长焦照相机
            //AVCaptureDeviceTypeBuiltIn UltraWideCamera:超宽摄影机
            //AVCaptureDeviceTypeBuiltIn DualCamera:双摄像头
            //AVCaptureDeviceTypeBuiltIn DualWideCamera:双宽摄像头
            //AVCaptureDeviceTypeBuiltIn TripleCamera:三重摄影机
            //AVCaptureDeviceTypeBuiltIn TrueDepthCamera:真深度照相机
            //AVCaptureDeviceTypeBuiltIn DuoCamera:双后置摄像头
            NSArray<AVCaptureDeviceType> *deviceTypes =@[
            AVCaptureDeviceTypeBuiltInMicrophone,
            AVCaptureDeviceTypeBuiltInTelephotoCamera,
            AVCaptureDeviceTypeBuiltInWideAngleCamera,
            AVCaptureDeviceTypeBuiltInDualCamera
            ];
        AVCaptureDeviceDiscoverySession *deviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes mediaType:AVMediaTypeVideo position:position];
        return deviceDiscoverySession.devices.firstObject;
    }
    

    聚焦 & 曝光

    • 聚焦
    - (void)focusAtPoint:(CGPoint)point {
        AVCaptureDevice *device = [self activeCamera];
        //是否支持兴趣点聚焦 和 自动聚焦
        if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {//锁定设备
                device.focusPointOfInterest = point;//聚焦点
                device.focusMode = AVCaptureFocusModeAutoFocus;//设置为自动聚焦
                [device unlockForConfiguration];//解锁设备
            } 
        }
    }
    
    • 曝光
    - (void)exposeAtPoint:(CGPoint)point {
        AVCaptureDevice *device = [self activeCamera];
        AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;
        //是否支持兴趣点曝光 和 持续自动曝光。
        if (device.isExposurePointOfInterestSupported && [device isExposureModeSupported:exposureMode]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                //配置期望值
                device.exposurePointOfInterest = point;
                device.exposureMode = exposureMode;
                //判断设备是否支持锁定曝光的模式。
                if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                    
                    //支持,则使用kvo确定设备的adjustingExposure属性的状态。
                    [device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&THCameraAdjustingExposureContext];
                }
                [device unlockForConfiguration];
            } 
        }
    }
    

    这里曝光用到了kvo进行监听属性的状态:

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context {
        if (context == &THCameraAdjustingExposureContext) {
            AVCaptureDevice *device = (AVCaptureDevice *)object;
            //判断设备是否不再调整曝光等级,
            //确认设备的exposureMode是否可以设置为AVCaptureExposureModeLocked
            if(!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                //移除作为adjustingExposure 的self,就不会得到后续变更的通知
                [object removeObserver:self forKeyPath:@"adjustingExposure" context:&THCameraAdjustingExposureContext];
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSError *error;
                    if ([device lockForConfiguration:&error]) {
                        //修改exposureMode
                        device.exposureMode = AVCaptureExposureModeLocked;
                        [device unlockForConfiguration];
                    } 
                });
            }
        } else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        } 
    }
    

    闪光灯 & 手电筒

    • 闪光灯
    - (void)setFlashMode:(AVCaptureFlashMode)flashMode {
        if ([self.photoOutput.supportedFlashModes containsObject:@(flashMode)]) {
            self.photoSettings.flashMode = flashMode;
        }
    }
    
    • 手电筒
    - (void)setTorchMode:(AVCaptureTorchMode)torchMode {
        AVCaptureDevice *device = [self activeCamera];
        if ([device isTorchModeSupported:torchMode]) {
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.torchMode = torchMode;
                [device unlockForConfiguration];
            } 
        }
    }
    

    相关文章

      网友评论

          本文标题:AVFoundation 拍照/录制视频

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