AVFoundation 拍照/录制视频

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

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