使用 AVFoundation 实现自定义相机拍照
使用摄像头拍照
使用 AVFoundation 拍照需要至少使用以下类:
private(set) var session = AVCaptureSession()
private(set) var device: AVCaptureDevice?
private(set) var input: AVCaptureDeviceInput?
private(set) var imageOutput: AVCapturePhotoOutput?
private(set) var previewLayer: AVCaptureVideoPreviewLayer?
其中, AVCaptureSession
用于视频捕获,AVCaptureDevice
用于管理设备,AVCaptureDeviceInput
是当前工作的摄像头(不限于摄像头),AVCapturePhotoOutput
是用来输出图像,AVCaptureVideoPreviewLayer
用于显示当前捕捉到的视频。
开始捕捉视频图像
// 照相功能初始化
private func setupCaptureSession() {
// 判断是否初始化完成
var successful = true
defer {
if !successful {
// log error msg...
print("error setting capture session")
}
}
// 判断至少有一个摄像头,并对涉嫌头初始化
guard let d = AVCaptureDevice.default(for: .video), let device = tunedCaptureDevice(device: d) else {
successful = false
return
}
// 配置 session
session.beginConfiguration()
defer {
session.commitConfiguration()
}
session.sessionPreset = .photo
do {
input = try AVCaptureDeviceInput(device: device)
if session.canAddInput(input!) {
session.addInput(input!)
} else {
successful = false
return
}
} catch {
print(error.localizedDescription)
successful = false
return
}
imageOutput = AVCapturePhotoOutput()
let settings = AVCapturePhotoSettings.init(format: [AVVideoCodecKey: AVVideoCodecJPEG])
imageOutput?.setPreparedPhotoSettingsArray([settings]) { (a, e) in
print(e?.localizedDescription ?? "")
}
if session.canAddOutput(imageOutput!) {
session.addOutput(imageOutput!)
} else {
successful = false
return
}
// 图像显示 layer 配置
previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer!.videoGravity = .resizeAspectFill
previewLayer!.frame = bounds
self.layer.insertSublayer(previewLayer!, at: 0)
}
// 摄像头参数配置
private func tunedCaptureDevice(device: AVCaptureDevice) -> AVCaptureDevice? {
do {
try device.lockForConfiguration()
if device.isFocusModeSupported(.continuousAutoFocus) {
device.focusMode = .continuousAutoFocus
}
if device.isExposureModeSupported(.continuousAutoExposure) {
device.exposureMode = .continuousAutoExposure
}
if device.isWhiteBalanceModeSupported(.continuousAutoWhiteBalance) {
device.whiteBalanceMode = .continuousAutoWhiteBalance
}
device.unlockForConfiguration()
return device
} catch {
print(error.localizedDescription)
return nil
}
}
关于捕捉到的图像与设备之前的横竖屏切换的设置
捕捉到的图像并不是根据设备自动切换横竖屏的,因此需要在代码里面手动的设置捕捉到的图像的状态。
// 根据当前设备的状态计算捕捉到的图像的状态
var videoOrientationFromCurrentDeviceOrientation: AVCaptureVideoOrientation? {
get {
switch UIDevice.current.orientation {
case .landscapeLeft:
return .landscapeRight
case .landscapeRight:
return .landscapeLeft
// case .portraitUpsideDown:
// return .portraitUpsideDown
case .portrait:
return .portrait
default:
return nil
}
}
}
// 切换图像横竖屏状态
func updateOrientation() {
guard let v = videoOrientationFromCurrentDeviceOrientation else {
return
}
previewLayer?.connection?.videoOrientation = v
imageOutput?.connection(with: .video)?.videoOrientation = v
}
前后摄像头切换
获取摄像头
// 当前设备支持的摄像头
var cameras: [AVCaptureDevice] {
get {
let s = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: .video, position: .unspecified)
return s.devices
}
}
// 根据摄像头位置获取前后摄像头
private func camera(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
let s = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: .video, position: position)
for d in s.devices {
if d.position == position {
return tunedCaptureDevice(device: d)
}
}
return nil
}
切换摄像头
func changeCamera() {
if cameras.count > 1 {
let animation = CATransition()
animation.duration = 0.5
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.type = "oglFlip"
var newCamera: AVCaptureDevice?
let position = input?.device.position
if position == .back {
newCamera = camera(position: .front)
animation.subtype = kCATransitionFromLeft
} else {
newCamera = camera(position: .back)
animation.subtype = kCATransitionFromRight
}
guard let ca = newCamera, let newInput = try? AVCaptureDeviceInput(device: ca) else {
return
}
OnMainThreadAsync {
self.previewLayer?.add(animation, forKey: nil)
self.session.beginConfiguration()
defer {
self.session.commitConfiguration()
}
if self.input != nil {
self.session.removeInput(self.input!)
}
if self.session.canAddInput(newInput) {
self.session.addInput(newInput)
self.input = newInput
} else {
if self.input != nil {
self.session.addInput(self.input!)
}
return
}
}
}
}
拍照
func captureImage() {
OnMainThreadAsync {
if let output = self.session.outputs.first as? AVCapturePhotoOutput {
output.isHighResolutionCaptureEnabled = true
let settings = AVCapturePhotoSettings()
settings.isHighResolutionPhotoEnabled = true
// settings.flashMode = AVCaptureDevice.FlashMode.auto
let previewPixelType = settings.availablePreviewPhotoPixelFormatTypes.first!
let previewFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPixelType,
kCVPixelBufferWidthKey as String: 160,
kCVPixelBufferHeightKey as String: 160,
]
settings.previewPhotoFormat = previewFormat
if output.supportedFlashModes.contains(AVCaptureDevice.FlashMode.auto) {
settings.flashMode = AVCaptureDevice.FlashMode.auto
} else if output.supportedFlashModes.contains(AVCaptureDevice.FlashMode.on) {
settings.flashMode = AVCaptureDevice.FlashMode.on
} else if output.supportedFlashModes.contains(AVCaptureDevice.FlashMode.off) {
settings.flashMode = AVCaptureDevice.FlashMode.off
}
output.capturePhoto(with: settings, delegate: self)
}
}
}
// AVCapturePhotoCaptureDelegate
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
guard let didFinishProcessingPhoto = photoSampleBuffer, let previewPhoto = previewPhotoSampleBuffer else {
self.delegate?.ErrorOnTakePhoto(view: self, error: error)
return
}
guard let data = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: didFinishProcessingPhoto, previewPhotoSampleBuffer: previewPhoto) else {
self.delegate?.ErrorOnTakePhoto(view: self, error: error)
return
}
guard let image = UIImage(data: data) else {
self.delegate?.ErrorOnTakePhoto(view: self, error: error)
return
}
self.delegate?.ImageDidSelected(view: self, image: image)
self.session.stopRunning()
}
遇到的问题。
-
拍照的时候概率性的出现
Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSUnderlyingError=0x17025db20 {Error Domain=NSOSStatusErrorDomain Code=-16800 "(null)"}, NSLocalizedFailureReason=An unknown error occurred (-16800), NSLocalizedDescription=The operation could not be completed}
这个错误。这个错误引起的原因是output.capturePhoto
之后立即执行session.stopRunning()
了,造成同一个 frametime 有n帧图片,这样就会报错。解决方法:
在代理中停止 session。 -
横竖屏设置的时候图像横屏的时候总是反的。原因在于 AVCaptureVideoOrientation 的 landscapeRight 对应的是 UIDeviceOrientation 的 landscapeLeft。Right 对应的是 Left。