关于iOS多媒体这一块,苹果已经做了很好的封装。虽然一开始多少会被数量众多的对象吓到,不过稍微了解就会发现,其实内容并不难。主要有播放,拍摄,编辑,滤镜等场景,每一个场景都有固定的套路。准备在学习过程中主要关注几个固定的场景,每个场景写一个Demo,加深理解同时与大家分享。
废话不多说,今天的主题是利用AVFoundation框架拍摄照片。不管是拍摄照片还是视频,都属于媒体捕捉,用到的核心类是AVCaptureSession。顾名思义,AVCaptureSession代表一个会话,这个会话用于连接输入源AVCaptureInput,和输出AVCaptureOutput。AVCaptureInput和AVCaptureOutput都是抽象类,我们需要根据需要实例化不同的子类。
在本例中,我们使用AVCaptureDeviceInput作为输入源。既然我们指定例输入源是设备,那必然需要诸如摄像头,麦克风等物理设备。在AVFoundation中,提供了AVCaptureDevice类实现对物理设备等抽象。同时,AVCaptureOutput的常用子类有AVCaptureFileOutput(文件输出),AVCaptureVideoDataOutput(视频帧数据输出),AVCaptureStillImageOutput(图像输出)等。这里用AVCaptureStillImageOutput就可以了。此外还有一个AVCaptureVideoPreviewLayer提供了预览画面。图1.1是apple官方对Capture Session的框架介绍。好吧,看起来有点复杂,动手写下就明白了。
首先,我们创建一个新的项目,storyBoard中拖入两个全屏的View,分别命名为previewView和overlayView,并连线到h文件。
@property(weak,nonatomic)IBOutletUIView*previewView;
@property(weak,nonatomic)IBOutletUIView*overlayView;
再拖入一个Button作为拍摄按钮放到overlayView的底部,并连线到m文件并取名为:
- (IBAction)captureButtonClicked:(id)sender {
// TODO
}
打开m文件,添加五个属性:
@property(nonatomic,strong)AVCaptureSession*captureSession;
@property(nonatomic,strong)AVCaptureDevice*captureDevice;
@property(nonatomic,strong)AVCaptureDeviceInput*captureDeviceInput;
@property(nonatomic,strong)AVCaptureStillImageOutput*stillImageOutput;
@property(nonatomic,strong)AVCaptureVideoPreviewLayer*videoPreviewLayer;
有了这五个属性我们就可以创建一个完整的拍照应用了。怎么样,很厉害吧!
下面,我们在viewDidLoad中加入调用setupCaptureSession方法(专业点应该叫发送消息- -!),用于初始化:
- (void)setupCaptureSession {
// 1.创建会话
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetPhoto;
// 2.创建输入设备
self.captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 3.创建输入
NSError *error = nil;
self.captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:self.captureDevice error:&error];
// 3.创建输出
self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
self.stillImageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};
// 4.连接输入与会话
if ([self.captureSession canAddInput:self.captureDeviceInput]) {
[self.captureSession addInput:self.captureDeviceInput];
}
// 5.连接输出与会话
if ([self.captureSession canAddOutput:self.stillImageOutput]) {
[self.captureSession addOutput:self.stillImageOutput];
}
// 6.预览画面
self.videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:self.captureSession];
self.videoPreviewLayer.frame = self.previewView.bounds;
[self.previewView.layer addSublayer:self.videoPreviewLayer];
}
1.中sessionPreset属性用于控制会话的数据质量,在拍摄视频时比较有用,这里我们只需要设为AVCaptureSessionPresetPhoto就可以了。类似地,3.中outputSettings属性用于设置输出图像的格式。4,5两步都在连接Session前加了判断,这是很有必要的。因为AVFoundation部分涉及到硬件,不同手机硬件并不一样且较难调试,在做一些重要操作前加判断在AVFoundation中是很常见的。6.中将预览的layer添加到了previewView上,其实更好的实现是自定义一个View并在layerClass方法中返回AVCaptureVideoPreviewLayer类型。作为演示,我们在viewWillLayoutSubviews中加一句。
self.videoPreviewLayer.frame=self.previewView.bounds;
目前为止配置都完成了,下面就让Session跑起来吧!viewDidLoad中再加一个方法startSession。
- (void)startSession {
if(![self.captureSession isRunning]) {
[self.captureSession startRunning];
}
}
有start就有stop,下面我们再写一个方法,停止Session。
- (void)stopSession {
if([self.captureSession isRunning]) {
[self.captureSession stopRunning];
}
}
还记得我们前面的代码中还有一个TODO吗?是的,按下拍摄按钮并得到照片,我们的任务就完成了。
- (IBAction)captureButtonClicked:(id)sender {
// 1.获得连接
AVCaptureConnection *connection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
// 2.拍摄照片
[self.stillImageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
NSData *imageData = [AVCaptureStillImageOutput
jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
UIImage *image = [UIImage imageWithData:imageData];
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
}];
}
第一步获得AVCaptureConnection,不知道AVCaptureConnection是什么的可以看看图1.1。每个输入员都有不同的端口(port),例如后置摄像头既可以记录图像还能记录声音。一个Connection代表一个输入到输出的连接。第二步调用系统方法拍摄照片,这里返回的格式是CMSampleBufferRef,它是对基本数据的封装,从中还有时间格式等信息。要实现帧级别的媒体操作就需要用到它,这里我们先不深入。只需要将它转为UIImage,再调用系统函数保存即可。现在到相册看一眼,应该多出一张刚才拍摄的照片了吧。
系统的相机能实现诸如闪光灯,调整焦距等功能,这些功能的接口都已经在AVCaptureDevice中定义了。我们可以很方便的达到系统相机的效果。
下面我们需要在storyBoard加点东西。分别在左右上角增加闪光灯和摄像头Button,在overlayView上加入pinchGestureRecognizer。
点击闪光灯按钮,实现闪光灯打开或关闭:
- (IBAction)flashButtonClicked:(id)sender {
if([self.captureDevice isFlashActive]) {
[self setFlashMode:AVCaptureFlashModeOff];
}else{
[self setFlashMode:AVCaptureFlashModeOn];
}
}
我们需要实现setFlashMode:方法。注意在切换模式前,我们调用了lockForConfiguration:方法,它实现了对设备的原子操作。
- (void)setFlashMode:(AVCaptureFlashMode)mode {
if ([self.captureDevice isFlashModeSupported:mode]) {
NSError *error = nil;
if ([self.captureDevice lockForConfiguration:&error]) {
[self.captureDevice setFlashMode:mode];
[self.captureDevice unlockForConfiguration];
}
}
}
切换摄像头的设置稍微复杂一点,因为他涉及到切换AVCaptureDevice,也要注意到并不是所有设备都有前置摄像头的。点击button时,调用一下代码:
- (IBAction)cameraPositionButtonClicked:(id)sender {
AVCaptureDevice*device =nil;
if(self.captureDevice.position==AVCaptureDevicePositionFront) {
device = [self deviceWithPosition:AVCaptureDevicePositionBack];
}else{
device = [self deviceWithPosition:AVCaptureDevicePositionFront];
}
if(!device) {
return;
}else{
self.captureDevice= device;
}
NSError*error =nil;
AVCaptureDeviceInput*input = [AVCaptureDeviceInput deviceInputWithDevice:deviceerror:&error];
if(!error) {
[self.captureSession beginConfiguration];
[self.captureSession removeInput:self.captureDeviceInput];
if([self.captureSession canAddInput:input]) {
[self.captureSession addInput:input];
self.captureDeviceInput= input;
[self.captureSession commitConfiguration];
}
}
}
同样需要实现自定义方法deviceWithPosition:
- (AVCaptureDevice *)deviceWithPosition:(AVCaptureDevicePosition)position {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
要实现焦距调整,需要监听用户的捏合手势。基本思路是保存用户在UIGestureRecognizerStateBegan时的手指距离,当UIGestureRecognizerStateChanged时,对比捏合的差距,调整焦距。
首先要增加一个属性记录手指距离:
@property (nonatomic, assign) CGFloat lastPinchDistance;
连线方法如下:
- (IBAction)pinchGestureRecognizer:(UIPinchGestureRecognizer *)sender {
if (sender.numberOfTouches != 2) {
return;
}
CGPoint point1 = [sender locationOfTouch:0 inView:self.overlayView];
CGPoint point2 = [sender locationOfTouch:1 inView:self.overlayView];
CGFloat distanceX = point2.x = point1.x;
CGFloat distanceY = point2.y - point1.y;
CGFloat distance = sqrtf(distanceX * distanceX +distanceY * distanceY);
if (sender.state == UIGestureRecognizerStateBegan) {
self.lastPinchDistance = distance;
}
CGFloat change = distance - self.lastPinchDistance;
change = change / CGRectGetWidth(self.view.bounds);
[self zoomCamera:change];
self.lastPinchDistance = distance;
}
同样,我们还需要实现自定义方法zoomCamera:
- (void)zoomCamera:(CGFloat)change {
if (self.captureDevice.position == AVCaptureDevicePositionFront) {
return;
}
if (![self.captureDevice respondsToSelector:@selector(videoZoomFactor)]){
return;
}
NSError *error = nil;
if ([self.captureDevice lockForConfiguration:&error]) {
CGFloat max = MIN(self.captureDevice.activeFormat.videoMaxZoomFactor, 3.0);
CGFloat factor = self.captureDevice.videoZoomFactor;
CGFloat scale = MIN(MAX(factor + change, 1.0), max);
self.captureDevice.videoZoomFactor = scale;
[self.captureDevice unlockForConfiguration];
}
}
现在,我们的Demo已经实现了切换摄像头,控制闪光灯,调整焦距的功能,还有更多功能都是类似的,可以查看官方文档调用相应的接口实现。
Demo地址:https://github.com/WorthyZhang/WZCameraDemo