iOS 使用相机之 AVFoundation

上篇文章介绍了如何使用 UIImagePickerController 调用相机拍照或者录制视频,但是 UIImagePickerController 只提供了一种非常简单的拍照和录制视频的方法,并且只提供了很简单的UI定制,如果你想要更多关于处理相机拍摄照片或者视频的方法、或者需要完整的自定义相机界面等,那就需要使用 AVFoundation 框架

AVFoundation 相关类

AVFoundation 框架基于以下几个类实现图像捕捉,通过这些类可以访问来自相机设备的原始数据

  • AVCaptureDevice 抽象化的硬件(如前置摄像头,后置摄像头等)对象,它用于控制硬件的相关特性(闪光灯,手电筒,聚焦模式等)
  • AVCaptureDeviceInput 硬件设备输入流数据的管理对象
  • AVCaptureOutput 一个抽象类,是输出流数据的管理对象,通常使用它的具体子类:
    • AVCaptureStillImageOutput 静态图片
    • AVCaptureAudioDataOutput 音频数据
    • AVCaptureVideoDataOutput 视频数据,实时的为预览图提供原始帧
    • AVCaptureMetadataOutput 元数据,可用于检测人脸识别、二维码等
    • AVCaptureMovieFileOutput 完整的视频数据地址
    • AVCapturePhotoOutput 照片(包括静态的、Live Photo 等),iOS10 新增,用于代替 AVCaptureStillImageOutput
  • AVCaptureSession 管理输入与输出之间的数据流的会话对象
  • AVCaptureVideoPreviewLayer 是 CALayer 的子类,用于显示相机产生的实时图像

他们之间的关系如图:

AVFoundation 的使用

使用 AVFoundation 自定义相机需要进行如下几个步骤:

  • 创建捕捉会话(captureSession
  • 选择并添加输入(相机、麦克风等)
  • 创建并设置输出
  • 通过添加输入、输出配置捕捉会话
  • 创建和添加预览视图
  • 启动捕捉会话

下面看具体步骤介绍

AVCaptureSession

AVCaptureSession 负责调度影音输入与输出之间的数据流

1
2
3
captureSession = AVCaptureSession()
// CaptureSession 的会话预设,这个地方设置的模式/分辨率大小将影响你后面拍摄照片/视频的大小
captureSession?.sessionPreset = AVCaptureSessionPresetPhoto

在创建 AVCaptureSession 的时候,我们一般会设定一个 sessionPreset 预设,设置于是可以让用户获取不同规格的数据,一共有如下12种不同的预设:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let AVCaptureSessionPresetPhoto: String
let AVCaptureSessionPresetHigh: String
let AVCaptureSessionPresetMedium: String
let AVCaptureSessionPresetLow: String
let AVCaptureSessionPreset352x288: String
let AVCaptureSessionPreset640x480: String
let AVCaptureSessionPreset1280x720: String
let AVCaptureSessionPreset1920x1080: String
let AVCaptureSessionPreset3840x2160: String
let AVCaptureSessionPresetiFrame960x540: String
let AVCaptureSessionPresetiFrame1280x720: String
let AVCaptureSessionPresetInputPriority: String

AVCaptureSessionPresetPhoto 用于获取静态照片和 LivePhoto,它会为我们选择最合适的照片配置,如果需要设置更多细节(静态图片的分辨率、感光度、曝光时间灯)可以从 AVCaptureDevice.formats 获取到设备设置支持的参数,并且可以自定义参数赋值给 AVCaptureDevice.activeFormat 属性,接下来的10个预设和上一篇博客中 UIImagePickerController 设置的 videoQualityUIImagePickerControllerQualityType 类似,都是设置视频相关的预设,最后一个 AVCaptureSessionPresetInputPriority 表示 captureSession 不去控制音频与视频输出设置,而是通过已连接的捕获设备的 activeFormat 反过来控制 captureSession 的输出质量等级

注意:所有对 AVCaptureSession 的调用都是线程阻塞的,所以我们最好创建一条线程异步调用

AVCaptureDevice 和 AVCaptureDeviceInput

AVCaptureDevice 表示抽象化的 iPhone 输入硬件,包括相机、麦克风等,AVCaptureDeviceInput 是通过 AVCaptureDevice 创建的输入源对象,并且需要添加到 captureSession

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
func cameraWithPosition(_ position: AVCaptureDevicePosition) -> AVCaptureDevice? {
if #available(iOS 10.0, *) {
let devices = AVCaptureDeviceDiscoverySession(deviceTypes: [AVCaptureDeviceType.builtInWideAngleCamera], mediaType: AVMediaTypeVideo, position: position).devices!
for device in devices {
if device.position == position {
return device
}
}
} else {
let devices = AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) as! [AVCaptureDevice]
for device in devices {
if device.position == position {
return device
}
}
}
return nil
}

上面定义的方法是通过传入一个设备的 AVCaptureDevicePosition 以及 mediaType 获取前置或者后置相机的 AVCaptureDevice 对象,如果 mediaTypeAVMediaTypeAudio 可以获取到麦克风的 AVCaptureDevice 对象(iPhone 内部前后有多个麦克风),iOS 10 之后苹果提供了一个专门的 AVCaptureDeviceDiscoverySession 类来获取 AVCaptureDevice 设备,iPhone 7 的双摄像头设备必须使用这个类才能获取到

接下来是是把设备添加到 AVCaptureDeviceInput 对象中去,再把输入对象添加到 captureSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 添加输入
do {
// 初始使用后置相机
let cameraDeviceInput = try AVCaptureDeviceInput(device: self.cameraWithPosition(.back))
if (captureSession?.canAddInput(cameraDeviceInput))! {
captureSession?.addInput(cameraDeviceInput)
self.captureInput = cameraDeviceInput
}
} catch let error as NSError {
print(error.localizedDescription)
}

注意当 App 第一次运行时,第一次调用 AVCaptureDeviceInput.init(device: AVCaptureDevice!) 会触发系统提示向用户请求允许访问设备,所以你的 App需要访问相机、麦克风或者相册等的时候,所以记得在info.plist添加相应配置权限

如果希望体验更好,我们可以先确认设备当前的授权状态,要是在授权还没有确定的情况下 (也就是说用户还没有看过弹出的授权对话框时),我们应该明确地发起授权请求,如果用户拒绝过授权请求也可以提示用户

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
func checkAuthorization() {
/**
AVAuthorizationStatusNotDetermined // 未进行授权选择
AVAuthorizationStatusRestricted // 未授权,且用户无法更新,如家长控制情况下
AVAuthorizationStatusDenied // 用户拒绝App使用
AVAuthorizationStatusAuthorized // 已授权,可使用
*/
// 传AVMediaTypeAudio 可以获取麦克风的授权状态
switch AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) {
case .authorized: // 已授权,可使用
// ... 开始设置AVCaptureSession
case .notDetermined://进行授权选择
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { (granted) in
if granted {//授权成功
// ... 开始设置AVCaptureSession
}else {
let alert = UIAlertController(title: "提示", message: "用户拒绝授权使用相机", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "确定", style: .default, handler: nil)
alert.addAction(alertAction)
self.present(alert, animated: true, completion: nil)
}
})
default: //用户拒绝和未授权
let alert = UIAlertController(title: "提示", message: "用户拒绝授权使用相机", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "确定", style: .default, handler: nil)
alert.addAction(alertAction)
self.present(alert, animated: true, completion: nil)
}
}

AVCaptureOutput

输出对象我们一般使用 AVCaptureOutput 的子类:AVCaptureVideoDataOutput, AVCaptureAudioDataOutput,
AVCaptureMovieFileOutput, AVCaptureStillImageOutput(AVCapturePhotoOutput), AVCaptureMetadataOutput, 他们都有相对应的代理方法,也就是说数据输出都是通过代理来获取的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 添加输出
// 本文 iOS 10 以后 相机的照片输出使用 AVCapturePhotoOutput
if #available(iOS 10.0, *) {
// 添加 AVCapturePhotoOutput 用于输出照片
let capturePhotoOutput = AVCapturePhotoOutput()
if (captureSession?.canAddOutput(capturePhotoOutput))! {
captureSession?.addOutput(capturePhotoOutput)
}
self.capturePhotoOutput = capturePhotoOutput
} else { // iOS 10 之前 相机的照片输出使用 AVCaptureStillImageOutput
captureStillImageOutput = AVCaptureStillImageOutput()
captureStillImageOutput?.outputSettings = [AVVideoCodecKey: AVVideoCodecJPEG]
if (captureSession?.canAddOutput(captureStillImageOutput))! {
captureSession?.addOutput(captureStillImageOutput)
}
}

iOS10 之前输出照片这里使用 AVCaptureStillImageOutput ,iOS 10 之后苹果推出一个替代它的类:AVCapturePhotoOutput,它不仅支持简单的静态图片拍摄,还支持LivePhoto照片和RAW格式的照片拍摄,AVCapturePhotoOutput 还可以通过代理协议通知拍照进度(照片即将被捕获,照片已被捕获但尚未处理,LivePhoto 已准备就绪等),当输出 LivePhoto 的时候不能和 AVCaptureMovieFileOutput 一起使用,如果把 AVCaptureMovieFileOutputAVCapturePhotoOutput 添加到会话中,AVCapturePhotoOutputlivePhotoCaptureSupported 属性将返回 NOAVCapturePhotoOutput 支持和 AVCaptureVideoDataOutput 一起添加到会话中使用,这样就可以同时支持 LivePhoto 和 视频拍摄

本文的 Demo 代码 iOS 10 之前相机的照片输出和视频输出使用 AVCaptureStillImageOutputAVCaptureMovieFileOutput,iOS 10 以后,相机的照片输出和视频输出使用 AVCapturePhotoOutputAVCaptureVideoDataOutput

AVCaptureVideoPreviewLayer

AVCaptureVideoPreviewLayer 是 CALayer 的一个子类,用来做为AVCaptureSession 预览视频输出,简单来说就是来把相机捕获的画面实时呈现出来的一个layer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
previewLayer?.frame = (self.preview?.bounds)!
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
//预览图层和视频方向保持一致
if #available(iOS 10.0, *) {
capturePhotoOutput?.connection(withMediaType: AVMediaTypeVideo).videoOrientation = (previewLayer?.connection.videoOrientation)!
} else {
captureStillImageOutput?.connection(withMediaType: AVMediaTypeVideo).videoOrientation = (previewLayer?.connection.videoOrientation)!
}
preview?.layer.insertSublayer(self.previewLayer!, at: 0)
captureSession?.commitConfiguration()
self.sessionQueue.async {
self.captureSession?.startRunning()
}

当上面的 captureSession 全部配置完成之后就可以调用 captureSessionstartRunning() 方法启动会话了,另外记住退出相机的时候调用stopRunning() 关闭会话

上面配置的 captureSession 定义的相机是只能拍摄照片,如果需要拍摄视频还需要往输入里添加麦克风设备,并且使用 AVCaptureMovieFileOutput 或者 AVCaptureVideoDataOutput 输出视频,为了让代码更加清晰 Demo 代码里把拍照和录制视频放在了两个控制器里面,实际开发中自定义相机可能既可以拍照又能录制视频

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
// 配置会话对象
fileprivate func configureCaptureSession() {
captureSession = AVCaptureSession()
captureSession?.beginConfiguration()
// CaptureSession 的会话预设,这个地方设置的模式/分辨率大小将影响你后面拍摄照片/视频的大小
captureSession?.sessionPreset = AVCaptureSessionPresetHigh
// 添加输入
do {
let cameraDeviceInput = try AVCaptureDeviceInput(device: self.cameraWithPosition(.back))
if (captureSession?.canAddInput(cameraDeviceInput))! {
captureSession?.addInput(cameraDeviceInput)
captureInput = cameraDeviceInput
}
let audioDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio)
let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice)
if (captureSession?.canAddInput(audioDeviceInput))! {
captureSession?.addInput(audioDeviceInput)
}
} catch let error as NSError {
print(error.localizedDescription)
}
// 添加输出
// 本文 iOS 10 以后 相机的照片输出和视频输出使用 AVCaptureVideoDataOutput
if #available(iOS 10.0, *) {
// 添加 AVCaptureVideoDataOutput 用于输出视频
let captureVideoDataOutput = AVCaptureVideoDataOutput()
if (captureSession?.canAddOutput(captureVideoDataOutput))! {
captureSession?.addOutput(captureVideoDataOutput)
}
self.captureVideoDataOutput = captureVideoDataOutput
// 添加 AVCaptureAudioDataOutput 用于输出音频
let captureAudioDataOutput = AVCaptureAudioDataOutput()
if (captureSession?.canAddOutput(captureAudioDataOutput))! {
captureSession?.addOutput(captureAudioDataOutput)
}
self.captureAudioDataOutput = captureAudioDataOutput
} else {
// iOS 10 以后 相机视频输出使用 AVCaptureMovieFileOutput
captureMovieFileOutput = AVCaptureMovieFileOutput()
if (captureSession?.canAddOutput(captureMovieFileOutput))! {
captureSession?.addOutput(captureMovieFileOutput)
}
}
previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
previewLayer?.frame = (self.preview?.bounds)!
previewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
//预览图层和视频方向保持一致
if #available(iOS 10.0, *) {
captureVideoDataOutput?.connection(withMediaType: AVMediaTypeVideo).videoOrientation = (previewLayer?.connection.videoOrientation)!
} else {
captureMovieFileOutput?.connection(withMediaType: AVMediaTypeVideo).videoOrientation = (previewLayer?.connection.videoOrientation)!
}
preview?.layer.insertSublayer(self.previewLayer!, at: 0)
captureSession?.commitConfiguration()
self.sessionQueue.async {
self.captureSession?.startRunning()
}
}

注意:配置或者更新(添加或删除输出,更改sessionPreset,或配置各个AVCaptureInput或Output属性)captureSession之前需要先调用 beginConfiguration(),完成之后再调用commitConfiguration()

操作相机

上面的工作完成之后就可以对自己定义的相机进行操作了,下面看一下如何在自定义相机下进行拍照、录制视频、保存等操作,首先是拍照,先看代码

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
func takePhoto(){
if #available(iOS 10.0, *) {
guard let capturePhotoOutput = self.capturePhotoOutput else { return }
sessionQueue.async {
let photoSettings = AVCapturePhotoSettings()
photoSettings.isAutoStillImageStabilizationEnabled = true
photoSettings.isHighResolutionPhotoEnabled = true
photoSettings.flashMode = self.currentFlashMode
if (capturePhotoOutput.isLivePhotoCaptureSupported) {
// 设置 livePhotoMovieFileURL 必须保证 isLivePhotoCaptureEnabled 为 true
// 设置了 livePhotoMovieFileURL 必须实现 livePhoto 相关的协议方法
photoSettings.livePhotoMovieFileURL = URL(fileURLWithPath: NSTemporaryDirectory() + "tempLivePhoto.mov")
capturePhotoOutput.isLivePhotoCaptureEnabled = true
capturePhotoOutput.isLivePhotoCaptureSuspended = false
capturePhotoOutput.isHighResolutionCaptureEnabled = true
}
//设置代理方法
capturePhotoOutput.capturePhoto(with: photoSettings, delegate: self)
}
} else {
let connection = captureStillImageOutput?.connection(withMediaType: AVMediaTypeVideo)
captureStillImageOutput?.captureStillImageAsynchronously(from: connection, completionHandler: { (buffer, error) in
guard error == nil else {
return
}
if let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer),
let image = UIImage(data: imageData){
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)),nil)
}
})
}
}
// MARK: - AVCapturePhotoCaptureDelegate
func capture(_ captureOutput: AVCapturePhotoOutput, didCapturePhotoForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings) {
print(#function)
}
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
print(#function)
guard error == nil, let photoSampleBuffer = photoSampleBuffer else {
print("Error capturing photo: \(String(describing: error))")
return
}
self.photoSampleBuffer = photoSampleBuffer
self.previewPhotoSampleBuffer = previewPhotoSampleBuffer
}
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL, duration: CMTime, photoDisplay photoDisplayTime: CMTime, resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
print(#function)
guard error == nil else {
print("Error capturing Live Photo: \(String(describing: error))")
return
}
self.livePhotoMovieURL = outputFileURL
}
// 在这个方法里面保存正常照片或者LivePhoto
func capture(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
print(#function)
if let photoSampleBuffer = self.photoSampleBuffer{
guard let jpgData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: self.previewPhotoSampleBuffer) else {
print("Unable to create JPEG data.")
return
}
//保存LivePhoto到相册中(livePhotoMovieURL 存在,判断为LivePhoto)
if let livePhotoMovieURL = self.livePhotoMovieURL {
PHPhotoLibrary.shared().performChanges({
let creationRequest = PHAssetCreationRequest.forAsset()
let creationOptions = PHAssetResourceCreationOptions()
creationOptions.shouldMoveFile = true
creationRequest.addResource(with: PHAssetResourceType.photo, data: jpgData, options: nil)
creationRequest.addResource(with: PHAssetResourceType.pairedVideo, fileURL: livePhotoMovieURL, options: creationOptions)
}) { (success, error) in
if success {
print("Added Live Photo to library.")
} else {
print("Error adding Live Photo to library: \(String(describing: error))")
}
}
}else {// 保存普通照片到相册(判断为非LivePhoto)
DispatchQueue.global().async {
UIImageWriteToSavedPhotosAlbum(UIImage(data: jpgData)!, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)),nil)
}
}
}
}

如果使用 AVCaptureStillImageOutput 输出照片可以直接使用 captureStillImageAsynchronously 方法在闭包中异步获取到当前照片,AVCapturePhotoOutput 则需要实现 AVCapturePhotoCaptureDelegate的协议方法,在协议方法里面获取拍摄的照片数据,LivePhoto(仅支持iPhone6s及以上)本质就是一张照片和拍摄这张照片前后1.5秒的视频组成的“照片”,所以要在相册里面保存一张 LivePhoto,必须包含两部分:LivePhoto 的图片Data数据和 LivePhoto 视频临时地址,使用PHPhotoLibrary 把这个两部分保存到相册就生成了一张 LivePhoto

注意:实际上只有 AVCaptureSession 的预设只有 AVCaptureSessionPresetPhoto 才支持 LivePhoto,其他视频相关的预设都不支持

使用 AVCaptureDevice 设置硬件属性(FocusMode、TorchMode、FlashMode、ExposureMode等)需要先锁定设备,设置完成之后再解锁

设置闪关灯

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
func changeflash(button: UIButton) {
if (self.captureInput?.device.hasFlash)! {// 判读是否有闪关灯
do {
// 先锁定设备
try self.captureInput?.device.lockForConfiguration()
if let flashMode = self.captureInput?.device.flashMode {
if flashMode == AVCaptureFlashMode.off {
self.captureInput?.device.flashMode = .on
button.setTitle("闪光灯:开启", for: .normal)
}else if flashMode == AVCaptureFlashMode.on {
self.captureInput?.device.flashMode = .auto
button.setTitle("闪光灯:自动", for: .normal)
}else {
self.captureInput?.device.flashMode = .off
button.setTitle("闪光灯:关闭", for: .normal)
}
}
} catch let error as NSError {
print(error.localizedDescription)
}
// 设置完成之后解锁设备
self.captureInput?.device.unlockForConfiguration()
}
}

设置手电筒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func changeTorch(button: UIButton) {
button.isSelected = !button.isSelected
if (self.captureInput?.device.hasTorch)! {//打开手电筒
do {
try self.captureInput?.device.lockForConfiguration()
if button.isSelected {
self.captureInput?.device.torchMode = AVCaptureTorchMode.on
}else {
self.captureInput?.device.torchMode = AVCaptureTorchMode.off
}
} catch let error as NSError {
print(error.localizedDescription)
}
self.captureInput?.device.unlockForConfiguration()
}
}

另外需要注意 TorchMode 和 FlashMode 的区别:前者表示手电筒模式,它的使用效果就和 iPhone 控制中心里手电筒开关效果一致,并且可以对手电筒的亮度进行设置(iOS 11 已经可以在控制中心之间调节亮度了),后者表示摄像头的闪光灯模式,用于拍照时候补光,这两者的使用之前都需要判断当前设备是否支持,这篇博客)可以参考更多的操作相机的功能

注意:使用 AVCapturePhotoOutput 之后,必须通过 AVCapturePhotoSettings 去设置相机的闪光灯模式,之前使用 AVCaptureDevice 的属性设置闪关灯模式会被忽略

接下来看如何用自定义的相机拍摄视频

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
func recordVideo(button: UIButton) {
button.isSelected = !button.isSelected
if #available(iOS 10.0, *) { // iOS 10 这里 使用 AVAssetWriter 保存视频
if let assetWriter = self.assetWriter,assetWriter.status == .writing {// 正在录制,保存
if let videoWriterInput = self.assetWriterVideoInput {
videoWriterInput.markAsFinished()
}
if let audioWriterInput = self.assetWriterAudioInput {
audioWriterInput.markAsFinished()
}
assetWriter.finishWriting(completionHandler: {
// 视频已经完成写入到指定的路径
// 可以把视频保存到相册或者保存到APP的沙盒
if UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(assetWriter.outputURL.path) {
DispatchQueue.global().async {
UISaveVideoAtPathToSavedPhotosAlbum(assetWriter.outputURL.path, self, #selector(self.video(_ :didFinishSavingWithError:contextInfo:)), nil)
}
}
})
}else {// 开始录制视频
configureAssetWriter()
captureVideoDataOutput?.setSampleBufferDelegate(self, queue: sessionQueue)
captureAudioDataOutput?.setSampleBufferDelegate(self, queue: sessionQueue)
}
} else { // iOS 10 之前 这里 使用 AVCaptureMovieFileOutput 输出视频
if (captureMovieFileOutput?.isRecording)! {//判断当前是否已经在录制视频
captureMovieFileOutput?.stopRecording()
}else {
let url = URL(fileURLWithPath: NSTemporaryDirectory() + "outPut.mov")
captureMovieFileOutput?.startRecording(toOutputFileURL: url, recordingDelegate: self)
}
}
}

使用 AVCaptureMovieFileOutput 输出视频需要AVCaptureFileOutputRecordingDelegate 的相关协议方法

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
// MARK: - AVCaptureFileOutputRecordingDelegate
func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) {
print("开始录制")
}
func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) {
print("停止录制")
if UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(outputFileURL.path) {//判断视频路径是否可以被保存的相册
//保存视频到图库中
DispatchQueue.global().async {
UISaveVideoAtPathToSavedPhotosAlbum(outputFileURL.path, self, #selector(self.video(_ :didFinishSavingWithError:contextInfo:)), nil)
}
}
print(outputFileURL)
}
// MARK: - UISaveVideoAtPathToSavedPhotosAlbum
//UISaveVideoAtPathToSavedPhotosAlbum 保存视频之后的回调,判断视频是否保存成功,方法名必须这样写
func video(_ videoPath: String,
didFinishSavingWithError error: NSError?,
contextInfo: UnsafeRawPointer) {
if let error = error {
// we got back an error!
let ac = UIAlertController(title: "警告", message: error.localizedDescription, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "确定", style: .default))
present(ac, animated: true)
} else {
let ac = UIAlertController(title: "提示", message: "视频成功保存到相册", preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "确定", style: .default))
present(ac, animated: true)
}
}

AVCaptureMovieFileOutput 只能用来写入 QuickTime 视频类型(.mov)的媒体文件,而且也不能实现暂停录制、不能定义视频文件的类型,不过AVCaptureMovieFileOutput 还有一些其他的配置选项,比如在某段时间后,在达到某个指定的文件尺寸时,或者当设备的最小磁盘剩余空间达到某个阈值时停止录制等

很多时候我们都是使用灵活性更强的 AVCaptureVideoDataOutputAVCaptureAudioDataOutput 来实现视频的录制,使用 AVCaptureVideoDataOutput 输出视频需要实现AVCaptureVideoDataOutputSampleBufferDelegate 的相关协议方法,并且在协议方法里面使用 AVAssetWriter 合成视频

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
// 设置 AVAssetWriter
fileprivate func configureAssetWriter() {
// 设置 AVAssetWriter 的视频输入设置
let videoSettings = [AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: 720,
AVVideoHeightKey: 1280] as [String : Any];
let assetWriterVideoInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoSettings)
assetWriterVideoInput.expectsMediaDataInRealTime = true
self.assetWriterVideoInput = assetWriterVideoInput
// 设置 AVAssetWriter 的音频输入设置
let audioSettings = [AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: NSNumber(value: 44100.0),
AVNumberOfChannelsKey: NSNumber(value: 2)] as [String : Any]
let assetWriterAudioInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: audioSettings)
assetWriterAudioInput.expectsMediaDataInRealTime = true
self.assetWriterAudioInput = assetWriterAudioInput
let videoUrlStr = NSTemporaryDirectory() + "tempVideo.mp4"
let videoUrl = URL(fileURLWithPath: videoUrlStr)
self.videoUrl = videoUrl
do {
try FileManager.default.removeItem(at: videoUrl)
} catch {
print(error)
}
do {
let assetWriter = try AVAssetWriter(url: videoUrl, fileType: AVFileTypeMPEG4)
if assetWriter.canAdd(assetWriterVideoInput) {
assetWriter.add(assetWriterVideoInput)
}
if assetWriter.canAdd(assetWriterAudioInput) {
assetWriter.add(assetWriterAudioInput)
}
self.assetWriter = assetWriter
} catch {
print(error)
}
}
// MARK: - AVCaptureVideoDataOutputSampleBufferDelegate,AVCaptureAudioDataOutputSampleBufferDelegate
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
objc_sync_enter(self)
if let assetWriter = assetWriter {
if assetWriter.status != .writing && assetWriter.status != .unknown {
return
}
}
if let assetWriter = self.assetWriter, assetWriter.status == AVAssetWriterStatus.unknown {
assetWriter.startWriting()
assetWriter.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
}
// 视频数据
if connection == captureVideoDataOutput?.connection(withMediaType: AVMediaTypeVideo) {
let videoDataOutputQueue = DispatchQueue(label: "com.xiaovv.videoDataOutputQueue")
videoDataOutputQueue.async {
if let videoWriterInput = self.assetWriterVideoInput, videoWriterInput.isReadyForMoreMediaData {
videoWriterInput.append(sampleBuffer)
}
}
}
// 音频数据
if connection == captureVideoDataOutput?.connection(withMediaType: AVMediaTypeAudio) {
let audioDataOutputQueue = DispatchQueue(label: "com.xiaovv.audioDataOutputQueue")
audioDataOutputQueue.async {
if let audioWriterInput = self.assetWriterAudioInput, audioWriterInput.isReadyForMoreMediaData {
audioWriterInput.append(sampleBuffer)
}
}
}
objc_sync_exit(self)
}
func captureOutput(_ captureOutput: AVCaptureOutput!, didDrop sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
}

从上面的流程可以看出 AVCaptureMovieFileOutputAVCaptureVideoDataOutputAVCaptureAudioDataOutput 的区别,前者只需要一个输出即可,指定一个文件路径后,视频和音频会写入到指定路径,不需要其他复杂的操作;后者则需要使用 AVAssetWriterAVCaptureVideoDataOutputAVCaptureAudioDataOutput 的输出数据通过 AVAssetWriterInput 写入视频文件,详细请参考 Demo 代码

上面就是 AVFoundation 自定义相机的基本使用,当然 AVFoundation的作用不仅仅是自定义相机,AVFoundation 是一个多媒体框架,可以使用它来播放和创建基于时间的音视频资源,AVFoundation 提供的接口可以精确地处理基于时间的音视频媒体数据,比如媒体文件的查找、创建、编辑甚至二次编码操作都可以使用 AVFoundation框架完成,一本书都可能讲解不完里面的全部知识,本人才疏学浅,也只是略知皮毛而已,还学习多多的学习

以上です

参考

Photo Capture Programming Guide

iOS 上的相机捕捉

在 iOS 上捕获视频

iOS 10 by Tutorials 笔记(十二)

本人刚开始写博客,主要是为了给自己的知识点做一个笔记,方便自己以后查阅,如果能让别人有所启发也是荣幸之至!如有错误,欢迎指正!