AVFoundation-拍照和录像

使用AV Foundation 捕捉照片和视频

使用AVFoundation拍照和录制视频的一般步骤如下:

  1. 创建AVCaptureSession对象。
  2. 使用AVCaptureDevice的静态方法获得需要使用的设备,例如拍照和录像就需要获得摄像头设备,录音就要获得麦克风设备。
  3. 利用输入设备AVCaptureDevice初始化AVCaptureDeviceInput对象。
  4. 初始化输出数据管理对象,如果要拍照就初始化AVCaptureStillImageOutput对象;如果拍摄视频就初始化AVCaptureMovieFileOutput对象。
  5. 将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中。
  6. 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器中,调用AVCaptureSession的startRuning方法开始捕获。
  7. 将捕获的音频或视频数据输出到指定文件。

主要用到的类:

  • AVCaptureDeviceInput (输入设备)
    • AVCaptureDevice
  • AVCaptureSession (捕捉会话)
  • AVCaptureVideoPreviewLayer (展示层)
  • AVCaptureOutput (输出)
    • AVCaptureStillImageOutput (捕捉图片)
    • AVCaptureAudioDataOutput
    • AVCaptureVideoDataOutput
    • AVCaptureFileOutput

配置会话:

简单配置会话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//初始化会话
_captureSession=[[AVCaptureSession alloc]init];
if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {//设置分辨率
_captureSession.sessionPreset=AVCaptureSessionPreset1280x720;
//_captureSession.sessionPreset=AVCaptureSessionPresetHigh;
}
//获得视频捕捉设备
AVCaptureDevice *captureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得后置摄像头
// AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];//获取默认摄像头
if (!captureDevice) {
NSLog(@"取得后置摄像头时出现问题.");
return;
}

NSError *error=nil;
//根据输入设备初始化设备输入对象,用于获得输入数据
_captureDeviceInput=[[AVCaptureDeviceInput alloc]initWithDevice:captureDevice error:&error];
if (error) {
NSLog(@"取得设备输入对象时出错,错误原因:%@",error.localizedDescription);
return;
}



//获取静态图片输出
_captureStillImageOutput=[[AVCaptureStillImageOutput alloc]init];
NSDictionary *outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
[_captureStillImageOutput setOutputSettings:outputSettings];//输出设置

//将设备输入添加到会话中
if ([_captureSession canAddInput:_captureDeviceInput]) {
[_captureSession addInput:_captureDeviceInput];
}

//将设备输出添加到会话中
if ([_captureSession canAddOutput:_captureStillImageOutput]) {
[_captureSession addOutput:_captureStillImageOutput];
}

//创建视频预览层,用于实时展示摄像头状态
_captureVideoPreviewLayer=[[AVCaptureVideoPreviewLayer alloc]initWithSession:self.captureSession];

CALayer *layer=self.viewContainer.layer;
layer.masksToBounds=YES;

_captureVideoPreviewLayer.frame=layer.bounds;
_captureVideoPreviewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;//填充模式
//将视频预览层添加到界面中
\[layer addSublayer:_captureVideoPreviewLayer];

添加音频捕捉设备(麦克风)

输入不仅可以添加摄像头 ,页可以添加麦克风

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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;
}

添加视频输出

输出不仅可以添加静态图片,可以添加视频输出

1
2
3
4
5
6
// Setup movie file output
self.movieOutput = [[AVCaptureMovieFileOutput alloc] init]; // 9

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

启动或者停止会话

启动会话,使它处于图片和视频捕捉状态。最好不要在主线程中启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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];
});
}
}

隐私访问权限

xcode8 iOS10中后不添加权限会出错

在info.plist文件添加对应的Key-Value:

聚焦(自动聚焦,手动聚焦)

坐标空间转换

如iPhone5屏幕坐标系左上角为(0,0)垂直时右下角为(320,568),水平时右下角(568,320)。
而设备坐标系基于摄像头的本地设置,左上角为(0,0)右下角为(1,1)。

AVCaptureVideoPreviewLayer定义了两个方法用于转换两者间的坐标:

  • - (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)pointInLayer NS_AVAILABLE_IOS(6_0);
    • 将屏幕坐标系转换为设备坐标系
  • - (CGPoint)pointForCaptureDevicePointOfInterest:(CGPoint)captureDevicePointOfInterest NS_AVAILABLE_IOS(6_0);
    • 将设备坐标系转换为屏幕坐标系

设置对焦模式

对焦模式AVCaptureFlashMode有 三种 AVCaptureFlashModeOffAVCaptureFlashModeOnAVCaptureFlashModeAuto
配置设备时一定要判断一下设备是否支持,比如前置摄像头不支持对焦操作
修改设备配置时需要先锁定配置 lockForConfiguration 执行所需的修改,最后解锁设备 unlockForConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {

AVCaptureDevice *device = [self activeCamera];

if (device.flashMode != flashMode &&
[device isFlashModeSupported:flashMode]) {

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

点击对焦

将屏幕坐标转换为捕捉设备坐标,然后将 focusPointOfInterest 设置为该坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma mark - Focus Methods

- (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];
}
}
}

调整曝光模式

与调整对焦模式类似,可以调整曝光模式 device.exposureMode
AVCaptureExposureModeLocked 锁定曝光 AVCaptureExposureModeAutoExpose 自动曝光 AVCaptureExposureModeContinuousAutoExposure 自动持续曝光 AVCaptureExposureModeCustom 自定义曝光 四个值

调整闪光灯模式

与之前的两个类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)cameraHasTorch {
return [[self activeCamera] hasTorch];
}

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

- (void)setTorchMode:(AVCaptureTorchMode)torchMode {

AVCaptureDevice *device = [self activeCamera];

if (device.torchMode != torchMode &&
[device isTorchModeSupported:torchMode]) {

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

切换摄像头

切换摄像头时担心停止会话和重启会话带来的开销,但是对会话进行改变时,要通过 beginConfigurationcommitConfiguration 进行单独的原子性的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#pragma mark - Device Configuration

//返回指定位置的AVCaptureDevice **AVCaptureDevicePositionBack** 和 **AVCaptureDevicePositionFront**
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position { // 1
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) { // 2
if (device.position == position) {
return device;
}
}
return nil;
}
//当前捕捉会话对应的摄像头
- (AVCaptureDevice *)activeCamera { // 3
return self.activeVideoInput.device;
}
//当前为激活的摄像头
- (AVCaptureDevice *)inactiveCamera { // 4
AVCaptureDevice *device = nil;
if (self.cameraCount > 1) {
if ([self activeCamera].position == AVCaptureDevicePositionBack) { // 5
device = [self cameraWithPosition:AVCaptureDevicePositionFront];
} else {
device = [self cameraWithPosition:AVCaptureDevicePositionBack];
}
}
return device;
}
//是否超过一个摄像头可用
- (BOOL)canSwitchCameras { // 6
return self.cameraCount > 1;
}
//可用的视频捕捉设备的数量
- (NSUInteger)cameraCount { // 7
return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}

- (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;
}

拍照,获取静态图片

AVCaptureStillImageOutput 用于捕捉静态图片,执行- (void)captureStillImageAsynchronouslyFromConnection:(AVCaptureConnection *)connection completionHandler:(void (^)(CMSampleBufferRef imageDataSampleBuffer, NSError *error))handler;方法来获取图片
使用 ALAssetsLibrary来将图片储存在照片中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//拍照
- (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];
});
}

视频捕捉

使用AVCaptureMovieFileOutput 来捕捉视频
在录制开始时,在文件的最前面会写入一个最小化的头信息,随着录制的进行,片段按照一定周期写入,创建完整的头信息,这样就确保了当程序遇到崩溃或者中断时,影片仍然会以最后一个写入的片段为重点进行保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#pragma mark - Video Capture Methods

- (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

if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.0) {
videoConnection.enablesVideoStabilizationWhenAvailable = YES;
} else {
videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
}

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];

}
}

- (CMTime)recordedDuration {
return self.movieOutput.recordedDuration;
}
//视频文件写入目录
- (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];
}
}

#pragma mark - AVCaptureFileOutputRecordingDelegate
//在AVCaptureFileOutputRecordingDelegate 实现中获取最终文件并写入
- (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];
});
});
}

#pragma mark - Recoding Destination URL

- (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;
}