iOS Camera didFinishProcessingPhoto返回随机错误方向问题求助
iOS 17 相机拍摄图片方向异常修复方案
问题描述
测试设备为运行iOS 17的iPhone 13,已关闭控制中心「旋转锁定」设置,使用AVFoundation拍摄后,imageViewForOutput显示的图片方向始终异常:
- 竖屏正常拍摄:图片水平翻转,打印方向值为6
- 设备顺时针旋转90°拍摄:图片逆时针旋转90°,打印方向值为3
- 设备逆时针旋转90°拍摄:图片上下颠倒,打印方向值为1
复现代码如下:
import UIKit import SnapKit import AVFoundation class ViewController: UIViewController, AVCapturePhotoCaptureDelegate { var clickerButton = UIButton() var flipCameraButton = UIButton() var frontCameraDeviceInput: AVCaptureDeviceInput? var backCameraDeviceInput: AVCaptureDeviceInput? let captureSession = AVCaptureSession() let photoOutput = AVCapturePhotoOutput() let viewForCameraPreview = UIView() var previewLayer : AVCaptureVideoPreviewLayer? var imageViewForOutput = UIImageView() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black view.addSubview(viewForCameraPreview) viewForCameraPreview.snp.makeConstraints { make in make.edges.equalToSuperview() } let padding = 15.0 let buttonSize = 70.0 clickerButton.addTarget(self, action: #selector(clickPhoto(sender:)), for: .touchUpInside) view.addSubview(clickerButton) clickerButton.snp.makeConstraints { make in make.size.equalTo(buttonSize) make.centerX.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).inset(padding) } clickerButton.layer.cornerRadius = buttonSize / 2 clickerButton.clipsToBounds = true clickerButton.layer.borderColor = UIColor.white.cgColor clickerButton.layer.borderWidth = 5 flipCameraButton.addTarget(self, action: #selector(flipCamera(sender:)), for: .touchUpInside) flipCameraButton.setTitle("F", for: .normal) flipCameraButton.titleLabel?.font = UIFont.systemFont(ofSize: 50, weight: .bold) flipCameraButton.setTitleColor(.white, for: .normal) view.addSubview(flipCameraButton) flipCameraButton.snp.makeConstraints { make in make.size.equalTo(buttonSize) make.right.equalTo(clickerButton.snp.left).inset(-padding) make.centerY.equalTo(clickerButton) } imageViewForOutput.layer.borderColor = UIColor.white.cgColor imageViewForOutput.layer.borderWidth = 2 imageViewForOutput.contentMode = .scaleAspectFit view.addSubview(imageViewForOutput) imageViewForOutput.snp.makeConstraints { make in make.size.equalTo(200) make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(padding) make.left.equalTo(view.safeAreaLayoutGuide.snp.left).offset(padding) } configureCameras() whichCameraStuff() startCamera() } @objc func flipCamera(sender: UIButton) { sender.isSelected = !sender.isSelected whichCameraStuff() } func configureCameras(){ if let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) { frontCameraDeviceInput = try? AVCaptureDeviceInput(device: frontCamera) } if let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { backCameraDeviceInput = try? AVCaptureDeviceInput(device: backCamera) } photoOutput.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil) if captureSession.canAddOutput(photoOutput) { captureSession.addOutput(photoOutput) } } func whichCameraStuff(){ captureSession.beginConfiguration() if flipCameraButton.isSelected { if let b = backCameraDeviceInput, captureSession.inputs.contains(b) { captureSession.removeInput(b) } if let f = frontCameraDeviceInput, !captureSession.inputs.contains(f) { captureSession.addInput(f) } } else { if let f = frontCameraDeviceInput, captureSession.inputs.contains(f) { captureSession.removeInput(f) } if let b = backCameraDeviceInput, !captureSession.inputs.contains(b) { captureSession.addInput(b) } } captureSession.commitConfiguration() } func startCamera(){ DispatchQueue.global().async { self.captureSession.startRunning() self.addCameraPreviewIfNeeded() } } func addCameraPreviewIfNeeded(){ DispatchQueue.main.async { if self.previewLayer == nil { self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession) if let pl = self.previewLayer { pl.videoGravity = AVLayerVideoGravity.resizeAspectFill pl.connection?.automaticallyAdjustsVideoMirroring = false self.viewForCameraPreview.layer.insertSublayer(pl, at: 0) pl.frame = self.viewForCameraPreview.bounds self.updateVideoOrientation() } } self.previewLayer?.isHidden = false } } func updateVideoOrientation() { guard let pl = self.previewLayer, let connection = pl.connection else { return } guard connection.isVideoOrientationSupported else { return } let videoOrientation = UIDevice.current.orientation.asCaptureVideoOrientation if connection.videoOrientation == videoOrientation { print("no change to videoOrientation") return } print("videoOrientation: \(videoOrientation)") previewLayer?.connection?.videoOrientation = videoOrientation photoOutput.connection(with: AVMediaType.video)?.videoOrientation = videoOrientation previewLayer?.frame = viewForCameraPreview.bounds previewLayer?.removeAllAnimations() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: nil, completion: { [weak self] (context) in DispatchQueue.main.async(execute: { self?.updateVideoOrientation() }) }) } @objc func clickPhoto(sender: UIButton) { guard captureSession.isRunning else { return } let settings = AVCapturePhotoSettings() settings.flashMode = .off photoOutput.capturePhoto(with: settings, delegate: self) } func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { if let e = error { print("Error: \(e)") } else if let cgImage = photo.cgImageRepresentation() { let orientation = photo.metadata[kCGImagePropertyOrientation as String] as! NSNumber let imageOrientation = UIImage.Orientation(rawValue: orientation.intValue)! print("orientation: \(orientation), imageOrientation: \(imageOrientation)") let image = UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation) imageViewForOutput.image = image } } } extension UIDeviceOrientation { var asCaptureVideoOrientation: AVCaptureVideoOrientation { switch self { case .landscapeLeft: return .landscapeRight case .landscapeRight: return .landscapeLeft case .portraitUpsideDown: return .portraitUpsideDown default: return .portrait } } }
修复方案
1. 修正设备方向与视频方向的映射逻辑
方向映射需要结合当前使用的摄像头位置(前置/后置)调整,因为前置摄像头的镜像特性会导致方向判断反转。修改UIDeviceOrientation扩展:
extension UIDeviceOrientation { func asCaptureVideoOrientation(for cameraPosition: AVCaptureDevice.Position) -> AVCaptureVideoOrientation { switch (self, cameraPosition) { case (.landscapeLeft, .back): return .landscapeRight case (.landscapeLeft, .front): return .landscapeLeft case (.landscapeRight, .back): return .landscapeLeft case (.landscapeRight, .front): return .landscapeRight case (.portraitUpsideDown, _): return .portraitUpsideDown default: return .portrait } } }
2. 切换摄像头后同步更新视频方向
在flipCamera方法最后调用updateVideoOrientation(),确保切换摄像头后photoOutput的方向同步:
@objc func flipCamera(sender: UIButton) { sender.isSelected = !sender.isSelected whichCameraStuff() updateVideoOrientation() // 新增:切换后更新方向 }
3. 处理前置摄像头拍摄图片的镜像
前置摄像头拍摄的图片需要手动镜像,否则会出现水平翻转。修改didFinishProcessingPhoto方法:
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { if let e = error { print("Error: \(e)") return } guard let cgImage = photo.cgImageRepresentation() else { return } let orientation = photo.metadata[kCGImagePropertyOrientation as String] as! NSNumber let imageOrientation = UIImage.Orientation(rawValue: orientation.intValue)! print("orientation: \(orientation), imageOrientation: \(imageOrientation)") var image = UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation) // 前置摄像头拍摄的图片需要镜像 if flipCameraButton.isSelected { if let mirroredImage = image.withHorizontallyFlippedOrientation() { image = mirroredImage } } imageViewForOutput.image = image }
4. 更新updateVideoOrientation方法
修改该方法,获取当前摄像头位置并传入方向映射:
func updateVideoOrientation() { guard let pl = self.previewLayer, let connection = pl.connection else { return } guard connection.isVideoOrientationSupported else { return } // 获取当前使用的摄像头位置 let currentCameraPosition: AVCaptureDevice.Position = flipCameraButton.isSelected ? .front : .back let videoOrientation = UIDevice.current.orientation.asCaptureVideoOrientation(for: currentCameraPosition) if connection.videoOrientation == videoOrientation { print("no change to videoOrientation") return } print("videoOrientation: \(videoOrientation)") previewLayer?.connection?.videoOrientation = videoOrientation photoOutput.connection(with: AVMediaType.video)?.videoOrientation = videoOrientation previewLayer?.frame = viewForCameraPreview.bounds previewLayer?.removeAllAnimations() }
5. 修复预览层镜像设置
在addCameraPreviewIfNeeded中手动控制预览层的镜像状态,确保前置摄像头预览正确:
func addCameraPreviewIfNeeded(){ DispatchQueue.main.async { if self.previewLayer == nil { self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession) if let pl = self.previewLayer { pl.videoGravity = AVLayerVideoGravity.resizeAspectFill pl.connection?.automaticallyAdjustsVideoMirroring = false // 手动设置前置摄像头预览镜像 pl.connection?.isVideoMirrored = flipCameraButton.isSelected self.viewForCameraPreview.layer.insertSublayer(pl, at: 0) pl.frame = self.viewForCameraPreview.bounds self.updateVideoOrientation() } } else { // 切换摄像头后更新预览镜像 previewLayer?.connection?.isVideoMirrored = flipCameraButton.isSelected } self.previewLayer?.isHidden = false } }
完整修复后的代码
import UIKit import SnapKit import AVFoundation class ViewController: UIViewController, AVCapturePhotoCaptureDelegate { var clickerButton = UIButton() var flipCameraButton = UIButton() var frontCameraDeviceInput: AVCaptureDeviceInput? var backCameraDeviceInput: AVCaptureDeviceInput? let captureSession = AVCaptureSession() let photoOutput = AVCapturePhotoOutput() let viewForCameraPreview = UIView() var previewLayer : AVCaptureVideoPreviewLayer? var imageViewForOutput = UIImageView() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black view.addSubview(viewForCameraPreview) viewForCameraPreview.snp.makeConstraints { make in make.edges.equalToSuperview() } let padding = 15.0 let buttonSize = 70.0 clickerButton.addTarget(self, action: #selector(clickPhoto(sender:)), for: .touchUpInside) view.addSubview(clickerButton) clickerButton.snp.makeConstraints { make in make.size.equalTo(buttonSize) make.centerX.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom).inset(padding) } clickerButton.layer.cornerRadius = buttonSize / 2 clickerButton.clipsToBounds = true clickerButton.layer.borderColor = UIColor.white.cgColor clickerButton.layer.borderWidth = 5 flipCameraButton.addTarget(self, action: #selector(flipCamera(sender:)), for: .touchUpInside) flipCameraButton.setTitle("F", for: .normal) flipCameraButton.titleLabel?.font = UIFont.systemFont(ofSize: 50, weight: .bold) flipCameraButton.setTitleColor(.white, for: .normal) view.addSubview(flipCameraButton) flipCameraButton.snp.makeConstraints { make in make.size.equalTo(buttonSize) make.right.equalTo(clickerButton.snp.left).inset(-padding) make.centerY.equalTo(clickerButton) } imageViewForOutput.layer.borderColor = UIColor.white.cgColor imageViewForOutput.layer.borderWidth = 2 imageViewForOutput.contentMode = .scaleAspectFit view.addSubview(imageViewForOutput) imageViewForOutput.snp.makeConstraints { make in make.size.equalTo(200) make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(padding) make.left.equalTo(view.safeAreaLayoutGuide.snp.left).offset(padding) } configureCameras() whichCameraStuff() startCamera() } @objc func flipCamera(sender: UIButton) { sender.isSelected = !sender.isSelected whichCameraStuff() updateVideoOrientation() // 新增:切换后更新方向 } func configureCameras(){ if let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) { frontCameraDeviceInput = try? AVCaptureDeviceInput(device: frontCamera) } if let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) { backCameraDeviceInput = try? AVCaptureDeviceInput(device: backCamera) } photoOutput.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil) if captureSession.canAddOutput(photoOutput) { captureSession.addOutput(photoOutput) } } func whichCameraStuff(){ captureSession.beginConfiguration() if flipCameraButton.isSelected { if let b = backCameraDeviceInput, captureSession.inputs.contains(b) { captureSession.removeInput(b) } if let f = frontCameraDeviceInput, !captureSession.inputs.contains(f) { captureSession.addInput(f) } } else { if let f = frontCameraDeviceInput, captureSession.inputs.contains(f) { captureSession.removeInput(f) } if let b = backCameraDeviceInput, !captureSession.inputs.contains(b) { captureSession.addInput(b) } } captureSession.commitConfiguration() } func startCamera(){ DispatchQueue.global().async { self.captureSession.startRunning() self.addCameraPreviewIfNeeded() } } func addCameraPreviewIfNeeded(){ DispatchQueue.main.async { if self.previewLayer == nil { self.previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession) if let pl = self.previewLayer { pl.videoGravity =
相关产品推荐
相关产品推荐

