我创建了下面的代码示例,它重现了整个问题。
请注意,我在控制中心设置了 iOS“旋转锁定”。
在运行 iOS 17 的 iPhone 13 上进行测试。
imageViewForOutput
中设置的图像方向总是错误的。
当我以普通肖像拍摄照片时,图像会水平翻转(即右侧出现在左侧,左侧出现在右侧)。方向打印为 6。
当我将设备顺时针旋转 90 度并拍照时,正在设置的图像逆时针旋转 90 度。方向打印为 3。
当我将设备逆时针旋转 90 度拍照时,设置的图像是上下颠倒的。方向打印为 1。
我不知道是什么使方向发生如此随机的变化。我关注了其他堆栈溢出帖子,并确保我在
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
}
}
}
我找到了解决办法。我在
photo.fileDataRepresentation()
中使用了 didFinishProcessingPhoto
,然后我根本不需要处理方向元数据。
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
}
}
请注意,当发生方向变化时或至少在拍摄照片之前,必须确保存在以下内容。否则就行不通。对我来说,我之前已经在我的
updateVideoOrientation
函数中使用了它:
photoOutput.connection(with: AVMediaType.video)?.videoOrientation = videoOrientation