首先介绍下实现拍照和录制视频需要用到的类:
-
AVCaptureVideoPreviewLayer
:捕获视频预览层。 -
AVCaptureSession
:捕获会话类。 -
AVCaptureDevice
:捕获设备类。 -
AVCaptureDeviceInput
:捕获设备输入类。 -
AVCapturePhotoOutput
:捕获照片输出类。 -
AVCaptureMovieFileOutput
:捕获电影文件输出类。 -
AVCaptureConnection
:捕获连接类。 -
AVCapturePhotoSettings
:捕获照片设置类。 -
AVAsset
:资产类。 -
AVAssetImageGenerator
:资产图片生成器类。
首先来看下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、我们将AVCaptureVideoPreviewLayer
的session
设置为AVCaptureSession
。
[(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
- 将捕捉数据直接输出到图层中,并确保与会话状态同步。
2、开始捕获
- (void)startSession {
if (![self.captureSession isRunning]) {
dispatch_async(self.videoQueue, ^{
[self.captureSession startRunning];
});
}
}
下面看下如何将捕获的内容生产图片。
一、拍照
同样先看流程图:
很明显拍照我们需要使用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;
}
}
二、录频
看下录频的操作:
录频核心代码:
- (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];
}
}
}