Quick 180 rotation of iOS Device results in the camera viewing upside-down
Asked Answered
M

2

12

I've implemented the code below to change the orientation of an AVCaptureVideoSession based upon the UIInterfaceOrientation:

- (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 (%ld)",(long)orientation);
    return AVCaptureVideoOrientationPortrait;
}

This code works almost perfectly. However, one problem that occurs is that if you quickly rotate the iOS device 180 degrees, the camera will be shown upside down.

Here's what the camera view looks like before the rotation:

enter image description here

And here's what it looks like after the rotation:

enter image description here

Additionally, here is my implementation of viewDidLayoutSubviews:

- (void)viewDidLayoutSubviews {

    [super viewDidLayoutSubviews];

    previewLayer.frame = self.view.bounds;

    self.view.translatesAutoresizingMaskIntoConstraints = NO;
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    [self.view.layer addSublayer:previewLayer];

    if (previewLayer.connection.supportsVideoOrientation) {
        previewLayer.connection.videoOrientation = [self interfaceOrientationToVideoOrientation:[UIApplication sharedApplication].statusBarOrientation];
    }
}

Does anyone have an idea of why the camera view would be upside down when this 180-degree rotation occurs?

Mountfort answered 17/6, 2015 at 20:45 Comment(8)
I've seen this in Apple tools so I'm not sure it is your bug.Shumpert
@PaulCezanne Ah, interesting! Which ones? I just tried to reproduce it on the camera app and wasn't able to do so.Mountfort
I've seen it in camera and Messages, but maybe I launched Camera from Messages. Can't recall. I also think it happened more in iOS8.0, don't recall seeing it from Camera recently, but I certainly have seen rotational weirdness in 8.3 when launch the camera from Messages.Shumpert
@PaulCezanne Weird...I just tried testing that by launching the camera from Messages and didn't see it there, either.Mountfort
Does anyone have an idea? Research has turned up nothing so far :/Mountfort
I can't help but a 100 pt bounty might do it. That has helped for me in the past, but not always. Sometimes the questions are too obscure...Shumpert
I had the same issue before, solution that i made and work to me was adding NSNotificationCenter, the changing the orientation of the previewLayer when notification was triggered directly in the method targeted by the notification.. i can't give a code right now it's too late here and my code is in the office.. hmm..Duad
@Duad Well if you have the chance to take a look tomorrow, do let me know!Mountfort
D
4

I had the same issue before here what solved my issue.

I declared a global variable:

@interface YourViewController ()
{
    ...
    UIDevice *iOSDevice;
}

..

@end

Under implementation (.m file) i declared NSNotification observer under -viewWillAppear like:

@implementation YourViewController

-(void)viewWillAppear:(BOOL)animated
{
    [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

    [[NSNotificationCenter defaultCenter] addObserver:self  selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification  object:[UIDevice currentDevice]];

}

-(void)viewWillDisappear:(BOOL)animated
{
    [[NSNotificationCenter defaultCenter]removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
}

- (void)orientationChanged:(NSNotification *)notification
{
    iOSDevice = notification.object;

    previewLayer.connection.videoOrientation = [self interfaceOrientationToVideoOrientation];

    //or

    [self setAutoVideoConnectionOrientation:YES];

}

I made a function that returns the orientation of the copied current device like:

Take a loot at my -interfaceOrientationToVideoOrientation method, it converts the iOSDevice.orientation into AVCaptureVideoOrientation same with what you have there..


(In my case i needed this functions below, but take a look at it might be useful to you for some reason)

- (AVCaptureVideoOrientation)interfaceOrientationToVideoOrientation
{
    switch (iOSDevice.orientation) {

        case UIInterfaceOrientationPortraitUpsideDown:

            return AVCaptureVideoOrientationPortraitUpsideDown;

        case UIInterfaceOrientationLandscapeLeft:

            return AVCaptureVideoOrientationLandscapeLeft;

        case UIInterfaceOrientationLandscapeRight:

            return AVCaptureVideoOrientationLandscapeRight;

        default:
        case UIDeviceOrientationPortrait:
            return AVCaptureVideoOrientationPortrait;
    }
}

- (AVCaptureConnection *)setAutoVideoConnectionOrientation:(BOOL)status
{
    AVCaptureConnection *videoConnection = nil;

    //This is for me
    //for (AVCaptureConnection *connection in stillImageOutput.connections) {

    //This might be for you
    for (AVCaptureConnection *connection in previewLayer.connection) {

        for (AVCaptureInputPort *port in [connection inputPorts])
        {
            if ([[port mediaType] isEqual:AVMediaTypeVideo])
            {
                videoConnection = connection;

                if (status == YES)
                {
                    [videoConnection setVideoOrientation:[self interfaceOrientationToVideoOrientation]];
                }
                else
                {
                    [videoConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
                }
            }
            if (videoConnection)
            {
                break;
            }
        }
    }
    return videoConnection;
}

Edit

For default orientation: You need to check the "current" orientation.

- (void)viewDidLoad
{
    [super viewDidLoad];

    // assuming you finish setting the `previewLayer`

    ..
    // after all of that code, when the view is ready for displaying
    // if you implemented this approach the best thing to do is:

    // set this 
    iOSDevice = [UIDevice currentDevice];

    // then call 
    previewLayer.connection.videoOrientation = [self interfaceOrientationToVideoOrientation];
    // to update the `previewLayer.connection.videoOrientation ` for default orientation        

}

Note:

Using NSNotificationCenter let the rotation of the device to be settled first before triggered..

Hope this have help you..

Duad answered 24/6, 2015 at 2:50 Comment(5)
This works almost perfectly! The only issue I now have is that if I run the app on an iPad and start off in landscape mode, the camera view will be displayed in landscape. If I rotate the device to portrait and then back to landscape, the camera will then display correctly.Mountfort
@narner, About that, you need to check the orientation inside -viewDidLoad.. for the default setting.. sorry i didn't include that here.. i thought you already have that one..Duad
That makes sense. So, something like ` previewLayer.connection.videoOrientation = [[UIDevice currentDevice]orientation];` ?Mountfort
Not exactly you need the converted orientation inside -interfaceOrientationToVideoOrientation method.. i'll try editing my answer.. :)Duad
@narner: there you go, try it.. :)Duad
E
5

I believe it's happening because viewDidLayoutSubviews doesn't get called when the view changes from Portrait orientation to UpsideDown. When you rotate very quickly it goes from one to another straight. If you rotates slowly it pass through a Landscape orientation and so viewDidLayoutSubviews gets called and subsequently your method.

I advise you to use one of the methods that get called when orientation changes. I know Apple advise to use them less and less because they want views to change when size changes, but there are cases, like this one when you really need to know that the orientation changed.

For example you could register to receive this notifications: UIApplicationDidChangeStatusBarOrientationNotification

Exiguous answered 23/6, 2015 at 14:24 Comment(5)
I see, that would make sense. So you're suggesting putting the code I currently have in viewDidLayoutSubviews inside of something like - (void)orientationChanged:(NSNotification *)notification or adjustViewsForOrientation, correct?Mountfort
I would say that you only need to put the last if in orientationChanged:. That rest of the code that handle resizing of the view is very well put in the viewDidLayoutSubviews. I don't think though that you should add that sublayer every time the method is called.Exiguous
That also makes sense. Where do you think would be a better place to add that sublayer? (Sorry for the questions, this has been hurting my head for awhile, haha).Mountfort
You could probably check whether it has a superlayer, and add it only if it doesn't.Exiguous
For Swift 3.0 - NotificationCenter.default.addObserver(self, selector: #selector(deviceRotated(notification:)), name: NSNotification.Name.UIApplicationDidChangeStatusBarOrientation, object: .none)Amphoteric
D
4

I had the same issue before here what solved my issue.

I declared a global variable:

@interface YourViewController ()
{
    ...
    UIDevice *iOSDevice;
}

..

@end

Under implementation (.m file) i declared NSNotification observer under -viewWillAppear like:

@implementation YourViewController

-(void)viewWillAppear:(BOOL)animated
{
    [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];

    [[NSNotificationCenter defaultCenter] addObserver:self  selector:@selector(orientationChanged:) name:UIDeviceOrientationDidChangeNotification  object:[UIDevice currentDevice]];

}

-(void)viewWillDisappear:(BOOL)animated
{
    [[NSNotificationCenter defaultCenter]removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil];
}

- (void)orientationChanged:(NSNotification *)notification
{
    iOSDevice = notification.object;

    previewLayer.connection.videoOrientation = [self interfaceOrientationToVideoOrientation];

    //or

    [self setAutoVideoConnectionOrientation:YES];

}

I made a function that returns the orientation of the copied current device like:

Take a loot at my -interfaceOrientationToVideoOrientation method, it converts the iOSDevice.orientation into AVCaptureVideoOrientation same with what you have there..


(In my case i needed this functions below, but take a look at it might be useful to you for some reason)

- (AVCaptureVideoOrientation)interfaceOrientationToVideoOrientation
{
    switch (iOSDevice.orientation) {

        case UIInterfaceOrientationPortraitUpsideDown:

            return AVCaptureVideoOrientationPortraitUpsideDown;

        case UIInterfaceOrientationLandscapeLeft:

            return AVCaptureVideoOrientationLandscapeLeft;

        case UIInterfaceOrientationLandscapeRight:

            return AVCaptureVideoOrientationLandscapeRight;

        default:
        case UIDeviceOrientationPortrait:
            return AVCaptureVideoOrientationPortrait;
    }
}

- (AVCaptureConnection *)setAutoVideoConnectionOrientation:(BOOL)status
{
    AVCaptureConnection *videoConnection = nil;

    //This is for me
    //for (AVCaptureConnection *connection in stillImageOutput.connections) {

    //This might be for you
    for (AVCaptureConnection *connection in previewLayer.connection) {

        for (AVCaptureInputPort *port in [connection inputPorts])
        {
            if ([[port mediaType] isEqual:AVMediaTypeVideo])
            {
                videoConnection = connection;

                if (status == YES)
                {
                    [videoConnection setVideoOrientation:[self interfaceOrientationToVideoOrientation]];
                }
                else
                {
                    [videoConnection setVideoOrientation:AVCaptureVideoOrientationPortrait];
                }
            }
            if (videoConnection)
            {
                break;
            }
        }
    }
    return videoConnection;
}

Edit

For default orientation: You need to check the "current" orientation.

- (void)viewDidLoad
{
    [super viewDidLoad];

    // assuming you finish setting the `previewLayer`

    ..
    // after all of that code, when the view is ready for displaying
    // if you implemented this approach the best thing to do is:

    // set this 
    iOSDevice = [UIDevice currentDevice];

    // then call 
    previewLayer.connection.videoOrientation = [self interfaceOrientationToVideoOrientation];
    // to update the `previewLayer.connection.videoOrientation ` for default orientation        

}

Note:

Using NSNotificationCenter let the rotation of the device to be settled first before triggered..

Hope this have help you..

Duad answered 24/6, 2015 at 2:50 Comment(5)
This works almost perfectly! The only issue I now have is that if I run the app on an iPad and start off in landscape mode, the camera view will be displayed in landscape. If I rotate the device to portrait and then back to landscape, the camera will then display correctly.Mountfort
@narner, About that, you need to check the orientation inside -viewDidLoad.. for the default setting.. sorry i didn't include that here.. i thought you already have that one..Duad
That makes sense. So, something like ` previewLayer.connection.videoOrientation = [[UIDevice currentDevice]orientation];` ?Mountfort
Not exactly you need the converted orientation inside -interfaceOrientationToVideoOrientation method.. i'll try editing my answer.. :)Duad
@narner: there you go, try it.. :)Duad

© 2022 - 2024 — McMap. All rights reserved.