iOS摄像头与媒体捕捉

iOS摄像头与媒体捕捉

Aron Lv3

本文记录的是苹果官方文档Cameras and Media Capture的阅读中文笔记, 以及相关自己的理解。

本框架用于实现调用iOS设备的音频以及视频输入输出设备, 在屏幕上显示相机的实时预览, 以及拍照等功能.

请求媒体捕捉访问权限

如果你的App要使用摄像头,需要在Info.plist文件的NSCameraUsageDescription指定使用摄像头的原因。
如果你的App要使用麦克风,需要在Info.plist文件的NSMicrophoneUsageDescription指定使用麦克风的原因。

请注意,如果没有指定对应API的使用原因,那么你在调用相关API时,系统会将你的App终结。

检查授权情况

苹果官方文档建议我们在使用以上这些API之前首先使用 AVCaptureDevice.authorizationStatus(for:) 检查应用权限的授予情况,如果没有被授予使用权限,需要使用AVCaptureDevice.requestAccess(for:completionHandler:)来显示弹框,请求权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: // 用户之前已经授予了使用权限
self.setupCaptureSession()

case .notDetermined: // 还没有授予权限
// requestAccess方法将会显示系统的权限请求弹窗
// 要先配置Info.plist文件,添加NSCameraUsageDescription字段
// 先停止sessionQueue的进行
self.sessionQueue.suspend()
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
self.sessionQueue.resume()
self.setupCaptureSession()
}
}

case .denied: // 被拒绝
self.sessionQueue.suspend()
return
case .restricted: // 因为某些原因无法授予权限
self.sessionQueue.suspend()
return
}

请注意,如果摄像或者拍照不是你的App的主要功能,那么你只能在会使用到相关功能的时候才可以请求权限。

请求保存媒体权限

如果是使用上述API拍摄的视频或者照片,请使用PHPhotoLibrary以及PHAssetCreationRequest类。这些类会使用到相册的读写权限,所以需要指定NSPhotoLibraryUsageDescription字段。

如果只是想保存UIImage对象,请使用 UIImageWriteToSavedPhotosAlbum(::::) 方法,此方法会使用到相册的写入权限。请注意,如果图片对象来自AVFoundation( AVCapturePhotoOutput),不推荐使用此方法写入,因为UIImage中不会包含图片中的全部信息。

如果想保存一段视频,请使用 UISaveVideoAtPathToSavedPhotosAlbum(::::) 方法,此方法同样会使用到相册的写入权限。

以上两个方法都需要指定NSPhotoLibraryAddUsageDescription字段。

配置媒体捕获与输出设备

AVCaptureSession类用于整合输入流(媒体输入设备,比如摄像头,麦克风)和输出流(媒体输出设备,比如相机预览,扬声器等),是输入流与输出流交互的管道。开发者通过AVCaptureConnection类将输入流额输出流绑定。

AVCaptureDevice类代表了物理媒体捕获设备,比如相机以及麦克风。

AVCaptureDeviceInput类代表一个输入流,它基于一个captureDevice,比如前置相机,话筒等。负责与captureSession交互。

AVCaptureDeviceOutput类代表了一个输出流,这是一个抽象类,它描述的是媒体输出的方式,比如图片输出:AVCapturePhotoOutput,用于描述的是静态图像,Live Photo等媒体输出方式。

所以初始化session之后,就是开始配置:

1
2
3
4
5
6
7
8
9
10
11
// 固定语法,开始AVCaptureSession类的配置
captureSession.beginConfiguration()
// 获取后置的摄像头
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video, position: .unspecified)
guard
let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!),
// 在添加输出或者输入流之前必须检查是否可以添加
captureSession.canAddInput(videoDeviceInput)
else { return }
captureSession.addInput(videoDeviceInput)

AVCaptureDevice的可选值有这些:

1
2
3
4
5
6
7
8
9
10
// 内置麦克风
static let builtInMicrophone: AVCaptureDevice.DeviceType
// 对于iPhone来说,指的是后置的摄像头
static let builtInWideAngleCamera: AVCaptureDevice.DeviceType
// 后置双摄
static let builtInDualCamera: AVCaptureDevice.DeviceType
// 这个就不用说了,粪叉以后带出来的TrueDepth摄像头
static let builtInTrueDepthCamera: AVCaptureDevice.DeviceType

/// ...

一个captureSession可以配置多个输入以及输出流,比如需求是需要同时调用后置摄像头以及麦克风,你可以这样写:

1
2
3
4
5
6
7
8
9
captureSession.beginConfiguration()
let photoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
let microphoneDevice = AVCaptureDevice.default(.builtInMicrophone, for: .audio, position: .unspecified)
guard let photoDeviceInput = try? AVCaptureDeviceInput(device: photoDevice!),
let microDeviceInput = try? AVCaptureDeviceInput(device: microphoneDevice!),
captureSession.canAddInput(photoDeviceInput),
captureSession.canAddInput(microDeviceInput) else {return}
captureSession.addInput(photoDeviceInput)
captureSession.addInput(microDeviceInput)

下一步,添加输出流:

1
2
3
4
5
6
7
8
9
let photoOutput = AVCapturePhotoOutput()
// photoOutput.isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureSupported
// photoOutput.isDepthDataDeliveryEnabled = photoOutput.isDepthDataDeliverySupported
// photoOutput.isPortraitEffectsMatteDeliveryEnabled = photoOutput.isPortraitEffectsMatteDeliverySupported
// 需要注意的是,如果你的应用支持切换前后摄像头的功能,以上这些属性都需要重新设置一次。
guard captureSession.canAddOutput(photoOutput) else { return }
captureSession.sessionPreset = .photo
captureSession.addOutput(photoOutput)
captureSession.commitConfiguration()

输出并不依赖于硬件,所以只需要添加一个输出流,配置完成之后调用commitConfiguration。这里只是进行了一个最基础的captureSession的配置,一般来说,只需要一个输入以及输出流,一个流程就算是走通了。

视频格式的媒体捕获,对应的输出类型是AVCaptureMovieFileOutput

####显示相机预览

实时预览到相机捕获到的画面,是基于AVCaptureVideoPreviewLayer类,它是CALayer的子类。它通过session属性与captureSession交互。

你可以以直接操作CALayer的方式,初始化previewLayer,设置session属性,然后self.view.layer.addSubLayer(previewLayer)的方式使用,也可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 苹果官方Demo给出的方案
class PreviewView: UIView {
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}

var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}

class TakePhotoViewController: UIViewController {
var videoPreviewView: PreviewView {
return self.view as! PreviewView
}

override func loadView() {
let previewView = PreviewView(frame: CGRect.zero)
// 配置预览页面的显示模式,类似于UIView的ContentMode
previewView.videoGravity = .resize
self.view = view
}
}

然后是设置session:

1
self.videoPreviewView.videoPreviewLayer.session = self.captureSession

请注意,如果你的拍照页面支持横向拍摄的话,请使用AVCaptureVideoPreviewLayer的connection属性来获得当前设备拍摄朝向。

1
2
3
4
5
6
7
// 在进入sessionQueue之前取得这个属性,是为了确保UI级别的操作都在主线程进行,session的操作都在sessionQueue进行。
let videoPreviewLayerOrientation = videoPreviewView.videoPreviewLayer.connection?.videoOrientation
sessionQueue.async {
if let photoOutputConnection = self.photoOutput.connection(with: .video) {
photoOutputConnection.videoOrientation = videoPreviewLayerOrientation!
}
}

跑起来吧

至此,基本的配置工作已经完成了,接下来就是让数据流在输入以及输出流中跑起来。

1
2
3
self.captureSession.startRunning()
// 调用了startRunning()方法之后,即可以在设置的videoPreviewLayer上预览到实时的相机效果。
// startRunning()以及stopRunning()方法都会阻塞当前线程,请确保是在sessionQueue里进行。

处理错误

退出页面时,请记得调用captureSession的stopRunning()方法,因为应用在后台时调用相机,是被苹果禁止的,这也是未越狱的苹果设备上,没有“偷拍应用”的原因。

另外需要注意的是,硬件的调用有可能被其他系统行为打断(比如有电话进来)。

AVCaptureSession类提供了一系列的NotificationName,其中包括session开始运行,结束运行,或者session被打断:

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
// startRunning()调用完成之后发送此通知
static let AVCaptureSessionDidStartRunning: NSNotification.Name

// stopRunning() 调用完成之后发送
static let AVCaptureSessionDidStopRunning: NSNotification.Name

// session被打断,通知内的`userInfo`会带有AVCaptureSessionInterruptionReasonKey字段,其值是枚举值的rawValue,标识着被打断的原因。
@available(iOS 9.0, *)
public enum InterruptionReason : Int {
// 当前应用被退出到后台
case videoDeviceNotAvailableInBackground
// 被其他应用占用了音配输入设备,比如来电话了,或者系统闹铃响了
case audioDeviceInUseByAnotherClient
// 被其他captureSession占用了输入设备
case videoDeviceInUseByAnotherClient
// "MultipleForegroundApps"一般指的是iPad或者macOS上的分屏功能
case videoDeviceNotAvailableWithMultipleForegroundApps
// 被系统强制关闭
@available(iOS 11.1, *)
case videoDeviceNotAvailableDueToSystemPressure
}
static let AVCaptureSessionWasInterrupted: NSNotification.Name

// “打断”结束了
static let AVCaptureSessionInterruptionEnded: NSNotification.Name

NotificationCenter.default.addObserver(self,
selector: #selector(self.sessionWasInterrupted),
name: .AVCaptureSessionWasInterrupted,
object: self.captureSession)

...

@objc func sessionWasInterrupted(_ note: Notification) {
//
if let reasonIntegerValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int,
let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
switch reason {
...
}
}
}

拍摄

准备工作

创建一个AVCapturePhotoSettings类,并配置相关设置。这些设置将在拍照时使用,比如拍照时是否打开闪光灯等属性;

1
2
3
4
5
6
7
let capturePhotoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
capturePhotoSettings.isAutoRedEyeReductionEnabled = true
capturePhotoSettings.isHighResolutionPhotoEnabled = true
...

// 接下来应该是调用AVCapturePhotoOutput的capturePhoto方法
photoOutput.capturePhoto(with: capturePhotoSettings, delegate: self)
Next Step,使用NSData或者将照片保存到相册

遵循AVCapturePhotoCaptureDelegate并实现相关代理方法。一般来说我们只需要关心拍照结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {

// photo.fileDataRepresentation()可用于获得拍照得到的NSData对象,可用于转化UIImage
// 请求相册权限,前文说了我们应该只在使用到相关功能的时候才会请求相关权限

guard error == nil else { print("Error capturing photo: \(error!)"); return }

PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else { return }

PHPhotoLibrary.shared().performChanges({
// Add the captured photo's file data as the main resource for the Photos asset.
let creationRequest = PHAssetCreationRequest.forAsset()
creationRequest.addResource(with: .photo, data: photo.fileDataRepresentation()!, options: nil)
}, completionHandler: self.handlePhotoLibraryError)
}

}

AVCapturePhotoCaptureDelegate的其他代理方法是用来追踪拍照进度的,分别代表 开始曝光,结束曝光,开始处理,结束处理等时间点。

若要拍摄Live Photo,请查看:官方文档

  • 标题: iOS摄像头与媒体捕捉
  • 作者: Aron
  • 创建于 : 2019-05-19 20:30:00
  • 更新于 : 2025-10-14 09:29:25
  • 链接: https://likeso.github.io/2019/05/19/cameras-and-media-capture/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论