AVFoundation开发秘籍笔记:第6章 捕捉媒体

2020-10-16  本文已影响0人  AlanGe

6.1 捕捉功能综述

AV Foundation的照片和视频捕捉功能从框架搭建之初就是它的一个强项。从iOS 4版本开始,开发者就可以直接访问iOS设备的摄像头和摄像头生成的数据,定义一个新的用于照片和视频应用的类。框架的捕捉功能仍然是苹果公司媒体工程师最关注的领域,每个新版本的发布都带来强大的新功能和提升。虽然核心的捕捉类在iOS和OS X上是一致的,但你会发现不同平台下的框架还是有一些区别,这些区别一般都是针对平台的定制功能。比如,Mac OSX为截屏功能定义了AVCaptureScreenInput类,而iOS由 于沙盒的限制就没有这个类。我们对于AVFoundation捕捉功能的讲解主要以iOS版本为例,但讨论的大部分功能都适用于OS X。

当开发一个带有捕捉功能的应用程序时会用到许多类,学习框架的功能第一步就是了解其中包含的各个类及每个类所扮演的角色和职责。图6-1给出了开发捕捉功能应用程序时可能用到的一些类。


6.1.1 捕捉会话

AV Foundation捕捉栈的核心类是AVCaptureSession。一个捕捉会话相当于一个虚拟的“插线板”,用于连接输入和输出的资源。捕捉会话管理从物理设备得到的数据流,比如摄像头和麦克风设备,输出到一个或多个目的地。可以动态配置输入和输出的线路,让开发者能够在会话进行中按需重新配置捕捉环境。

捕捉会话还可以额外配置一个 会话预设值(session preset),用来控制捕捉数据的格式和质量。会话预设值默认为AVCaptureSessionPresetHigh, 它适用于大多数情况,不过框架仍然提供了多个预设值对输出进行定制,以满足应用程序的特殊需求。

6.1.2 捕捉设备

AVCaptureDevice为诸如摄像头或麦克风等物理设备定义了一个接口。大多数情况下,这些设备都内置于Mac、iPhone或iPad中,不过也可能是外部数码相机或便携式摄像机。AVCaptureDevice针对物理硬件设备定义了大量的控制方法,比如控制摄像头的对焦、曝光、白平衡和闪光灯等。

AVCaptureDevice定义了大量类方法用于访问系统的捕捉设备,最常用的一个方法是defaultDeviceWithMediaType:,它会根据给定的媒体类型返回一个 系统指定的默认设备。如下例所示:

AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

示例中我们请求一个默认的视频设备,在包含前置摄像头和后置摄像头的iOS系统下,这个方法会返回后置摄像头,因为它是系统默认的摄像头。在带有摄像头的Mac机器上,会返回内置的FaceTime摄像头。

6.1.3 捕捉设备的输入

在使用捕捉设备进行处理前,首先需要将它添加为捕捉会话的输入。不过一个捕捉设备不能直接添加到AVCaptureSession中,但是可以通过将它封装在一个AVCaptureDevicelnput实例中来添加。这个对象在设备输出数据和捕捉会话间扮演接线板的作用。使用deviceInputWithDevice:error:方法创建AVCaptureDeviceInput,如下所示:

NSError *error;
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputwithDevice:videoDevice error:serror];

需要为这个方法传递一个有 效的NSError指针,因为输入创建中会遇到的错误描述信息都会在这里体现。

6.1.4 捕捉的输出

AV Foundation定义了AVCaptureOutput的许多扩展类。AVCaptureOutput是一个抽象基类,用于为从捕捉会话得到的数据寻找输出目的地。框架定义了一些这个抽象类的高级扩展类,比如AVCapturetlillmageOutput和AVCaptureMovieFileOutput,使用它们可以很容易地实现捕捉静态照片和视频的功能。还可以在这里找到底层扩展,比如AVCaptureAudioDataOutput和AVCaptureVideoDataOutput,使用它们可以直接访问硬件捕捉到的数字样本。使用这些底层输出类需要对捕捉设备的数据渲染有更好的理解,不过这些类可以提供更强大的功能,比如对音频和视频流进行实时处理。

6.1.5 捕捉连接

在上面的插图中有一个类没 有明确的名字,而是由一一个连接 不同组件的黑色箭头所表示,这就是AVCaptureConnection类。捕捉会话首先需要确定由给定捕捉设备输入渲染的媒体类型,并自动建立其到能够接收该媒体类型的捕捉输出端的连接。比如,AVCaptureMovieFileOutput可以接受音频和视频数据,所以会话会确定哪些输入产生视频,哪些输入产生音频,并正确地建立该连接。对这些连接的访问可以让开发者对信号流进行底层的控制,比如禁用某些特定的连接,或在音频连接中访问单独的音频轨道。

6.1.6 捕捉预览

如果不能在影像捕捉中看到正在捕捉的场景,那么应用程序就不会那么好用。幸运的是,框架定义了AVCaptureVideoPreviewLayer类来满足该需求。预览层是一个Core Animation的CALayer子类,对捕捉视频数据进行实时预览。这个类所扮演的角色类似于AVPlayerLayer,不过还是针对摄像头捕捉的需求进行了定制。像AVPlayerLayer一样 ,AVCaptureVideo-PreviewLayer也支持视频重力的概念,可以控制视频内容渲染的缩放和拉伸效果,如图6-2、图6-3和图6- 4所示。


6.2 简单的秘籍

我们需要站在更高层面理解捕捉类,那么先看一下如何为简单的相机应用程序创建一个捕捉会话。

// 1. Create a capture session.
AVCaptureSession *session = [[AVCaptureSession alloc] init];

//2. Get a reference to the default camera.
AVCaptureDevice *cameraDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

//3. Create a device input for the camera .
NSError *error;
AVCaptureDeviceInput * cameraInput = [AVCaptureDeviceInput deviceInputWithDevice:cameraDevice error:serror] ;

//4. Connect the input to the session.
if ([session canAddInput:cameraInput]) {
    [session addInput:cameraInput] ;
}

//5. Create an AVCaptureOutput to capture still images .
AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init] ;

// 6. Add the output to the session.
if ([session canAddOutput:imageOutput]) {
    [session addOutput:imageOutput] ;
}
// 7. Start the session and begin the fl

上面的示例中,我们为从默认摄像头捕捉静态图片建立了基础的架构。创建了一个捕捉会话,经过捕捉设备输入将默认摄像头添加到会话中,将一个捕捉输出添加到会话用于输出静态图片,然后开始运行该会话,视频数据流就可以开始在系统中传输。典型的会话创建过程会比示例中的代码复杂一些,不过我们的示例已经很好地将这些核心组件组合在一起了。

现在我们对核心的捕捉类都已经有所了解,那么开始通过实际的应用程序更加深入的了解AVFoundation有关捕捉的功能吧。

6.3 创建相机应用程序

本节将通过创建一个名为Kamera的相机应用程序(如图6-5所示)来详细学习AVFoundation有关捕捉的API。示例项目是模仿苹果手机内置的Camera应用程序,支持用户拍摄高质量静态图片和视频,并通过Assets Library框架将它们写入iOS Camera Roll。在实现这个应用程序的过程中,你会实现完成这一核心功能所需的大量实用方法,会对AVFoundation有关捕捉的类有更深入的理解。可以在Chapter6目录中找到名为Kamera_Starter的初始项目。


注意:
开发Kamera应用程序需要开发者使用真机进行编译和测试。大多数AV Foundation的功能可以用iOs模拟器进行测试,不过捕捉相关API只能在真机上测试。

6.3.1 创建预览视图

图6-6给出了Kamera应用程序用户界面的组成图示。我们把重点放在中间层THPreviewView的实现上,因为它直接包含AV Foundation的用户界面。


图6-6中所示的THPreviewView类提供给用户一个摄像头当前拍摄内容的实时预览视图。我们将使用AVCaptureVideoPreviewLayer方法实现这个行为。首先通过认识THPreviewView的接口开始了解如何实现,如代码清单6-1所示。

代码清单6-1 THPreviewView 接口


#import <AVFoundation/AVFoundation.h>

@protocol THPreviewViewDelegate <NSObject>
- (void)tappedToFocusAtPoint:(CGPoint)point;
- (void)tappedToExposeAtPoint:(CGPoint)point;
- (void)tappedToResetFocusAndExposure;
@end

@interface THPreviewView : UIView

@property (strong, nonatomic) AVCaptureSession *session;
@property (weak, nonatomic) id<THPreviewViewDelegate> delegate;

@property (nonatomic) BOOL tapToFocusEnabled;
@property (nonatomic) BOOL tapToExposeEnabled;

@end

大部分属性和定义的方法都需要与多种点击手势一起使用。Kamera应用程序支持点击对焦和点击曝光功能。不过这个类定义的关键属性是session,用来关联AVCaptureVideo-PreviewLayer和激活的AVCaptureSession。让我们继续看这个类的具体实现。

代码清单6-2为这个类定义了一个缩写列表。这个项目版本的类中大部分代码都带有触控处理方法,不过这里我们不对触控动作进行讨论。下面让我们重点学习代码中有关AVFoundation的部分。

代码清单6-2 THPreviewView 的实现

#import "THPreviewView.h"

@implementation THPreviewView

+ (Class)layerClass {                                                       // 1
    return [AVCaptureVideoPreviewLayer class];
}

- (void)setSession:(AVCaptureSession *)session {                            // 2
    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}

- (AVCaptureSession*)session {
    return [(AVCaptureVideoPreviewLayer*)self.layer session];
}

- (CGPoint)captureDevicePointForPoint:(CGPoint)point {                      // 3
    AVCaptureVideoPreviewLayer *layer =
        (AVCaptureVideoPreviewLayer *)self.layer;
    return [layer captureDevicePointOfInterestForPoint:point];
}

(1) CALayer实例通常都会支持UIView。一般都是一个泛型CALayer实例,不过重写layerClass类方法可以让开发者在创建视图实例时自定义图层类型。可在UIView.上重写layerClass方法并返回AVCaptureVideoPreviewLayer类对象。

(2)重写session属性 的访问方法。在setSession:方法中 访问视图的layer属性,它是一个AVCaptureVideoPreviewLayer实例,并为它设置AVCaptureSession。这会将捕捉数据直接输出到图层中,并确保与会话状态同步。如果需要的话还要重写session方法来返回捕捉会话。

(3) captureDevicePointForPoint:方法是一个私有方法,用于支持该类定义的不同触控处理方法。该方法将屏幕坐标系上的触控点转换为摄像头坐标系上的点。

坐标空间转换

这里我们将代码清单6-2中的captureDevicePointForPoint:方法重点拿出来引起你对AVCaptureVideoPreviewLayer类定义的几个重要方法的注意。当使用AV Foundation的捕捉API时,一定要理解屏幕坐标系和捕捉设备坐标系的不同。

iPhone 5或iPhone 5s的屏幕坐标系左上角为(0, 0),垂直模式右下角坐标为(320, 568),水平模式右下角坐标为(568, 320)。作为一名iOS开发者一定要对这一坐标空间非常熟悉。捕捉设备坐标系有着不同的定义。它们通常是基于摄像头传感器的本地设置,水平方向不可旋转,并且左上角为(0, 0)右下角为(1, 1),如图6-7所示。


iOS 6之前的版本,要在这两个坐标空间间进行转换非常困难。要精确地将屏幕坐标点转换为摄像头坐标点(或者相反的变换),开发者必须考虑诸如视频重力、镜像、图层变换和方向等因素进行综合计算。幸运的是,AVCaptureVideoPreviewLayer现在定义了一个转换方法让这一过程变得简单多了。

AVCaptureVideoPreviewLayer定义了两个方法用于在两个坐标系间进行转换:

●captureDevicePointOfInterestForPoint: 获取屏幕坐标系的CGPoint数据,返回转换得到的设备坐标系CGPoint数据。
●pointForCaptureDevicePointOfInterest: 获取摄像头坐标系的CGPoint数据,返回转换得到的屏幕坐标系CGPoint数据。

THPreviewView使用captureDevicePointOfInterestForPoint:方法将用户触控点信息转换为摄像头设备坐标系中的点。在Kamera应用程序点击对焦和点击曝光功能的实现中会用到这个转换坐标点。

实现THPreviewView后,让我们继续讨论核心的捕捉代码。

6.3.2 创建捕捉控制器

捕捉会话的代码会包含在一个 名为THCameraController的类里面。这个类用于配置和管理不同的捕捉设备,同时也对捕捉的输出进行控制和交互。首先来看THCameraController类的接口,如代码清单6-3所示。

代码清单6-3 THCameraController 的接口

#import <AVFoundation/AVFoundation.h>

extern NSString *const THThumbnailCreatedNotification;

@protocol THCameraControllerDelegate <NSObject>                             // 1
- (void)deviceConfigurationFailedWithError:(NSError *)error;
- (void)mediaCaptureFailedWithError:(NSError *)error;
- (void)assetLibraryWriteFailedWithError:(NSError *)error;
@end

@interface THCameraController : NSObject

@property (weak, nonatomic) id<THCameraControllerDelegate> delegate;
@property (nonatomic, strong, readonly) AVCaptureSession *captureSession;

// Session Configuration                                                    // 2
- (BOOL)setupSession:(NSError **)error;
- (void)startSession;
- (void)stopSession;

// Camera Device Support                                                    // 3
- (BOOL)switchCameras;
- (BOOL)canSwitchCameras;
@property (nonatomic, readonly) NSUInteger cameraCount;
@property (nonatomic, readonly) BOOL cameraHasTorch;
@property (nonatomic, readonly) BOOL cameraHasFlash;
@property (nonatomic, readonly) BOOL cameraSupportsTapToFocus;
@property (nonatomic, readonly) BOOL cameraSupportsTapToExpose;
@property (nonatomic) AVCaptureTorchMode torchMode;
@property (nonatomic) AVCaptureFlashMode flashMode;

// Tap to * Methods                                                         // 4
- (void)focusAtPoint:(CGPoint)point;
- (void)exposeAtPoint:(CGPoint)point;
- (void)resetFocusAndExposureModes;

/** Media Capture Methods **/                                               // 5

// Still Image Capture
- (void)captureStillImage;

// Video Recording
- (void)startRecording;
- (void)stopRecording;
- (BOOL)isRecording;

@end

这次的接口是我们至今为止见过的代码最多的,所以让我们将它分解成几个小的部分来学习。
(1) THCameraControllerDelegate定 义了当有错误事件发生时需要在对象委托上调用的方法。
(2)这些方法都用于配置和控制捕捉会话。
(3)这些方法能够在不同摄像头中切换以测试摄像头的不同功能,保证用户可以在界面上进行正确选择。
(4)这些方法可实现点击对焦和点击曝光功能,允许开发者通过多点触控设置焦点和曝光参数。
(5)这些方法中还包括实现捕捉静态图片和视频的功能。

让我们转到实现文件开始学习具体的实现过程。

6.3.3 设置捕捉会话

首先实现THCameraController类,先从setupSession:方法开始, 如代码清单6 4所示。

代码清单6-4 setupSession:方法

#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import "NSFileManager+THAdditions.h"

@interface THCameraController ()

@property (strong, nonatomic) dispatch_queue_t videoQueue;
@property (strong, nonatomic) AVCaptureSession *captureSession;
@property (weak, nonatomic) AVCaptureDeviceInput *activeVideoInput;

@property (strong, nonatomic) AVCaptureStillImageOutput *imageOutput;
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieOutput;
@property (strong, nonatomic) NSURL *outputURL;

@end

@implementation THCameraController

- (BOOL)setupSession:(NSError **)error {

    self.captureSession = [[AVCaptureSession alloc] init];                  // 1
    self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;

    // Set up default camera device
    AVCaptureDevice *videoDevice =                                          // 2
        [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    AVCaptureDeviceInput *videoInput =                                      // 3
        [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
    if (videoInput) {
        if ([self.captureSession canAddInput:videoInput]) {                 // 4
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        }
    } else {
        return NO;
    }

    // Setup default microphone
    AVCaptureDevice *audioDevice =                                          // 5
        [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

    AVCaptureDeviceInput *audioInput =                                      // 6
        [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
    if (audioInput) {
        if ([self.captureSession canAddInput:audioInput]) {                 // 7
            [self.captureSession addInput:audioInput];
        }
    } else {
        return NO;
    }

    // Setup the still image output
    self.imageOutput = [[AVCaptureStillImageOutput alloc] init];            // 8
    self.imageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};

    if ([self.captureSession canAddOutput:self.imageOutput]) {
        [self.captureSession addOutput:self.imageOutput];
    }

    // Setup movie file output
    self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];             // 9

    if ([self.captureSession canAddOutput:self.movieOutput]) {
        [self.captureSession addOutput:self.movieOutput];
    }
    
    self.videoQueue = dispatch_queue_create("com.tapharmonic.VideoQeue", NULL);

    return YES;
}

@end

(1)创建的第一个对象就是会话本身。AVCaptureSession是捕捉场景各活动的中心枢纽,也是输入和输入数据需要添加的对象。一个捕捉会话可以配置会话预设值。本例中我们为其 设置AVCaptureSessionPresetHigh预设值,因为它是默认选择且满足Kamera的需求。其他会话预设值可在有关AVCaptureSession的文档中找到。
(2)得到一个指向默认视频捕捉设备的指针。在几乎所有iOS系统中,AVCaptureDevice都会返回手机后置摄像头。
(3)在将捕捉设备添加到AVCaptureSession前, 首先需要将它封装成-个AVCaptureDeviceInput对象。尤其重要的是开发者需要传递一个有效的指向这个方法的NSError指针,用于捕捉尝试创建输入数据时出现的任何错误信息。
(4)当返回一个有效AVCaptureDeviceInput时,首先希望调用会话的canAddInput:方法测试其是否可以被添加到会话中,如果可以,则调用addInput:方法将其添加到会话并给它传递捕捉设备的输入信息。
(5)与查找设备获得默认视频捕捉设备类似,选择默认的音频捕捉设备也是同样的,即返回一个指向内置麦克风的指针。
(6)为这个设备创建一个捕捉设备输入。同样,务必留意调用该方法时遇到的错误。
(7)测试设备是否可以被添加到会话,如果可以,将其添加到捕捉会话。
(8)创建一个AVCapturetillmageOutput实例。 它是AVCaptureOutput子类,用于从摄像头捕捉静态图片。可为对象的outputSettings属性配置一个字典来表示希望捕捉JPEG格式的图片。创建并配置完毕后,就可以调用addOutput:方法将它添加到捕捉会话。与刚才添加设备输入信息一样,开发者同样希望在此之前测试该输出是否可以被添加到捕捉会话。如果不经过测试盲目添加输出内容可能会抛出异常错误,导致应用程序崩溃。
(9)创建一个新的AVCaptureMovieFileOutput实例。 它是一个AVCaptureOutput子类,用于将QuickTime电影录制到文件系统。开发者也需要测试并将这个输出内容添加到会话。最后,返回YES,让调用函数知道会话配置已经完成了。

6.3.4 启动和停止会话

捕捉会话的对象图会通过调用setupSession:方法被妥善设置,不过在使用捕捉会话前,首先需要启动会话。启动会话第一步是启动数据流并使它处于准备捕捉图片和视频的状态,让我们看一下startSession方法和stopSession方法的实现,如代码清单6-5所示。

代码清单6-5启动和停止捕捉会话

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

- (void)stopSession {
    if ([self.captureSession isRunning]) {                                  // 2
        dispatch_async([self globalQueue], ^{
            [self.captureSession stopRunning];
        });
    }
}

(1)检查并确保捕捉会话没有处于准备运行状态。如果没有准备好,则调用捕捉会话的startRunning方法,这是一个同步调用并会消耗一定时间,所以你要以异步方式在videoQueue排队调用该方法,这样就不会阻碍主线程。
(2) stopSession的实现基本相同。在捕捉会话上调用stopRunning方法会停止系统中的数据流,这也是一一个同步调用,所以你要采用异步方式调用这个方法。

运行该应用程序,在启动时立即会出现一个或两个对话框,如图6-8所示。下面继续讨论为什么会显示这些对话框及如何对其进行处理。


6.3.5 处理隐私需求

在iOS 7和iOS 8版本中有关隐私保护的功能又得到了进一步的改进,让应用程序试图使用设备硬件的动作变得更加透明化。当应用程序试图访问麦克风和相机时就会弹出一个对话框询问用户是否授权。这一有关隐私保护的改进对于iOS平台非常受欢迎,只是我们在创建捕捉应用程序时可能要更加麻烦一点了。

注意:
iOS7版本中只有特地区有法律规定时才会询问用户是否可以访问设备的相机。从iOS8开始,所有地区的用户都要在应用程序中取得授权才可以访问相机。

这一提醒的触发 是通过创建AVCaptureDeviceInput实现的,当这些对话框出现时,系统并没有停止来等待用户响应,而是立即返回一个设备,比如调用音频设备则返回静音的设备,如果是相机则返回黑白帧。直到用户回答并同意了对话框中的内容,才会实际开始捕捉音频或视频内容。如果用户回答“不允许”,则在这个会话期间不会记录任何内容。如果用户不同意访问,“下 次应用程序启动时的AVCaptureDeviceInput创建情况就会返回nil,创建方法会收到NSError产生的错误代码AVErrorApplicationIsNotAuthorizedToUseDevice和失败原因,如下所示:

NSLocalizedFailureReason = This app is not authorized to use iPhone Microphone.

如果收到这个错误信息,唯一的办法就是弹出一个错误消息,如图6-9所示,告诉用户需要在“设置"应用程序中授权访问所需的硬件。


注意:
用户可随时在“设置”应用程序中修改隐私设置,所以在创建AVCaptureDeviceInput对象时一定要检查任何返回的错误信息。

会话配置完成后,让我们]继续开始学习Kamera应用程序具体功能的实现方法。

6.3.6 切换摄像头

基本上目前所有的iOS设备都具有前置和后置两个摄像头。Kamera应用程序会用到这两个摄像头,所以首先要开发的功能就是让用户能够在摄像头之间进行切换。让我们先从几个支撑方法开始学习,这些方法可以简化功能的实现,如代码清单6-6所示。

代码清单6-6摄像头的支撑方法

- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position { // 1
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device in devices) {
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}

- (AVCaptureDevice *)activeCamera {                                         // 2
    return self.activeVideoInput.device;
}

- (AVCaptureDevice *)inactiveCamera {                                       // 3
    AVCaptureDevice *device = nil;
    if (self.cameraCount > 1) {
        if ([self activeCamera].position == AVCaptureDevicePositionBack) {
            device = [self cameraWithPosition:AVCaptureDevicePositionFront];
        } else {
            device = [self cameraWithPosition:AVCaptureDevicePositionBack];
        }
    }
    return device;
}

- (BOOL)canSwitchCameras {                                                  // 4
    return self.cameraCount > 1;
}

- (NSUInteger)cameraCount {                                                 // 5
    return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}

(1) cameraWithPosition: 方法返回指定位置的AVCaptureDevice,有效的位置为AVCaptureDevicePositionFront或AVCaptureDevicePositionBack。遍历可用的视频设备并返回position参数对应的值。
(2)activeCamera方法返回当前捕捉会话对应的摄像头,返回激活的捕捉设备输入的device属性。
(3) inactiveCamera方法返回当前未激活的摄像头,通过查找当前激活摄像头的反向摄像头实现。如果应用程序运行的设备只有一个摄像头,则返回nil。
(4) canSwitchCameras方法返回一个BOOL值用于表示是否有超过一个摄像头可用。
(5)最后,cameraCount会 返回可用视频捕捉设备的数量。

实现了这些方法后,我们继续完成摄像头切换功能。切换前置和后置摄像头需要重新配置捕捉会话。幸运的是,可以动态重新配置AVCaptureSession,所以不必担心停止会话和重启会话带来的开销。不过我们对会话进行的任何改变,都要通过beginConfiguration和commitConfiguration方法进行单独的、原子性的变化。如代码清单6-7所示。

代码清单6-7 切换摄像头

- (BOOL)switchCameras {

    if (![self canSwitchCameras]) {                                         // 1
        return NO;
    }

    NSError *error;
    AVCaptureDevice *videoDevice = [self inactiveCamera];                   // 2

    AVCaptureDeviceInput *videoInput =
    [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];

    if (videoInput) {
        [self.captureSession beginConfiguration];                           // 3

        [self.captureSession removeInput:self.activeVideoInput];            // 4

        if ([self.captureSession canAddInput:videoInput]) {                 // 5
            [self.captureSession addInput:videoInput];
            self.activeVideoInput = videoInput;
        } else {
            [self.captureSession addInput:self.activeVideoInput];
        }

        [self.captureSession commitConfiguration];                          // 6

    } else {
        [self.delegate deviceConfigurationFailedWithError:error];           // 7
        return NO;
    }

    return YES;
}

(1)首先要确认是否可以切换摄像头。如果不可以,则返回NO,并退出方法。
(2)接下来,获取指向未激活摄像头的指针并为它创建一个 新的AVCaptureDeviceInput。
(3)在会话中调用beginConfiguration,并标注原子配置变化的开始。
(4)移除当前激活的AVCaptureDevicelnput。该当前视频捕捉设备输入信息必须在新的对象添加前移除。
(5)执行标准测试来检查是否可以添加新的AVCaptureDeviceInput,如果可以,将它添加到会话并设置为activeVideoInput。为确保安全,如果新的输入不能被添加,需要重新添加之前的输入。
(6)配置完成后,对AVCaptureSession调用commitConfiguration,会分批将所有变更整合在一起,得出一个有关会话的单独的、原子性的修改。
(7)当创建新的AVCaptureDeviceInput时 如果出现错误,需要通知委托来处理该错误。

再次运行该应用程序。假设iOS设备具有两个摄像头,点击屏幕右上角的摄像头图标就可以切换前置和后置摄像头了。

6.3.7 配置捕捉设备

AVCaptureDevice定义了很多方法让开发者控制iOS设备上的摄像头。尤其是可以独立调整和锁定摄像头的焦距、曝光和白平衡。对焦和曝光还可以基于特定的兴趣点进行设置,使其在应用程序中实现点击对焦和点击曝光的功能。AVCaptureDevice还可 以让你控制设备的LED作为拍照的闪光灯或手电筒使用。

每当修改摄像头设备时,一定 要先测试修改动作是否能够被设备支持。并不是所有摄像头(即使单摄像头的iOS设备)都能支持所有功能。比如,前置摄像头不支持对焦操作,因为它和目标之间不会超过一个臂长的距离;但是大部分的后置摄像头就可以支持全尺寸对焦。尝试应用一个不被支持的修改动作可能会带来异常,导致应用程序崩溃,所以在进行修改前对其进行测试非常有必要。比如,在将对焦模式设置为自动之前,首先就要检查这一模式是否被支持,如下面的代码所示:

AVCaptureDevice *device = // Active video capture device
if ([device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
    // Perform configuration
}

当验证这一个配置的修改可以支持时,就可以执行实际的设备配置了。修改捕捉设备的 基本技巧包括先锁定设备准备配置,执行所需的修改,最后解锁设备。比如,在确定摄像头支持自定对焦模式后,按如下方式配置focusMode属性:

AVCaptureDevice *device = // Active video capture device
if ([device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
    NSError *error;
    device.focusMode = AVCaptureFocusModeAutoFocus;
    [device unlockForConfiguration];
} else {
    // handle error
}

Mac、iPhone或iPad上的设备都是系统通用的,所以在进行设备修改前,AVCaptureDevice要求开发者获取设备上的一个排它锁,不这样做会导致应用程序抛出一个异常。 虽然不要求配置完成后立即释放这个排它锁,不过如果没有释放则会对其他使用同一资源的应用程序产生副作用,所以大多数时候我们都要做平台的好市民,每当配置完成后就释放这个排它锁。

学习了,上述知识后,让我们继续研究如何实现Kamera的各种设备配置功能。

6.3.8 调整焦距和曝光

iOS设备上的大多数后置摄像头都支持基于给定的兴趣点设置对焦和曝光数据。在界面上直观地利用这个功能是允许用户在摄像头界面点击一个位置,就会在这个点自动对焦或曝光。还可以对这些兴趣点锁定焦距和曝光,确保用户可以稳定地点击拍照按钮。我们首先实现点击对焦功能,如代码清单6-8所示。

代码清单6-8点击对焦方法的实现

- (BOOL)cameraSupportsTapToFocus {                                          // 1
    return [[self activeCamera] isFocusPointOfInterestSupported];
}

- (void)focusAtPoint:(CGPoint)point {                                       // 2

    AVCaptureDevice *device = [self activeCamera];

    if (device.isFocusPointOfInterestSupported &&                           // 3
        [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {

        NSError *error;
        if ([device lockForConfiguration:&error]) {                         // 4
            device.focusPointOfInterest = point;
            device.focusMode = AVCaptureFocusModeAutoFocus;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

(1)首先实现cameraSupportsTapToFocus方法。第一步询问激活中的摄像头是否支持兴趣点对焦。客户端利用这个方法按需更新用户界面。
(2)传递一个CGPoint结构给focusAtPoint:方法。传进来的点已经从屏幕坐标转换为捕捉设备坐标,这一转换是在之前实现的THPreviewView类中进行的。
(3)在得到激活摄像头设备的指针后,测试它是否支持兴趣点对焦并确认它是否支持自动对焦模式。这一模式会使用单独扫描的自动对焦,并将focusMode设置为AVCapture-FocusModeLocked。
(4)锁定设备准备配置。如果获得了锁,将focusPointOfInterest属性 设置为传进来的CGPoint值,设置对焦模式为AVCaptureFocusModeAutoFocus。最后调用AVCaptureDevice的unlockForConfiguration释放该锁定。

运行应用程序,找到一个你希望设置对焦和执行单击的特定对象。应用程序会在屏幕上显示一个蓝色的矩形区域,摄像头的焦点就在其中。现在焦点锁定了,移动摄像头找到一个新的兴趣点并点击确定新的对象。点击对焦功能完成后,让我们看点击曝光的实现。

大部分设备的默认曝光模式都是AVCaptureExposureModeContinuousAutoExposure,即根据场景的变化自动调整曝光度。我们可以用与点击对焦类似的方法实现“点击曝光并锁定”功能;不过这一过程有 点难度。虽然AVCaptureExposureMode枚举定 义了AVCaptureExposureModeAutoExpose值,不过当前的iOS设备都不支持这个值。这意味着开发者需要一点创造力以便采用点击对焦的同样方式实现这个功能。让我们看一下具体的实现过程,如代码清单6-9所示。

代码清单6-9 “点击曝光"方法

- (BOOL)cameraSupportsTapToExpose {                                         // 1
    return [[self activeCamera] isExposurePointOfInterestSupported];
}

// Define KVO context pointer for observing 'adjustingExposure" device property.
static const NSString *THCameraAdjustingExposureContext;

- (void)exposeAtPoint:(CGPoint)point {

    AVCaptureDevice *device = [self activeCamera];

    AVCaptureExposureMode exposureMode =
    AVCaptureExposureModeContinuousAutoExposure;

    if (device.isExposurePointOfInterestSupported &&                        // 2
        [device isExposureModeSupported:exposureMode]) {

        NSError *error;
        if ([device lockForConfiguration:&error]) {                         // 3

            device.exposurePointOfInterest = point;
            device.exposureMode = exposureMode;

            if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
                [device addObserver:self                                    // 4
                         forKeyPath:@"adjustingExposure"
                            options:NSKeyValueObservingOptionNew
                            context:&THCameraAdjustingExposureContext];
            }

            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {

    if (context == &THCameraAdjustingExposureContext) {                     // 5

        AVCaptureDevice *device = (AVCaptureDevice *)object;

        if (!device.isAdjustingExposure &&                                  // 6
            [device isExposureModeSupported:AVCaptureExposureModeLocked]) {

            [object removeObserver:self                                     // 7
                        forKeyPath:@"adjustingExposure"
                           context:&THCameraAdjustingExposureContext];

            dispatch_async(dispatch_get_main_queue(), ^{                    // 8
                NSError *error;
                if ([device lockForConfiguration:&error]) {
                    device.exposureMode = AVCaptureExposureModeLocked;
                    [device unlockForConfiguration];
                } else {
                    [self.delegate deviceConfigurationFailedWithError:error];
                }
            });
        }

    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

(1) cameraSupportsTapToExpose 方法的实现几乎与cameraSupportsTapToFocus的实现一样。即询问激活设备是否支持对一个兴趣点进行曝光。
(2)执行设备配置测试来确保需要的配置是被支持的。本例中需要验证是否支持AVCaptureExposureModeContinuousAutoExposure模式。
(3)锁定设备准备配置,设置exposurePointOfInterest和exposureMode属性为期望的值。
(4)判断设备是否支持锁定曝光模式。如果支持,使用KVO来确定设备adjustingExposure属性的状态。观察该属性可以知道曝光调整何时完成,让我们有机会在该点上锁定曝光。
(5)通过测试上下文参数是否为&THCameraAdjustingExposureContext指针来判断监听回调是否对应我们期望的变更操作。
(6)判断设备是否不再调整曝光等级,确认设备的exposureMode是否可以设置为AVCapture-ExposureModeLocked。如果可以,则继续下面的流程。
(7)移除作为adjustingExposure属性监听器的self,这样就不会得到后续变更的通知了。
(8)最后,以异步方式调度回主队列,定义-一个块来设置exposureMode属性为AVCapture-ExposureModeL ocked.将exposureMode更改转移到下一个事件循环运行非常重要,这样上一步中的removeObserver:调用才有机会完成。

的确,这比点击对焦的实现要复杂一些;不过你会发现最终效果是一样的。再次运行应用程序,在预览视图中找一些较暗的区域双击。会看到屏幕上出现一个橙色的矩形框,曝光会锁定这个区域。在明亮区域指向摄像头(电脑屏幕可以很好地工作),再次双击,会看到曝光调整为合理的程度。

添加了点击设置锁定焦点和曝光区域功能对Kamera应用程序来说增光不少。不过开发者还需要定义一种方法让用户可以切换回连续对焦和曝光模式。我们现在就来实现。代码清单6-10给出了resetFocusAndExposureModes方法的实现。

代码清单6-10重新设置对焦和曝光

- (void)resetFocusAndExposureModes {

    AVCaptureDevice *device = [self activeCamera];

    AVCaptureExposureMode exposureMode = AVCaptureExposureModeContinuousAutoExposure;

    AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;

    BOOL canResetFocus = [device isFocusPointOfInterestSupported] &&        // 1
    [device isFocusModeSupported:focusMode];

    BOOL canResetExposure = [device isExposurePointOfInterestSupported] &&  // 2
    [device isExposureModeSupported:exposureMode];

    CGPoint centerPoint = CGPointMake(0.5f, 0.5f);                          // 3

    NSError *error;
    if ([device lockForConfiguration:&error]) {

        if (canResetFocus) {                                                // 4
            device.focusMode = focusMode;
            device.focusPointOfInterest = centerPoint;
        }

        if (canResetExposure) {                                             // 5
            device.exposureMode = exposureMode;
            device.exposurePointOfInterest = centerPoint;
        }
        
        [device unlockForConfiguration];
        
    } else {
        [self.delegate deviceConfigurationFailedWithError:error];
    }
}

(1)首先硝人対焦光趣点和達縷自幼対焦模式是否被支持。
(2)丸行炎似的測式,硝保曝光度可以通辻相美的功能測試被重没。
(3)创建一个CGPoint, x和y的値均カ0.5f。回顾一下,捕捉没各空同的左上角カ(0, O),右下角匁(1, 1)。創建一个0.5, 0.5)的CGPoint会从摂像尖空同的中点幵始扣描。
(4)鎖定没各准各配畳,如果焦点可以重没,就迸行期望的修改。
(5)同祥,如果曝光度可以重没,就没畳カ期望的曝光模式。

运行应用程序,执行一些焦点和曝光的凋整。在预览视图中两个手指双击任何位畳,会发现対焦和曝光被重新设置为连续模式。

6.3.9 凋整内光灯和手电筒模式

AVCaptureDevice炎可以让开发者修改摄像头的内光灯和手电筒模式。设备后面的LED灯当拍摄静态图片时作为内光灯,而当拍摄视频时用作连续灯光(手电筒)。捕捉设备的fashMode和torchMode属性可以被设置为以下3个值中的一个:
AVCapture(TorchFlash)ModeOn:总是开启。
AVCapture(FlashlTorch)ModeOff: 总是关闭。
AVCapture(FlashlTorch)ModeAuto: 系统会基于周围环境光照情况自动关闭或打开LED。

代码清单6-11给出了内光灯和手电筒模式方法的实现。

代码清单6-11内光灯和手电筒方法

- (BOOL)cameraHasFlash {
    return [[self activeCamera] hasFlash];
}

- (AVCaptureFlashMode)flashMode {
    return [[self activeCamera] flashMode];
}

- (void)setFlashMode:(AVCaptureFlashMode)flashMode {

    AVCaptureDevice *device = [self activeCamera];

    if (device.flashMode != flashMode) {

        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.flashMode = flashMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

- (BOOL)cameraHasTorch {
    return [[self activeCamera] hasTorch];
}

- (AVCaptureTorchMode)torchMode {
    return [[self activeCamera] torchMode];
}

- (void)setTorchMode:(AVCaptureTorchMode)torchMode {

    AVCaptureDevice *device = [self activeCamera];

    if ([device isTorchModeSupported:torchMode]) {

        NSError *error;
        if ([device lockForConfiguration:&error]) {
            device.torchMode = torchMode;
            [device unlockForConfiguration];
        } else {
            [self.delegate deviceConfigurationFailedWithError:error];
        }
    }
}

我们这里不对上述代码逐步讲解了,因为这段代码现在看起来应该非常熟悉了。与之前一样,在更改配置前,开发者需要确认我们尝试修改的功能是否被支持。

Kamera应用程序的主视图控制器会正确调用setFlashMode:或setTorchMode:方法,这取决于用户界面上Video/Photo模式选择器的位置。运行应用程序,点击屏幕左上角的闪电图标可在不同模式之间切换。

6.3.10 拍摄静态图片

在setupSession:方法的实现过程中,我们将一个AVCapturetillmageOutput实例添加到捕捉会话。这个类是AVCaptureOutput的子类,用于捕捉静态图片。AVCapturetillmageOutput类定义了captureStillImageAsynchronouslyFromConnection:completionHandler:方法来执行实际的拍摄。让我们看下面这个简单示例:

AVCaptureConnection *connection = // Active video capture connection
id completionHandler = ^(CMSampleBufferRef buffer, NSError *error) {
    // Handle image capture
};
[imageOutput captureStillImageAsynchronouslyFromConnection:connection
                                         completionHandler:completionHandler];

这个方法包含了一些新的对象类型需要我们讨论;第一个就是AVCaptureConnection。当 创建一个会话并添加捕捉设备输入和捕捉输出时,会话自动建立输入和输出的连接,按需选择信号流线路。访问这些连接在一些情况下是非常实用的功能,因为可以让开发者更好地对发送到输出端的数据进行控制。另一个新的对象类型是CMSampleBuffer。CMSampleBuffer是由Core Media框架定义的一个Core Foundation对象。下一章会对这个对象类型进行详细讲述,不过现在我们只需要知道这个对象用来保存捕捉到的图片数据。由于我们在创建静态图片输出对象时指定了AVVideoCodecJPEG作为编解码的键,所以该对象包含的字节就会被压缩成JPEG格式。

让我们看一下Kamera应用程序拍摄静态图片时是如何使用这些对象的(如代码清单6-12所示)

代码清单6-12捕捉静态图片

- (void)captureStillImage {

    AVCaptureConnection *connection =                                       // 1
    [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];

    if (connection.isVideoOrientationSupported) {                           // 2
        connection.videoOrientation = [self currentVideoOrientation];
    }

    id handler = ^(CMSampleBufferRef sampleBuffer, NSError *error) {
        if (sampleBuffer != NULL) {

            NSData *imageData =                                             // 4
                [AVCaptureStillImageOutput
                    jpegStillImageNSDataRepresentation:sampleBuffer];

            UIImage *image = [[UIImage alloc] initWithData:imageData];      // 5
        } else {
            NSLog(@"NULL sampleBuffer: %@", [error localizedDescription]);
        }
    };
    // Capture still image                                                  // 6
    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection
                                                  completionHandler:handler];
}

- (AVCaptureVideoOrientation)currentVideoOrientation {

    AVCaptureVideoOrientation orientation;

    switch ([UIDevice currentDevice].orientation) {                         // 3
        case UIDeviceOrientationPortrait:
            orientation = AVCaptureVideoOrientationPortrait;
            break;
        case UIDeviceOrientationLandscapeRight:
            orientation = AVCaptureVideoOrientationLandscapeLeft;
            break;
        case UIDeviceOrientationPortraitUpsideDown:
            orientation = AVCaptureVideoOrientationPortraitUpsideDown;
            break;
        default:
            orientation = AVCaptureVideoOrientationLandscapeRight;
            break;
    }

    return orientation;
}

(1)首先通过调用connectionWithMediaType:方法获取AVCapturetillmageOutput对象使用的当前AVCaptureConnection的指针。当查找AVCapturetillmageOutput连接时一般会传递AVMediaTypeVideo媒体类型。
(2) Kamera应用程序本身只支持垂直方向,所以当旋转设备时,用户界面保持不变。不过我们希望满足横向使用手机的用户需求,所以需要相应地调整结果图片的方向。要确定连接是否支持设置视频方向;如果是,则设置其为currentVideoOrientation方法返回的AVCaptureVideoOrientation.
(3)取UIDevice的orientation,切换该值确定相应的AVCaptureVideoOrientation。重要的一点是注意左侧和右侧的AVCaptureVideoOrientation值是和它们的UIDeviceOrientation值相反的。
(4)在completion handler块中,如果接收一个有效的CMSampleBuffer,则调用AVCapture-StillmageOutput类的jpegSillmageNSDataRepresentation方法,会返回一个表示图片字节的NSData。
(5)用NSData创建一个 新的Ullmage实例。

现在我们已经成功捕捉到一个图片并创建了相应的UlIlmage。可以把它呈现在用户界面的相应位置或写入应用程序沙盒的指定位置。不过大部分用户都希望将通过Kamera应用程序拍摄的图片写入Photos应用程序中的Camera Roll中。要添加这个功能,需要用到Assets Library框架。

6.3.11 使用 Assets Library框架

Assets Library框架可以让开发者通过编程方式访问iOS Photos应用程序所管理的用户相册和视频库。该框架在许多AV Foundation应用程序中都是默认添加的,所以掌握它的用法非常重要。

这个框架的核心类是ALAssetsLibrary。ALAssetsLibrary类的实例定 义了与用户库进行交互的接口。该对象具有多个“写入”方法,可以让开发者这将照片或视频写入到自己的库中。应用程序第一次试图 与库进行交互时,会弹出一个如图6-10所示的对话框。


image.png

在实际使用前,用户必须明确允许应用程序访问该库。如果用户不同意访问,则任何向库中的写入操作都会失败,并返回一个表示用户拒绝访问的错误信息。如果访问用户库是应用程序的核心功能,开发者首先就需要确定应用程序的授权状态,如下面的示例所示。

ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
if (status == ALAuthorizationStatusDenied) {
    // Show prompt indicating the application won't function
    // correctly without access to the library
} else {
    // Perform authorized access to the library
}

让我们看一下如何将捕捉到的图片写入Assets Library,如代码清单6-13所示。

代码清单6-13 捕捉静态图片

- (void)captureStillImage {

    AVCaptureConnection *connection =
        [self.imageOutput connectionWithMediaType:AVMediaTypeVideo];

    if (connection.isVideoOrientationSupported) {
        connection.videoOrientation = [self currentVideoOrientation];
    }

    id handler = ^(CMSampleBufferRef sampleBuffer, NSError *error) {
        if (sampleBuffer != NULL) {

            NSData *imageData =
                [AVCaptureStillImageOutput
                    jpegStillImageNSDataRepresentation:sampleBuffer];

            UIImage *image = [[UIImage alloc] initWithData:imageData];
            [self writeImageToAssetsLibrary:image];                         // 1

        } else {
            NSLog(@"NULL sampleBuffer: %@", [error localizedDescription]);
        }
    };
    // Capture still image
    [self.imageOutput captureStillImageAsynchronouslyFromConnection:connection
                                                  completionHandler:handler];
}

- (void)writeImageToAssetsLibrary:(UIImage *)image {

    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];              // 2

    [library writeImageToSavedPhotosAlbum:image.CGImage                     // 3
                              orientation:(NSInteger)image.imageOrientation // 4
                          completionBlock:^(NSURL *assetURL, NSError *error) {
                              if (!error) {
                                  [self postThumbnailNotifification:image]; // 5
                              } else {
                                  id message = [error localizedDescription];
                                  NSLog(@"Error: %@", message);
                              }
                          }];
}

- (void)postThumbnailNotifification:(UIImage *)image {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
        [nc postNotificationName:THThumbnailCreatedNotification object:image];
    });
}

(1)在捕捉completion handler中,调用新方法writeImage' ToAssetsLibrary:,并将用图片数据创建的Ullmage对象传递给它。
(2)创建一个新的ALAssctsLibrary实例,用于通过编程方式向用户的Camera Roll写入。
(3)调用库的writeImage ToSavedPhotosAlbum:orientation:completion-Block:方法。图片参数一定是CGImageRef,所以我们需要获取Ullmage的CGlmage表示。
(4)方向参数是一个ALAssetOrientation枚举值。 这些值直接对应于由图片的imageOrientation返回的UIlmageOrientation值,所以将这个值转换为一个NSInterger。
(5)如果写入成功,发送一个带有捕捉图片的通知。用于绘制Kamera应用程序用户界面中的缩略图。

运行应用程序,切换为Photos模式,拍摄几张图片。第一次拍照时会弹出一个提示框询问是否授权访问“照片库”,本例中一定要回答“同意”。在应用程序中点击拍照按钮左边的图片图标会得到一个用户Camera Roll的简单视图;不过要查看图片细节,还需要转到Photos应用程序。

拍摄静态图片的功能已经完成,让我们继续学习有关视频捕捉的功能。

6.3.12 视频捕捉

结束本章学习之前还有最后一件需要讨论的事,即视频内容的捕捉。当设置捕捉会话时,添加一个名为AVCaptureMovieFileOutput的输出。这个类定义了一个简单实用的方法将QuickTime影片捕捉到磁盘。这个类的大多数核心功能继承于超类AVCaptureFileOutput。这个抽象超类定义了许多实用功能,比如录制到最长时限或录制到特定文件大小时为止。还可以配置成保留最小可用磁盘空间,这一点在存储空间有限的移动设备上录制视频时非常重要。

通常当QuickTime影片准备发布时,影片头的元数据处于文件的开始位置。这样可以让视频播放器快速读取头包含的信息,来确定文件的内容、结构和其包含的多个样本的位置。不过当录制一个QuickTime影片时, 直到所有的样本都完成捕捉后才能创建信息头。当录制结束时,创建头数据并将它附在文件结尾处,如图6-11所示。


将创建头的过程放在所有影片样本完成捕捉之后存在一个问题,尤其是在移动设备的情况下。如果遇到崩溃或其他中断,比如有电话拨入,则影片头就不会被正确写入,会在磁盘生成一个不可读的影片文件。AVCaptureMoviceFileOutput提供的 一个核心功能就是分段捕捉QuickTime影片,如图6-12所示。

当录制开始时,在文件最前面会写入一个最小化的头信息,随着录制的进行,片段按照一定的周期写入,创建完整的头信息。默认状态下,每10秒写入一个片段,不过这个时间间隔可以通过修改捕捉输出的movieFragmentInterval属性来改变。写入片段的方式可以逐步创建完整的QuickTime影片头,这样就确保了当遇到应用程序崩溃或中断时,影片仍然会以最后一个写入的片段为终点进行保存。默认的片段间隔足以满足Kamera应用程序的要求,不过可以在你自己的应用程序中修改这个值。

让我们从用于启动和停止录制的传输方法开始实现视频录制功能,如代码清单6-14所示。

代码清单6-14视频录制的传输方法

- (BOOL)isRecording {                                                       // 1
    return self.movieOutput.isRecording;
}

- (void)startRecording {

    if (![self isRecording]) {

        AVCaptureConnection *videoConnection =                              // 2
            [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];

        if ([videoConnection isVideoOrientationSupported]) {                // 3
            videoConnection.videoOrientation = self.currentVideoOrientation;
        }

        if ([videoConnection isVideoStabilizationSupported]) {              // 4
            
            videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
            
            // Deprecated approach below
            // videoConnection.enablesVideoStabilizationWhenAvailable = YES;
        }

        AVCaptureDevice *device = [self activeCamera];

        if (device.isSmoothAutoFocusSupported) {                            // 5
            NSError *error;
            if ([device lockForConfiguration:&error]) {
                device.smoothAutoFocusEnabled = NO;
                [device unlockForConfiguration];
            } else {
                [self.delegate deviceConfigurationFailedWithError:error];
            }
        }

        self.outputURL = [self uniqueURL];                                  // 6
        [self.movieOutput startRecordingToOutputFileURL:self.outputURL      // 8
                                      recordingDelegate:self];

    }
}

- (NSURL *)uniqueURL {                                                      // 7

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *dirPath =
        [fileManager temporaryDirectoryWithTemplateString:@"kamera.XXXXXX"];

    if (dirPath) {
        NSString *filePath =
            [dirPath stringByAppendingPathComponent:@"kamera_movie.mov"];
        return [NSURL fileURLWithPath:filePath];
    }

    return nil;
}

- (void)stopRecording {                                                     // 9
    if ([self isRecording]) {
        [self.movieOutput stopRecording];
    }
}

(1)此处提供了一个isRecording方法用于获取AVCaptureMovieFileOutput实例的状态。这是一个支撑方法,主要为外部客户端提供控制器的当前状态。
(2)在startRecording方法中,获取处理当前视频捕捉连接的信息。用于对捕捉的视频数据配置一些核心属性。
(3)判断是否支持设置videoOrientation属性;如果支持,将其设置为当前视频方向。设置视频的方向不会物理上旋转像素缓存,不过会应用QuickTime文件相应的矩阵变化。
(4)如果可以设置enablesVideoStabilizationWhenAvailable,则设置它为YES。并不是所有摄像头和设备都支持该功能,所以需要对其进行测试。支持视频稳定可以显著提升捕捉到的视频质量,尤其是在iPhone这样的设备上。其中一点需要重点关注的是视频稳定只在录制视频文件时才会涉及。这一稳定效果也不会在视频预览屏幕中看到。
(5)摄像头可以进行平滑对焦模式的操作,即减慢摄像头镜头对焦的速度。通常情况下,当用户移动拍摄时摄像头会尝试快速自动对焦,这会在捕捉的视频中出现脉冲式效果。当平滑对焦时,会降低对焦操作的速率,从而提供更加自然的视频录制效果。
(6)查找写入捕捉视频的唯一文件系统URL。开发者需要保持对地址的强引用,因为这个地址在后面处理视频时会用到。
(7)唯一URL查找代码看起来眼熟,因为我们之前就使用过这个方法。这个方法用到了一个我们添加到NSFileManager的分类方法,叫termporaryDirectoryWithTemplateString. 它可以为文件将要写入的目的地创建一个唯一命名的目录。
(8)最后在捕捉输出上调用startRecordingToOutputFileURL:recordingDelegate:方法,传递outputURL并将self设置为委托。这样就实际开始录制了。
(9)添加一个方法用于停止录制,可以对捕捉输出调用stopRecording方法。

如果现在编译项目,会注意到编辑器有警告,意思是我们指定的录制委托self没有遵循AVCaptureFileOutputRecordingDelegate协议。我们需要修改一下 控制器类的扩展,如代码清单6-15所示。

代码清单6-15遵循AVCaptureFileOutputRecordingDelegate协议

@interface THCameraController () <AVCaptureFileOutputRecordingDelegate>

@property (strong, nonatomic) AVCaptureSession *captureSession;
@property (weak, nonatomic) AVCaptureDeviceInput *activeVideoInput;

@property (strong, nonatomic) AVCaptureStillImageOutput *imageOutput;
@property (strong, nonatomic) AVCaptureMovieFileOutput *movieOutput;
@property (strong, nonatomic) NSURL *outputURL;

@end
遵循这些协议意味着还需要实现其中一个必要的方法。该方法用于获取最终文件并将它写入到Camera Roll中。代码清单6- 16给出了这些方法的实现。

代码清单6-16写 入捕捉到的视频

- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
      fromConnections:(NSArray *)connections
                error:(NSError *)error {
    if (error) {                                                            // 1
        [self.delegate mediaCaptureFailedWithError:error];
    } else {
        [self writeVideoToAssetsLibrary:[self.outputURL copy]];
    }
    self.outputURL = nil;
}

- (void)writeVideoToAssetsLibrary:(NSURL *)videoURL {

    ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];              // 2

    if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoURL]) {   // 3

        ALAssetsLibraryWriteVideoCompletionBlock completionBlock;

        completionBlock = ^(NSURL *assetURL, NSError *error){               // 4
            if (error) {
                [self.delegate assetLibraryWriteFailedWithError:error];
            } else {
                [self generateThumbnailForVideoAtURL:videoURL];
            }
        };

        [library writeVideoAtPathToSavedPhotosAlbum:videoURL                // 8
                                    completionBlock:completionBlock];
    }
}

- (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL {

    dispatch_async([self globalQueue], ^{

        AVAsset *asset = [AVAsset assetWithURL:videoURL];

        AVAssetImageGenerator *imageGenerator =                             // 5
            [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
        imageGenerator.maximumSize = CGSizeMake(100.0f, 0.0f);
        imageGenerator.appliesPreferredTrackTransform = YES;

        CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero // 6
                                                     actualTime:NULL
                                                          error:nil];
        UIImage *image = [UIImage imageWithCGImage:imageRef];
        CGImageRelease(imageRef);

        dispatch_async(dispatch_get_main_queue(), ^{                        // 7
            [self postThumbnailNotifification:image];
        });
    });
}

(1)在委托回调中,如果返回错误信息则只需通知委托,让委托对错误进行处理。如果没有遇到错误,则尝试将视频写入用户的Camera Roll,这通过调用writeVideoToAssetsLibrary:方法来实现。
(2)创建一个ALAssetsLibrary实例,它会提供写入视频的接口。
(3)向资源库写入前,应调用videoAtPathIsCompatibleWithSavedPhotosAlbum:方法检查视频是否可被写入。本例中这个方法会返回YES,不过开发者要养成在写入资源库时调用该方法的习惯。
(4)创建一个completion handler块,当向资源库的写入完成时调用该块。如果操作中出现错误,调用委托来通知错误信息;否则就是写入操作成功,所以接下来生成用于界面展示的视频缩略图。
(5)在videoQueue上,为新建的视频建立一“个新的AVAsset和AVAssetlmageGenerator。设置maximumSize属性为宽100和高0,根据视频的宽高比来设置图片高度。还需要设置appliesPreferredTrackTransform为YES,这样当捕捉缩略图时会考虑视频的变化(如视频方向的变化)。如果不这样设置,则会出现错误的缩略图方向。
(6)因为只需要捕捉一张图片, 所以可以使用copyCGiImageAtTime:actualTime:error:方法。这是一个同步方法,这就是为什么我们将这个操作移到主线程之外。根据返回的CGImageRef数据,创建一个用户界面中展示用的UIlmage。当调用copyCGImageAtTime:actualTime:error:方法时,开发者需要负责释放创建的图片,所以需要调用CGImageRelease(imageRef)方法避免出现内存泄漏。
(7)调度回主线程,并发送一个通知传递最新创建的UlImage。
(8)执行实际写入资源库的动作,传递videoURL和completionBlock。

运行应用程序,录制一段几秒钟的视频。点击Stop会 生成一个为刚才拍摄的场景创建的新缩略图。可以点击这个缩略图打开浏览框并查看最新创建的视频。

6.4 小结

现在你应该已经很好地掌握了核心AV Foundation捕 捉API的知识。我们学习如何配置和控制一个AVCaptureSession,通过一些示例学习了如何直接控制和操作捕捉设备,还学习了如何利用AVCaptureOutput的子类来捕捉静态图片和视频。使用这些核心功能,创建了一个与苹果内置的“相机”应用程序的功能类似的一一个 应用程序。虽然本章的讲解都是针对iOS平台的,不过我们用到方法和技术在Mac平台创建摄像头和视频应用程序时同样适用。我们已经对AVFoundation捕捉API的学习有了一一个好的开端,在下一章中我们会继续学习一些更高级的捕捉特性,将摄像头和视频应用程序提升到一个新高度。

上一篇下一篇

猜你喜欢

热点阅读