You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 =
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 13:20:06