请注意,如果没有指定对应API的使用原因,那么你在调用相关API时,系统会将你的App终结。
苹果官方文档建议我们在使用以上这些API之前首先使用 AVCaptureDevice.authorizationStatus(for:) 检查应用权限的授予情况,如果没有被授予使用权限,需要使用AVCaptureDevice.requestAccess(for:completionHandler:)来显示弹框,请求权限。
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之后,就是开始配置:
// 固定语法,开始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
的可选值有这些:
// 内置麦克风
static let builtInMicrophone: AVCaptureDevice.DeviceType
// 对于iPhone来说,指的是后置的摄像头
static let builtInWideAngleCamera: AVCaptureDevice.DeviceType
// 后置双摄
static let builtInDualCamera: AVCaptureDevice.DeviceType
// 这个就不用说了,粪叉以后带出来的TrueDepth摄像头
static let builtInTrueDepthCamera: AVCaptureDevice.DeviceType
/// ...
一个captureSession可以配置多个输入以及输出流,比如需求是需要同时调用后置摄像头以及麦克风,你可以这样写:
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)
下一步,添加输出流:
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)
的方式使用,也可以:
// 苹果官方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:
self.videoPreviewView.videoPreviewLayer.session = self.captureSession
请注意,如果你的拍照页面支持横向拍摄的话,请使用
AVCaptureVideoPreviewLayer
的connection属性来获得当前设备拍摄朝向。// 在进入sessionQueue之前取得这个属性,是为了确保UI级别的操作都在主线程进行,session的操作都在sessionQueue进行。 let videoPreviewLayerOrientation = videoPreviewView.videoPreviewLayer.connection?.videoOrientation sessionQueue.async { if let photoOutputConnection = self.photoOutput.connection(with: .video) { photoOutputConnection.videoOrientation = videoPreviewLayerOrientation! } }
至此,基本的配置工作已经完成了,接下来就是让数据流在输入以及输出流中跑起来。
self.captureSession.startRunning()
// 调用了startRunning()方法之后,即可以在设置的videoPreviewLayer上预览到实时的相机效果。
// startRunning()以及stopRunning()方法都会阻塞当前线程,请确保是在sessionQueue里进行。
退出页面时,请记得调用captureSession的stopRunning()
方法,因为应用在后台时调用相机,是被苹果禁止的,这也是未越狱的苹果设备上,没有“偷拍应用”的原因。
另外需要注意的是,硬件的调用有可能被其他系统行为打断(比如有电话进来)。
AVCaptureSession
类提供了一系列的NotificationName
,其中包括session开始运行,结束运行,或者session被打断:
// 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
类,并配置相关设置。这些设置将在拍照时使用,比如拍照时是否打开闪光灯等属性;
let capturePhotoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
capturePhotoSettings.isAutoRedEyeReductionEnabled = true
capturePhotoSettings.isHighResolutionPhotoEnabled = true
...
// 接下来应该是调用AVCapturePhotoOutput的capturePhoto方法
photoOutput.capturePhoto(with: capturePhotoSettings, delegate: self)
遵循AVCapturePhotoCaptureDelegate
并实现相关代理方法。一般来说我们只需要关心拍照结果:
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,请查看:官方文档。