Disable taking images in any orientation other than Portrait AVFoundation
Asked Answered
G

3

12

I am using AVFoundation to show the camera.

I would like to prevent the camera itself to rotate so the viewer will see the camera only in portrait and the images will be taken only in portrait mode.

I defined Supported Interface Orientation to support portrait only and the view itself is being displayed only in portrait mode, but not the camera - is being rotated with the device orientation

How can I force the AVFoundation camera to be displayed and capture images only in portrait like the UIViewController?

My code to set the camera:

AVCaptureVideoPreviewLayer* lay = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.sess];
UIView *view = [self videoPreviewView];
CALayer *viewLayer = [view layer];
[viewLayer setMasksToBounds:YES];
CGRect bounds = [view bounds];
[lay setFrame:bounds];
if ([lay respondsToSelector:@selector(connection)])
 {
   if ([lay.connection isVideoOrientationSupported])
   {
      [lay.connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
    }
 }
 [lay setVideoGravity:AVLayerVideoGravityResizeAspectFill];
 [viewLayer insertSublayer:lay below:[[viewLayer sublayers] objectAtIndex:0]];
 self.previewLayer = lay;
Guillemette answered 7/2, 2013 at 12:15 Comment(5)
Not sure about your Q. Nothing will force the user not to rotate his iPhone. So you want to allow recording video only when your interface is in portrait? Or better: the device is only in vertical position?Muckraker
No - I would like the camera not to rotate to portrait - the same way that I can force the UiViewcontroller to stay in portrait positionGuillemette
I don't understand the problem. In iOS6, if I set supported interface orientations to portrait only (in target summary), my camera preview stays put along with the rest of the interface, whatever the device orientation. Does yours not do this? Obviously the camera preview image rotates with the device so that the horizon remains level - is that what you want to disable, so the horizon remains fixed as per the portrait view, whatever the orientation?Wennerholn
Yes. This is exactly what I want to doGuillemette
@HeWas Was And also that the taken picture will only be in portraitGuillemette
W
10

Here is a partial answer based on my understanding of your question (which differs from the other answers you have had).

You have the app locked to portrait orientation. So the status bar is always at the portrait top of the phone regardless of the phone's orientation. This successfully locks your interface, including your AVCapture interface. But you want to also lock the raw image feed from the camera so that the image horizon is always parallel with the status bar.

This will ideally need to be done continuously - so that if you have the camera at a 45degree angle the image will be counter-rotated 45 degrees. Otherwise, most of the time, the image will not be aligned correctly (the alternative is that it is always out of line until your 90degree orientation switch updates, which would swivel the image 90 degrees).

To do this you need to use Core Motion and the accelerometer. You want to get angle of the phone's Y-axis to true vertical and rotate the image accordingly. See here for geometry details:

iPhone orientation -- how do I figure out which way is up?

Using Core Motion, trigger this method from viewDidLoad

- (void)startAccelerometerUpdates {
    self.coreMotionManager = [[CMMotionManager alloc] init];
    if ([self.coreMotionManager isAccelerometerAvailable] == YES) {
        CGFloat updateInterval = 0.1;
            // Assign the update interval to the motion manager
        [self.coreMotionManager setAccelerometerUpdateInterval:updateInterval];
        [self.coreMotionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] 
           withHandler: ^(CMAccelerometerData *accelerometerData, NSError *error) {
             CGFloat angle =  -atan2( accelerometerData.acceleration.x, 
                                      accelerometerData.acceleration.y) 
                               + M_PI ;
             CATransform3D rotate = CATransform3DMakeRotation(angle, 0, 0, 1);
             self.previewLayer.transform = rotate;

         }];
    }
}

a upright b 45deg c 90deg

phone held (a) portrait; (b) rotated ~30deg; (c) landscape
.

You may find this is a little jumpy, and there is a bit of a lag between the device movement and the view. You can play with the updateInterval, and get in deeper with other Core Motion trickery to dampen the movement. (I have not treated the case of the phone being exactly upside down, and if you hold the camera face down or face up, the result is undefined fixed with updated code/ use of atan2).

Now orientation is reasonably correct, but your image does not fit your view. There is not a lot you can do about this as the format of the raw camera feed is fixed by the physical dimensions of it's sensor array. The workaround is to zoom the image so that you have enough excess image data at all angles to enable you to crop the image to fit the portrait format you want.

Either in Interface Builder:

  • set your previewLayer's view to square centered on it's superview, with width and height equal to the diagonal of the visible image area (sqrt (width2+height2)

Or in code:

- (void)resizeCameraView
{
    CGSize size = self. videoPreviewView.bounds.size;
    CGFloat diagonal = sqrt(pow(size.width,2)+pow(size.height,2));
    diagonal = 2*ceil(diagonal/2);  //rounding
    self.videoPreviewView.bounds = (CGRect){0,0,diagonal,diagonal};
}

If you do this in code, resizeCameraView should work if you call it from your viewDidLoad. Make sure that self.videoPreviewView is your IBOutlet reference to the correct view.

Now when you take a photo, you will capture the whole of the 'raw' image data from the camera's array, which will be in landscape format. It will be saved with an orientation flag for display rotation. But what you may want is to save the photo as seen onscreen. This means that you will have to rotate and crop the photo to match your onscreen view before saving it, and remove it's orientation metadata. That's for you to work out (the other part of the 'partial answer'): I suspect you might decide that this whole approach doesn't get you what you want (I think what you'd really like is a camera sensor that hardware-rotates against the rotation of the device to keep the horizon stable).

update
changed startAccelerometerUpdates to get angle from atan2 instead of acos, smoother and takes account of all directions without fiddling

update 2
From your comments, it seems your rotated preview layer is getting stuck? I cannot replicate your error, it must be some other place in your code or settings.

So that you can check with clean code, I have added my solution into Apple's AVCam project, so you can check it against that. Here is what to do:

  • add the Core Motion framework to AVCam.

In AVCamViewController.m

  • #import <CoreMotion/CoreMotion.h>
  • add my startAccelerometerUpdates method
  • add my resizeCameraView method (stick both of these methods near the top of the class file or you may get confused, there are more than one @implementations in that file)
  • add the line: [self resizeCameraView]; to viewDidLoad (it can be the first line of the method)
  • add the property
    @property (strong, nonatomic) CMMotionManager* coreMotionManager
    to the @interface (it doesn't need to be a property, but my method assumes it exists, so if you don't add it you will have to modify my method instead).

In startAccelerometerUpdates change this line:

    self.previewLayer.transform = rotate;  

to:

    self.captureVideoPreviewLayer.transform = rotate;

also, in the Objects list in AVCamViewController.xib, move the videoPreview View above the ToolBar (otherwise when you enlarge it you cover the controls)

Be sure to disable rotations - for iOS<6.0, that is already true, but for 6.0+ you need to select just portrait in supported orientations in the target summary.

I think that is a complete list of changes I made to AVCam, and the rotation/orientation is all working very well. I suggest you try doing the same. If you can get this to work smoothly, you know there is some other glitch in your code somewhere. If you still find your rotations stick, I would be curious to know more about your hardware and software environment such as which devices are you testing on.

I am compiling on XCode 4.6/OSX10.8.2, and testing on:

- iPhone4S  /  iOS5.1  
- iPhone3G  /  iOS6.1  
- iPad mini /  iOS6.1 

All results are smooth and accurate.

Wennerholn answered 12/2, 2013 at 3:39 Comment(7)
I tried your solution again. I updated my answer with how my screen looks like after trying it, and when holding the iphone rotated to right but straight. I didn't try it on AVCAM, but I don't have much on my code to prevent itGuillemette
@Odelya, it looks like you just didn't enlarge the camera view (the resizeCameraView method isn't getting called correctly).Wennerholn
If the phone is not rotated to right when I hold it, it does fill the square. Is it something that I have to call via notifications (it happens only if I rotate to right)Guillemette
@Odelya, in resizeCameraView did you change the property name in my code - self.cameraPreview - to the one your code uses - self.videoPreviewView?Wennerholn
You mean to replace my code from the question that sets the image with the resizeCameraView method?Guillemette
@Odelya - no, don't replace your code, which is fine. Call resizeCameraView just before your code, that should work (or, as I say, you could call it as the first thing in ViewDidLoad). It is just enlarging the view so that you don't get the edges cropped as you rotate.Wennerholn
@Odelya - and make sure that your videoPreviewView is BEHIND your camera controls in your IB layout - otherwise it will cover them up when you enlarge it.Wennerholn
K
2

I guess you need to use this method to restrict the camera rotation.

AVCaptureConnection *videoConnection = [CameraVC connectionWithMediaType:AVMediaTypeVideo fromConnections:[imageCaptureOutput connections]];
if ([videoConnection isVideoOrientationSupported])
{
    [videoConnection setVideoOrientation:[UIDevice currentDevice].orientation];
}

Assuming your preview layer is defined as property, can use

[self.previewLayer setOrientation:[[UIDevice currentDevice] orientation]];

In your case you can replace [[UIDevice currentDevice] orientation] by UIInterfaceOrientationPortrait

edited

Try to add the preview layer when you actually need it. Example

preview = [[self videoPreviewWithFrame:CGRectMake(0, 0, 320, 480)] retain];
[self.view addSubview:preview];

The videoPreviewWithFrame function.

- (UIView *) videoPreviewWithFrame:(CGRect) frame 
{
  AVCaptureVideoPreviewLayer *tempPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:[self captureSession]];
  [tempPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
  tempPreviewLayer.frame = frame;

  UIView* tempView = [[UIView alloc] init];
  [tempView.layer addSublayer:tempPreviewLayer];
  tempView.frame = frame;

  [tempPreviewLayer autorelease];
  [tempView autorelease];
  return tempView;
}
Kakaaba answered 10/2, 2013 at 5:29 Comment(7)
I edited my question. 1. I already set the orientation to Portrait in the video connection. Regarding the preview layer - setOrientation is deprecated, so I need to write: self.previewLayer.connection which is the same as I am already doing no?Guillemette
I have edited my answer. Try to implement which I have written after the edited line.Kakaaba
if ([captureVideoPreviewLayer respondsToSelector:@selector(connection)]) { if ([captureVideoPreviewLayer.connection isVideoOrientationSupported]) { [captureVideoPreviewLayer.connection setVideoOrientation:self.interfaceOrientation]; } } else { // Deprecated in 6.0; here for backward compatibility if ([captureVideoPreviewLayer isOrientationSupported]) { [captureVideoPreviewLayer setOrientation:self.interfaceOrientation]; } }Kakaaba
Your setting videoOrientation which is only in iOS6 so for backward compatibility use the setOrientation in the else part.Kakaaba
Not able to understand as to what is not working. It would be easy to understand if you upload few screenshots. Did you try with imagePicker? By default imagePicker stays in portrait so I guess it can help you.Kakaaba
See He Was comment on my question - I wan't to disable the camera to rotate when the user rotates the deviceGuillemette
PreviewLayer.orientation = AVCaptureVideoOrientationPortrait works like gem but is deprecated in iOS6. Don't know the work around. You can use this and try. I tried and its working.Kakaaba
A
2

Assuming your previewlayer is added to a viewcontroller view. Do this in viewDidLoad :

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

and define the selector as:

- (void)orientationChanged:(NSNotification*)notification {
    UIInterfaceOrientation interfaceOrientation = [UIApplication sharedApplication].statusBarOrientation;
    if ([self.previewlayer respondsToSelector:@selector(orientation)]) {
        //for iOS5
        if (interfaceOrientation != UIInterfaceOrientationPortrait) {
            self.previewlayer.orientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
        }
    } else {
        //for iOS6
        if (interfaceOrientation != UIInterfaceOrientationPortrait) {
            self.previewlayer.connection.videoOrientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
        }
    }
}

Note: put tempPreviewLayer in the property self.previewlayer .

This will force the preview layer to portrait position when the device orientation changes.

EDIT

you can also add this in ur 'shouldAutoRotate` method of the viewController

    - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
    {
        // Return YES for supported orientations
        if ([self.previewlayer respondsToSelector:@selector(orientation)]) {
            if (interfaceOrientation != UIInterfaceOrientationPortrait) {
                self.previewlayer.orientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
            }
        } else {
            if (interfaceOrientation != UIInterfaceOrientationPortrait) {
                self.previewlayer.connection.videoOrientation = (AVCaptureVideoOrientation)UIInterfaceOrientationPortrait;
            }
        }

        return (interfaceOrientation == UIInterfaceOrientationPortrait);
    }

for ios6 over ride these two and check.

-(NSUInteger)supportedInterfaceOrientations {
    //UIInterfaceOrientation interfaceOrientation = [UIApplication sharedApplication].statusBarOrientation;
    //return (
    //interfaceOrientation == UIInterfaceOrientationLandscapeLeft |
    //interfaceOrientation == UIInterfaceOrientationLandscapeRight);
    return UIInterfaceOrientationMaskLandscape;//(UIInterfaceOrientationLandscapeLeft | UIInterfaceOrientationLandscapeRight);
}

- (BOOL)shouldAutorotate {
    return YES;
}

before return in these two methods apend the code ..and in the notification that i gave, see if its called when you roate the device.

Adenosine answered 11/2, 2013 at 10:10 Comment(8)
did you try it? add [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; before adding the notification. This works perfectly for me, it locks the preview layer in the orientation i want.Adenosine
what is the note to put tempPreviewLayer in the property? what do you mean by that?Guillemette
take a property @property (nonatomic, retain) AVCaptureVideoPreviewLayer *previewlayer; and do self.previewlayer = tempPreviewLayer in your videoPreviewWithFrame method after the line tempPreviewLayer.frame = frame; and then onwards use self.previewlayer evrywhereAdenosine
And yes - I tried the code the method is being called, but the previewLayer is not locked in portrait.I am using iOS6Guillemette
self.previewlayer = tempPreviewLayer? what is the tempPreviewLayer?Guillemette
tempPreviewLayer is the one in ur example code. if its in iOS6 then over ride the two new rotation api's and checkAdenosine
let us continue this discussion in chatAdenosine
AVCaptureConnection *videoConnection = [CameraVC connectionWithMediaType:AVMediaTypeVideo fromConnections:[imageCaptureOutput connections]]; if ([videoConnection isVideoOrientationSupported]) { [videoConnection setVideoOrientation:[UIDevice currentDevice].orientation]; } was not getting recognized until u give it after specifying output setting. Try to set output settings and then add code.Kakaaba

© 2022 - 2024 — McMap. All rights reserved.