AVCaptureVideoPreviewLayer landscape orientation
Asked Answered
A

7

19

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.

Antilepton answered 21/1, 2014 at 12:37 Comment(0)
A
36

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

  1. Capture will appear rotated (as if you're holding the camera 90 degrees off)
  2. 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.

Antilepton answered 21/1, 2014 at 12:37 Comment(2)
Great ! But I had to implement will- or didRotateToInterfaceOrientation() with a call to self.view.setNeedsLayout() to get viewWillLayoutSubviews calledSestos
Using viewWillLayoutSubviews() messes up sometimes when you rotate the device multiple times (especially 180° rotations). Implementing the same behavior in willRotateToInterfaceOrientation() better solved the problem for me. Thanks anyway ;)Earl
W
11

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
        }
    }
}
Whispering answered 4/12, 2016 at 21:43 Comment(1)
Perfect. Thanks!Roadability
D
9

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
        }
    }
}

Dorinedorion answered 22/6, 2020 at 0:45 Comment(0)
M
6
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

Magdalen answered 10/10, 2016 at 20:42 Comment(1)
This works perfectly except in cases where the camera is not displayed and running by viewDidLoad(), which introduces all sorts of problems because the previewLayer might not exist yet. Because of that, the answer by @Whispering above should be considered correct.Roadability
H
2

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
    }
}
Harwilll answered 14/8, 2020 at 13:23 Comment(0)
C
1

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

Colitis answered 1/8, 2020 at 11:11 Comment(0)
A
0

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).

Arrogance answered 5/8, 2015 at 10:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.