I have created the below code example which reproduces the entire issue.
Note that I have the iOS "Rotation lock" setting off in control centre.
Testing on iPhone 13 running iOS 17.
The image orientation which is being set in imageViewForOutput
is always wrong.
When I take a photo in normal portrait, the image is flipped horizontally (aka the right is appearing on left side and left is appearing on right side). The orientation is being printed as 6.
When I turn my device 90 degree clockwise and take a pic, the image being set is rotated 90 degree anti-clockwise. The orientation is being printed as 3.
When I turn my device 90 degree anti-clockwise and take a pic, the image being set is upside down. The orientation is being printed as 1.
I have no idea what is making such random changes to orientation. I have followed other stack overflow posts and am making sure I am setting the orientation changes on the photoOutput
.
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
}
}
}
I figured out the solution. I used the photo.fileDataRepresentation()
in didFinishProcessingPhoto
and then I didn't have to deal with orientation metadata at all.
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let e = error {
print("Error: \(e)")
} else if let photoData = photo.fileDataRepresentation(), let photoImage = UIImage(data: photoData) {
imageViewForOutput.image = photoImage
}
}
Note that one must make sure the following is present when orientation change occurs OR at least right before a photo is taken. Otherwise it won't work. For me, I had it in my updateVideoOrientation
function previously already:
photoOutput.connection(with: AVMediaType.video)?.videoOrientation = videoOrientation