AVFoundation自定义相机
一般需要使用相机时候, 调用系统的相机就可以了, 但是如果有复杂的自定义拍照需求的话, 使用更强大的AVFoundation就会很方便, 可以实现自定义拍照界面, 不显示预览层的盲拍, 以及不存储手机相册, 音量键拍照等功能都可以轻松实现.
导入依赖库 AVFoundation.framework
首先导入一个头文件
#import <AVFoundation/AVFoundation.h>
由于后面我们需要将拍摄好的照片写入系统相册中,所以我们在这里还需要导入一个相册需要的头文件
#import <AssetsLibrary/AssetsLibrary.h>
导入头文件后我们需要创建几个相机必须的属性
/**
* AVCaptureSession对象来执行输入设备和输出设备之间的数据传递
*/
@property (nonatomic, strong) AVCaptureSession* session;
/**
* 输入设备
*/
@property (nonatomic, strong) AVCaptureDeviceInput* videoInput;
/**
* 照片输出流
*/
@property (nonatomic, strong) AVCaptureStillImageOutput* stillImageOutput;
/**
* 预览图层
*/
@property (nonatomic, strong) AVCaptureVideoPreviewLayer* previewLayer;
AVCaptureSession控制输入和输出设备之间的数据传递
AVCaptureDeviceInput调用所有的输入硬件。例如摄像头和麦克风
AVCaptureStillImageOutput用于输出图像
AVCaptureVideoPreviewLayer镜头捕捉到得预览图层
接下来初始化所有对象,下面这个方法的调用我放到viewDidLoad里面调用了
- (void)initAVCaptureSession{
self.session = [[AVCaptureSession alloc] init];
NSError *error;
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//更改这个设置的时候必须先锁定设备,修改完后再解锁,否则崩溃
[device lockForConfiguration:nil];
//设置闪光灯为自动
[device setFlashMode:AVCaptureFlashModeAuto];
//这句代码是对夜间拍照时候的自动补光, 如果没有这句代码, 晚上拍照基本上是黑色的, 比苹果系统的相机照片差很多
if (device.isLowLightBoostSupported) {
device.automaticallyEnablesLowLightBoostWhenAvailable = YES;
}
[device unlockForConfiguration];
self.videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
if (error) {
NSLog(@"%@",error);
}
self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
//输出设置。AVVideoCodecJPEG 输出jpeg格式图片
NSDictionary * outputSettings = [[NSDictionary alloc] initWithObjectsAndKeys:AVVideoCodecJPEG,AVVideoCodecKey, nil];
[self.stillImageOutput setOutputSettings:outputSettings];
if ([self.session canAddInput:self.videoInput]) {
[self.session addInput:self.videoInput];
}
if ([self.session canAddOutput:self.stillImageOutput]) {
[self.session addOutput:self.stillImageOutput];
}
//初始化预览图层
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.session];
[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
NSLog(@"%f",kMainScreenWidth);
self.previewLayer.frame = CGRectMake(0, 0,kMainScreenWidth, kMainScreenHeight - 64);
self.backView.layer.masksToBounds = YES;
[self.backView.layer addSublayer:self.previewLayer];
}
之后在viewWillAppear,viewDidDisappear方法里开启和关闭session
注意:session的开启时很消耗CUP的 一直处于开启状态 会导致手机发热 不建议放在viewWillApper方法里面开启和viewDidDisappear里面关闭
最好是拍照前开启 拍照后关闭
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:YES];
if ([self.session isRunning]) {
}else{
[self.session startRunning];
}
}
- (void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:YES];
if ([self.session isRunning]) {
[self.session stopRunning];
}
}
到这里所有的初始化工作基本完成,运行程序可以看到镜头捕捉到得画面。接下来实现拍照按钮
接下来搞一个获取设备方向的方法,再配置图片输出的时候需要使用
-(AVCaptureVideoOrientation)avOrientationForDeviceOrientation:(UIDeviceOrientation)deviceOrientation
{
AVCaptureVideoOrientation result = (AVCaptureVideoOrientation)deviceOrientation;
if ( deviceOrientation == UIDeviceOrientationLandscapeLeft )
result = AVCaptureVideoOrientationLandscapeRight;
else if ( deviceOrientation == UIDeviceOrientationLandscapeRight )
result = AVCaptureVideoOrientationLandscapeLeft;
return result;
}
下面是拍照按钮方法
- (IBAction)takePhotoButtonClick:(UIBarButtonItem *)sender {
AVCaptureConnection *stillImageConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
UIDeviceOrientation curDeviceOrientation = [[UIDevice currentDevice] orientation];
AVCaptureVideoOrientation avcaptureOrientation = [self avOrientationForDeviceOrientation:curDeviceOrientation];
[stillImageConnection setVideoOrientation:avcaptureOrientation];
[stillImageConnection setVideoScaleAndCropFactor:1];
[self.stillImageOutput captureStillImageAsynchronouslyFromConnection:stillImageConnection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
NSData *jpegData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
//以下是写入手机相册的代码 如果有不写入手机相册的需求 注掉以下代码 就好
CFDictionaryRef attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault,
imageDataSampleBuffer,
kCMAttachmentMode_ShouldPropagate);
ALAuthorizationStatus author = [ALAssetsLibrary authorizationStatus];
if (author == ALAuthorizationStatusRestricted || author == ALAuthorizationStatusDenied){
//无权限
return ;
}
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeImageDataToSavedPhotosAlbum:jpegData metadata:(__bridge id)attachments completionBlock:^(NSURL *assetURL, NSError *error) {
}];
//以上是写入手机相册的代码 如果有不写入手机相册的需求 注掉以下代码 就好
//这里还有一个问题 就是如果旋转手机后拍照后直接上传到服务器 服务器收到的图片是反的(上下颠倒) 但是如果是在手机相册查看是正常的 所以这里需要做一下处理
UIImage *img = [UIImage imageWithData:jpegData];
if (img.imageOrientation == UIImageOrientationDown) {
NSLog(@"照片反了!!!!");
img = [self normalizedImage:img];
}
if (img.imageOrientation == UIImageOrientationUp) {
NSLog(@"照片正常!!");
}
//这里写上传服务器的代码就好了 将img传过去即可
}];
}
- (UIImage *)normalizedImage:(UIImage *)image {
if (image.imageOrientation == UIImageOrientationUp){
return image;
}
UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
[image drawInRect:(CGRect){0, 0, image.size}];
UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return normalizedImage;
}
至此相机的拍照功能已经完成
- [stillImageConnection setVideoScaleAndCropFactor:1];这个方法是控制焦距用的暂时先固定为1,后边写手势缩放焦距的时候会修改这里
- 照片写入相册之前需要进行旋转(我在代码里并没有进行旋转)
-
写入相册之前需要判断用户是否允许了程序访问相册,否则程序会崩溃,包括在开启相机的时候和拍摄按钮点击的时候都需要做安全验证,验证设别是否支持拍照,用户是否允许程序访问相机。
接下来完成闪光灯
-
- (IBAction)flashButtonClick:(UIBarButtonItem *)sender { NSLog(@"flashButtonClick"); AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; //修改前必须先锁定 [device lockForConfiguration:nil]; //必须判定是否有闪光灯,否则如果没有闪光灯会崩溃 if ([device hasFlash]) { if (device.flashMode == AVCaptureFlashModeOff) { device.flashMode = AVCaptureFlashModeOn; [sender setTitle:@"flashOn"]; } else if (device.flashMode == AVCaptureFlashModeOn) { device.flashMode = AVCaptureFlashModeAuto; [sender setTitle:@"flashAuto"]; } else if (device.flashMode == AVCaptureFlashModeAuto) { device.flashMode = AVCaptureFlashModeOff; [sender setTitle:@"flashOff"]; } } else { NSLog(@"设备不支持闪光灯"); } [device unlockForConfiguration]; }
</pre><pre>
闪光灯的设置非常简单,只需要修改device的flashMode属性即可,这里需要注意的是,修改device时候需要先锁住,修改完成后再解锁,否则会崩溃,设置闪光灯的时候也需要做安全判断,验证设备是否支持闪光灯,有些iOS设备是没有闪光灯的,如果不做判断还是会crash掉 T_T
剩下一个小功能就是切回镜头了,方法如下
- (IBAction)switchCameraSegmentedControlClick:(UISegmentedControl *)sender { NSLog(@"%ld",(long)sender.selectedSegmentIndex); AVCaptureDevicePosition desiredPosition; if (isUsingFrontFacingCamera){ desiredPosition = AVCaptureDevicePositionBack; }else{ desiredPosition = AVCaptureDevicePositionFront; } for (AVCaptureDevice *d in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) { if ([d position] == desiredPosition) { [self.previewLayer.session beginConfiguration]; AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:d error:nil]; for (AVCaptureInput *oldInput in self.previewLayer.session.inputs) { [[self.previewLayer session] removeInput:oldInput]; } [self.previewLayer.session addInput:input]; [self.previewLayer.session commitConfiguration]; break; } } isUsingFrontFacingCamera = !isUsingFrontFacingCamera; }
isUsingFrontFacingCamera这个属性是个BOOL值变量,前面忘记写这个属性了。用于防止重复切换统一摄像头,调用这个点击方法的控件是个segement,文章最后我会附上demo地址。
最后一步就是加入手势缩放,手动调节相机焦距。
加入两个属性,并遵守这个协议<UIGestureRecognizerDelegate>
这两个属性分别用于记录缩放的比例。相机支持的焦距是1.0~67.5,所以再控制器加载的时候分别给这两个属性附上一个初值 1.0。之后给view添加一个缩放手势,手势调用的方法如下/** * 记录开始的缩放比例 */ @property(nonatomic,assign)CGFloat beginGestureScale; /** * 最后的缩放比例 */ @property(nonatomic,assign)CGFloat effectiveScale;
这样之再实现一个delegate//缩放手势 用于调整焦距 - (void)handlePinchGesture:(UIPinchGestureRecognizer *)recognizer{ BOOL allTouchesAreOnThePreviewLayer = YES; NSUInteger numTouches = [recognizer numberOfTouches], i; for ( i = 0; i < numTouches; ++i ) { CGPoint location = [recognizer locationOfTouch:i inView:self.backView]; CGPoint convertedLocation = [self.previewLayer convertPoint:location fromLayer:self.previewLayer.superlayer]; if ( ! [self.previewLayer containsPoint:convertedLocation] ) { allTouchesAreOnThePreviewLayer = NO; break; } } if ( allTouchesAreOnThePreviewLayer ) { self.effectiveScale = self.beginGestureScale * recognizer.scale; if (self.effectiveScale < 1.0){ self.effectiveScale = 1.0; } NSLog(@"%f-------------->%f------------recognizerScale%f",self.effectiveScale,self.beginGestureScale,recognizer.scale); CGFloat maxScaleAndCropFactor = [[self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo] videoMaxScaleAndCropFactor]; NSLog(@"%f",maxScaleAndCropFactor); if (self.effectiveScale > maxScaleAndCropFactor) self.effectiveScale = maxScaleAndCropFactor; [CATransaction begin]; [CATransaction setAnimationDuration:.025]; [self.previewLayer setAffineTransform:CGAffineTransformMakeScale(self.effectiveScale, self.effectiveScale)]; [CATransaction commit]; } }
在每次手势开始的时候把上一次实际缩放值赋给初始缩放值,如果不这么做的话你会发现每次手势开始的时候界面都会跳来跳去的(非常性感)。一个简单功能的相机基本上完成了,最后一步就是之前我们在拍照的方法里写死了一个1.0,我们还需要修改一下它,,否则虽然你看到的界面焦距改变了,但是实际拍出来的照片是没有变化的。找到拍照方法里的- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ( [gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]] ) { self.beginGestureScale = self.effectiveScale; } return YES; }
修改为[stillImageConnection setVideoScaleAndCropFactor:1.0];
[stillImageConnection setVideoScaleAndCropFactor:self.effectiveScale];
- 如果是横屏开发时候会出现预览层貌似旋转的效果
- 解决方法:
- 添加代码
注意事项: 如果使用过程中出现 拍摄照片是黑色的(就像是在晚上拍的一样)self.previewLayer.orientation = AVCaptureVideoOrientationLandscapeRight;
- 是以下代码的使用问题
-
if (self.session) { [self.session startRunning]; }
- demo演示下载地址
- https://github.com/RockyAo/RACustomCamera