How can I get 'AVCaptureVideoPreviewLayer' to display properly in landscape orientation? It works fine in portrait but doesn't rotate, and shows a rotated camera capture when the parent view controller is in landscape orientation.
First, the answer
- (void)viewWillLayoutSubviews {
_captureVideoPreviewLayer.frame = self.view.bounds;
if (_captureVideoPreviewLayer.connection.supportsVideoOrientation) {
_captureVideoPreviewLayer.connection.videoOrientation = [self interfaceOrientationToVideoOrientation:[UIApplication sharedApplication].statusBarOrientation];
}
}
- (AVCaptureVideoOrientation)interfaceOrientationToVideoOrientation:(UIInterfaceOrientation)orientation {
switch (orientation) {
case UIInterfaceOrientationPortrait:
return AVCaptureVideoOrientationPortrait;
case UIInterfaceOrientationPortraitUpsideDown:
return AVCaptureVideoOrientationPortraitUpsideDown;
case UIInterfaceOrientationLandscapeLeft:
return AVCaptureVideoOrientationLandscapeLeft;
case UIInterfaceOrientationLandscapeRight:
return AVCaptureVideoOrientationLandscapeRight;
default:
break;
}
NSLog(@"Warning - Didn't recognise interface orientation (%d)",orientation);
return AVCaptureVideoOrientationPortrait;
}
I've come across several SO posts on this matter, and couldn't find any simple explanation so I thought I'd share my own.
If you follow Apple's sample on the matter, you'll run into two potential problems when you rotate your iOS device to landscape
- Capture will appear rotated (as if you're holding the camera 90 degrees off)
- It won't span its set bounds entirely
The problem here is that 'CALayer' doesn't support autorotation hence, unlike a 'UIView' you'd add as a subview, it won't rotate when its parent 'UIView' rotates. Therefore, you have to manually update its frame every time the parent view's bounds changes (not parent view's frame since frame stays the same after rotation). This is achieved by overriding 'viewWillLayoutSubviews' in the container view controller.
Secondly, you should use 'videoOrientation' property to inform AVFoundation about the orientation so it does the preview properly.
Hope this helps.
Swift3
func updateVideoOrientation() {
guard let previewLayer = self.previewLayer else {
return
}
guard previewLayer.connection.isVideoOrientationSupported else {
print("isVideoOrientationSupported is false")
return
}
let statusBarOrientation = UIApplication.shared.statusBarOrientation
let videoOrientation: AVCaptureVideoOrientation = statusBarOrientation.videoOrientation ?? .portrait
if previewLayer.connection.videoOrientation == videoOrientation {
print("no change to videoOrientation")
return
}
previewLayer.frame = cameraView.bounds
previewLayer.connection.videoOrientation = videoOrientation
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()
})
})
}
extension UIInterfaceOrientation {
var videoOrientation: AVCaptureVideoOrientation? {
switch self {
case .portraitUpsideDown: return .portraitUpsideDown
case .landscapeRight: return .landscapeRight
case .landscapeLeft: return .landscapeLeft
case .portrait: return .portrait
default: return nil
}
}
}
Swift 5 (iOS 13.0+):
func updateVideoOrientation() {
guard let videoPreviewLayer = self.videoPreviewLayer else {
return
}
guard videoPreviewLayer.connection!.isVideoOrientationSupported else {
print("isVideoOrientationSupported is false")
return
}
let statusBarOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
let videoOrientation: AVCaptureVideoOrientation = statusBarOrientation?.videoOrientation ?? .portrait
videoPreviewLayer.frame = view.layer.bounds
videoPreviewLayer.connection?.videoOrientation = videoOrientation
videoPreviewLayer.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()
})
})
}
extension UIInterfaceOrientation {
var videoOrientation: AVCaptureVideoOrientation? {
switch self {
case .portraitUpsideDown: return .portraitUpsideDown
case .landscapeRight: return .landscapeRight
case .landscapeLeft: return .landscapeLeft
case .portrait: return .portrait
default: return nil
}
}
}
override func viewWillLayoutSubviews() {
self.previewLayer.frame = self.view.bounds
if previewLayer.connection.isVideoOrientationSupported {
self.previewLayer.connection.videoOrientation = self.interfaceOrientation(toVideoOrientation: UIApplication.shared.statusBarOrientation)
}
}
func interfaceOrientation(toVideoOrientation orientation: UIInterfaceOrientation) -> AVCaptureVideoOrientation {
switch orientation {
case .portrait:
return .portrait
case .portraitUpsideDown:
return .portraitUpsideDown
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
default:
break
}
print("Warning - Didn't recognise interface orientation (\(orientation))")
return .portrait
}
//SWIFT 3 CONVERSION
It's not enough just to change AVCaptureVideoPreviewLayer's connection orientation. This will only update the image you see on the screen.
But actual image will still stay the same. You can print output image size in different orientations and check the results.
To make rotation really work you also need to change orientation for AVCaptureSession's connection.
func updateVideoOrientation() {
if let previewLayerConnection = previewLayer.connection, previewLayerConnection.isVideoOrientationSupported {
previewLayerConnection.videoOrientation = currentVideoOrientation
}
if let captureSessionConnection = captureSession.connections.first, captureSessionConnection.isVideoOrientationSupported {
captureSessionConnection.videoOrientation = currentVideoOrientation
}
}
Jacksanford and NeonEye's answers are great. However, I have a quick suggestion.
Because statusBarOrientation
is deprecated in iOS 13, I ended up using
connection.videoOrientation = self.interfaceOrientation(toVideoOrientation: UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .portrait
Besides viewWillLayoutSubviews(), you also have to implement didRotateFromInterfaceOrientation().
This is because the subview layouts won't be updated if the device rotates from portrait mode to portrait upside-down mode (obviously for efficiency reasons).
© 2022 - 2024 — McMap. All rights reserved.