I have an iPhone camera app in which I use the AVCaptureVideoPreviewLayer. When someone takes a picture/stillImage this is shown in a new ViewController. The magic happens once this view controller is dismissed.
The whole app rotates properly, both viewcontroller; but for one exception. If I take a picture in one orientation, rotate the device in the new ViewController and then go back to the AVCaptureVideoPreviewLayer the whole layer rotates along just fine, except for the image, so suddenly the input is presented sideways through the previewLayer.
I have checked, and tried setting the frame for the PreviewLayer, but this all seems fine, the values I see when debugging are all correct. It is just the image that is displayed that is skewed. Rotating the device back and forth fixes this issue during use.
Has anyone seen this before, and does anyone have a clue how to fix this?
You could change the previewLayer's connection's videoOrientation when the interface rotates
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
self.previewLayer.connection.videoOrientation = self.interfaceOrientation;
}
Since didRotateFromInterfaceOrientation: is deprecated in iOS 8, I change the video orientation of the connection in viewDidLayoutSubviews
switch ([UIApplication sharedApplication].statusBarOrientation) {
case UIInterfaceOrientationPortrait:
self.captureVideoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortrait;
break;
case UIInterfaceOrientationPortraitUpsideDown:
self.captureVideoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
break;
case UIInterfaceOrientationLandscapeLeft:
self.captureVideoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
break;
case UIInterfaceOrientationLandscapeRight:
self.captureVideoPreviewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
break;
default:
break;
}
I had a similar problem in the past.
Once dismissed camera controller you could refresh view rotation with this:
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIView *topView = [window.subviews objectAtIndex:0];
[topView removeFromSuperview];
[window addSubview:topView];
So, removing top most view on shown window and resetting it on window, it should refresh view rotation properly.
Hope it helps
Maybe, instead of changing AVCaptureVideoPreviewLayer frame, you could try to put it in a container UIView, and set this container's frame. (I fixed a 'sizing' bug on ÀVCaptureVideoPreviewLayerthis way : sizing directly PreviewLayer had no effect, but sizing its containingUIView` worked).
Or,
maybe you can force orientation in your viewController viewWillAppear (or viewDidAppear ?)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// try and force current orientation
UIApplication *application = [UIApplication sharedApplication];
UIInterfaceOrientation currentOrientation = application.statusBarOrientation;
[application setStatusBarOrientation:currentOrientation animated:animated];
}
Good luck !
Since the interfaceOrientation is deprecated, the up to date swift 2.0 solution shall be:
let previewLayerConnection = self.previewLayer.connection
let orientation = AVCaptureVideoOrientation(rawValue: UIApplication.sharedApplication().statusBarOrientation.rawValue)
previewLayerConnection.videoOrientation = orientation!
Related
I have a AVCaptureVideoPreviewLayer instance added to a view controller view hierarchy.
- (void) loadView {
...
self.previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:nil];
self.previewLayer.frame = self.view.bounds;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[self.view.layer addSublayer: _previewLayer];
// adding other UI elements
...
}
...
- (void) _setupPreviewLayerWithSession: (AVCaptureSession*) captureSession
{
self.previewLayer.session = self.captureManager.captureSession;
self.previewLayer.connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
}
The layer frame is updated in -viewDidLayoutSubviews method. View controller orientation is locked to UIInterfaceOrientationMaskLandscapeRight.
The issue is as following:
the device is held in landscape orientation
the view controller is presented modally - video layer displays correctly.
the device is then locked and while it's locked the device is rotated to portrait orientation.
the device is then unlocked while still being in portrait orientation and for several seconds the video layer is displayed rotated 90 degrees. However, the frame for the video layer is correct. All other UI elements display correctly. After several seconds the layer snaps to correct orientation.
Please find the bounds for the layer and UI elements below
I've tried updating the video layer orientation as following (with no results):
subscribing to AVCaptureSessionDidStartRunningNotification and UIApplicationDidBecomeActiveNotification notifications
calling the update when -viewWillTransitionToSize:withTransitionCoordinator: method is called
on -viewWillAppear:
The issue doesn't seem to be connected to video layer orientation itself, but more to view hierarchy layout.
UPDATE:
As suggested, I also tried updating the video layer orientation on device orientation change which didn't help.
I also noticed that the issue mostly happens after the application is launched and the screen is presented for the first time. On subsequent screen presentations during the same session, the reproduce rate for the issue is really low (something like 1/20).
Try this code:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
-(void)orientationChanged:(NSNotification *)notif {
[_videoPreviewLayer setFrame:_viewPreview.layer.bounds];
if (_videoPreviewLayer.connection.supportsVideoOrientation) {
_videoPreviewLayer.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 have got an issue regarding video capture orientation. first of all its about voip app which uses pjsip which can transmit video. the video is being captured using AVCapture frame. so the issue arises while device orientation changes then i have to set the avcapture orientation as well.
for example:
capConnection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
it works fine but i have repeat image part.
so the question is how to get rid of this repeated image part. i have tried this solution but it keeps crashing on vImageRotate90_ARGB8888 any idea how resolve this issue?
in order to try this yourself out, you can get PJSIP version 2.3 video with sample project compile and run it against a test SIP server.
edit: the preview layer is rotated and scaled fine. the particular issue happens on receiving RTP (video) stream when that device rotates and sends images with repeated edges. for instance, if iPadA(horizontal) starts video call with iPadB(horizontal) the image is fine and not repeated edges. but if the iPadA rotates to vertical then iPadB gets this repeated edge images. notice on rotation the capture connection orientation is set to current device orientation.
note the the preview layer has AVLayerVideoGravityResize but that does not affect the outgoing video stream.
The two key pieces are setting the autoresizingMask in viewDidLoad and adjusting your captureVideoPreviewLayer.frame to self.view.layer.bounds; in willAnimateRotationToInterfaceOrientation.
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
//
// react to device orientation notifications
//
[[NSNotificationCenter defaultCenter] addObserver : self
selector : #selector(deviceOrientationDidChange:)
name : UIDeviceOrientationDidChangeNotification
object : nil];
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
}
- (void) willAnimateRotationToInterfaceOrientation : (UIInterfaceOrientation)toInterfaceOrientation
duration : (NSTimeInterval)duration
{
captureVideoPreviewLayer.frame = self.view.layer.bounds;
[[captureVideoPreviewLayer connection] setVideoOrientation:toInterfaceOrientation];
}
- (void)deviceOrientationDidChange: (NSNotification*)notification
{
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
switch (orientation)
{
case UIDeviceOrientationPortrait:
case UIDeviceOrientationPortraitUpsideDown:
case UIDeviceOrientationLandscapeLeft:
case UIDeviceOrientationLandscapeRight:
currentDeviceOrientation = orientation;
break;
// unsupported?
case UIDeviceOrientationFaceUp:
case UIDeviceOrientationFaceDown:
default:
break;
}
}
I have downloaded a simpleCamera view from Cocoa Controls which use this method
- (AVCaptureVideoOrientation)orientationForConnection
{
AVCaptureVideoOrientation videoOrientation = AVCaptureVideoOrientationPortrait;
switch (self.interfaceOrientation) {
case UIInterfaceOrientationLandscapeLeft:
videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
break;
case UIInterfaceOrientationLandscapeRight:
videoOrientation = AVCaptureVideoOrientationLandscapeRight;
break;
case UIInterfaceOrientationPortraitUpsideDown:
videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
break;
default:
videoOrientation = AVCaptureVideoOrientationPortrait;
break;
}
return videoOrientation;
}
the problem is "interfaceOrientation" deprecated in iOS 8 but i dont know what and how to replace this in Switch condition.
Getting the device orientation is not correct. For instance, the device might be in landscape mode, but a view controller that only supports portrait will remain in portrait.
Instead, use this:
[[UIApplication sharedApplication] statusBarOrientation]
Another bonus is that it still uses the UIInterfaceOrientation enum, so very little of your code needs to change.
Since iOS8, Apple recommends to use TraitCollections (Size Classes) instead of interfaceOrientation.
Moreover, since iOS 9 and the new iPad feature "Multitasking", there are some cases where the device orientation doesn't fit with the window proportions! (breaking your application UI)
So you should also be very careful when using Regular Size Classes because it will not necessary take up the whole iPad screen.
Sometimes TraitCollections doesn't fill all your design needs. For those cases, Apple recommends to compare view's bounds :
if view.bounds.size.width > view.bounds.size.height {
// ...
}
I was quite surprised, but you can check on the WWDC 2015 video Getting Started with Multitasking on iPad in iOS 9 at 21'15.
Maybe you're really looking for the device orientation and not for the screen or window proportions. If you care about the device camera, you should not use TraitCollection, neither view bounds.
Swift 4+ version
UIApplication.shared.statusBarOrientation
Using -orientation property of UIDevice is not correct (even if it could work in most of cases) and could lead to some bugs, for instance UIDeviceOrientation consider also the orientation of the device if it is face up or down, there is no pair in UIInterfaceOrientation enum for those values.
Furthermore, if you lock your app in some particular orientation, UIDevice will give you the device orientation without taking that into account.
On the other side iOS8 has deprecated the interfaceOrientation property on UIViewController class.
There are 2 options available to detect the interface orientation:
Use the status bar orientation
Use size classes, on iPhone if they are not overridden they could give you a way to understand the current interface orientation
What is still missing is a way to understand the direction of a change of interface orientation, that is very important during animations. In the session of WWDC 2014 "View controller advancement in iOS8" the speaker provides a solution to that problem too, using the method that replaces -will/DidRotateToInterfaceOrientation.
Here the proposed solution partially implemented:
-(void) viewWillTransitionToSize:(CGSize)s withTransitionCoordinator:(UIVCTC)t {
orientation = [self orientationFromTransform: [t targetTransform]];
oldOrientation = [[UIApplication sharedApplication] statusBarOrientation];
[self myWillRotateToInterfaceOrientation:orientation duration: duration];
[t animateAlongsideTransition:^(id <UIVCTCContext>) {
[self myWillAnimateRotationToInterfaceOrientation:orientation
duration:duration];
}
completion: ^(id <UIVCTCContext>) {
[self myDidAnimateFromInterfaceOrientation:oldOrientation];
}];
}
When you use UIApplication.shared.statusBarOrientation, Xcode 11 will show the warning 'statusBarOrientation' was deprecated in iOS 13.0: Use the interfaceOrientation property of the window scene instead.
This answer is in Swift 5, and supports both iOS 13 and lower versions. It assumes the following:
You will create only one scene, and never more than one
You wish to contain your code to a specific view
You have a view that has a member variable previewLayer of type AVCaptureVideoPreviewLayer
First, in your SceneDelegate.swift, add the following lines:
static var lastCreatedScene: UIWindowScene?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let newScene = (scene as? UIWindowScene) else { return }
Self.lastCreatedScene = newScene
}
Then in your view, add the following function and call it in layoutSubviews():
func refreshOrientation() {
let videoOrientation: AVCaptureVideoOrientation
let statusBarOrientation: UIInterfaceOrientation
if #available(iOS 13, *) {
statusBarOrientation = SceneDelegate.lastCreatedScene?.interfaceOrientation ?? .portrait
} else {
statusBarOrientation = UIApplication.shared.statusBarOrientation
}
switch statusBarOrientation {
case .unknown:
videoOrientation = .portrait
case .portrait:
videoOrientation = .portrait
case .portraitUpsideDown:
videoOrientation = .portraitUpsideDown
case .landscapeLeft:
videoOrientation = .landscapeLeft
case .landscapeRight:
videoOrientation = .landscapeRight
#unknown default:
videoOrientation = .portrait
}
self.previewLayer.connection?.videoOrientation = videoOrientation
}
I created a UIViewController (based on How to switch views when rotating) to switch between 2 views when the device rotates. Each view is "specialized" for a particular orientation.
It uses the UIDeviceOrientationDidChangeNotification notification to switch views:
-(void) deviceDidRotate: (NSNotification *) aNotification{
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
NSLog(#"Device rotated to %d!", orientation);
if ((orientation == UIDeviceOrientationPortrait) ||
(orientation == UIDeviceOrientationPortraitUpsideDown)) {
[self displayView:self.portraitViewController.view];
}else if ((orientation == UIDeviceOrientationLandscapeLeft) ||
(orientation == UIDeviceOrientationLandscapeRight)) {
[self displayView:self.landscapeViewController.view];
}
}
and sort of works. The problems shows up when I rotate to Landscape and then back to Portrait. When going back to portrait the subviews aren't displayed in the right place, specially the UIPickerView:
First Time Portrait:
Rotate to Landscape:
Back to Portrait:
If I repeat the rotation process, things just get worse. What am I doing wrong?
The source code is here: http://dl.dropbox.com/u/3978473/forums/Rotator.zip
Thanks in advance!
To solve your offset problems, rewrite the displayView: method as below.
-(void) displayView: (UIView *)aView{
self.view = aView;
}
Rotations however are strange. you should review that part of code.
Use the UIViewController rotation methods
(void)willRotateToInterfaceOrientation:duration:
(void)didRotateFromInterfaceOrientation:
instead of -(void)deviceDidRotate:
Much simpler, you will avoid that strange bouncing, and you don't need notifications any more.
Do some reading on the apple documentation on the methods i specified above.
Hope this helps.
OK, I found the error. It's pretty simple and stupid: I mixed frame and bounds.
In the displayView: code I was setting the frame of the child view to the frame of the parent view, when it should be the bounds of the parent.
When setStatusBarHidden:NO is set before the view loads, the UINavigationBar and other elements appear aligned immediately below the StatusBar as they should. However, when setStatusBarHidden:NO is set after the view loads, the UINavigationBar is partially covered.
The StatusBar must be revealed after loading the said view, but how can this be done without encountering the aforementioned problem?
I found a hack in a code of mine, though can't remember or find where it came from. The trick is to refresh the navigation bar by hiding and reshowing it:
[self.navigationController setNavigationBarHidden:YES animated:NO];
[self.navigationController setNavigationBarHidden:NO animated:NO];
In my code the function looks like this:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
[self.navigationController setNavigationBarHidden:YES animated:NO];
[self.navigationController setNavigationBarHidden:NO animated:NO];
}
However, BE WARNED, this is a hack, and currently I'm struggling with some bugs that appear to originate from this code (navigation item doesn't match navigation content). But since it did work for me in some places, I'd thought I'd mention it.
Edit:
I think I found the initial post here:
How do I get the navigation bar in a UINavigationController to update its position when the status bar is hidden?
GL,
Oded
(I realise this was an old question, but I just spent half an hour trying to find the answer myself without success, so I thought I would post it here for anyone else who get stuck... especially if you are trying to SHOW the status bar and your view is ending up overlapping it)
I found this works if you want to HIDE the status bar...
[[UIApplication sharedApplication] setStatusBarHidden:YES];
[self.view setFrame: [[UIScreen mainScreen] bounds]];
but not when you want to SHOW the status bar...
in that case I use this solution which works, but worries me because it hard codes the status bar height to 20...
it also worries me that I have to adjust the view differently depending on orientation. but if I didn't do that it always had the 20 point gap on the wrong edge.
In my case I want to turn the status bar off for some views, and then back on when I return. I had particular problems if I rotated the device while the bar was off. so the switch statement, although ugly (someone might post a cleaner solution), works.
[[UIApplication sharedApplication] setStatusBarHidden:NO];
CGRect frame = [[UIScreen mainScreen] bounds];
switch (self.interfaceOrientation)
{
case UIInterfaceOrientationPortrait:
frame.origin.y = 20;
frame.size.height -= 20;
break;
case UIInterfaceOrientationPortraitUpsideDown:
frame.origin.y = 0;
frame.size.height -= 20;
break;
case UIInterfaceOrientationLandscapeLeft:
frame.origin.x = 20;
frame.size.width -= 20;
break;
case UIInterfaceOrientationLandscapeRight:
frame.origin.x = 0;
frame.size.width -= 20;
break;
}
[self.view setFrame:frame];
My guess is the nav bar is being loaded before the status bar is shown, so the position of the nav bar is (0,0) which then overlaps with the status bar at (0,0). You can just move the frame of the navigation bar (or set up an animation block) in viewDidLoad, after you call setStatusBarHidden:NO.
Try doing navigationBar.frame = CGRectMake(0,20,320,44);
The status bar is 320x20, so just moving your navigation bar down by 20 should accomodate for it.
If you are having this problem because you are not displaying the status bar while your Default.png is loading, and then want to display the status bar immediately upon viewing your first View Controller, just make sure you put [[UIApplication sharedApplication] setStatusBarHidden:NO]; before [self.window makeKeyAndVisible]; in your AppDelegate.m. It happens so quick, you won't ever see the status bar on the splash screen.
[[UIApplication sharedApplication] setStatusBarHidden:NO];
[self.window makeKeyAndVisible];
Here's what I'm doing in my root controller now in iOS 5 after I tell the status bar to animate in. Ugly, but it seems to work.
CGRect rect;
if ( self.interfaceOrientation == UIInterfaceOrientationPortrait )
rect = CGRectMake(0, 20, 320, 460);
else if ( self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown )
rect = CGRectMake(0, 0, 320, 460);
else if ( self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft )
rect = CGRectMake(20, 0, 300, 480);
else
rect = CGRectMake(0, 0, 300, 480);
[UIView animateWithDuration:0.35 animations:^{ self.view.frame = rect; }];
in iOS 7 you can use:
setNeedsStatusBarAppearanceUpdate
for example:
[self.mainViewController.navigationController setNeedsStatusBarAppearanceUpdate];
apple docs:
Call this method if the view controller's status bar attributes, such
as hidden/unhidden status or style, change. If you call this method
within an animation block, the changes are animated along with the
rest of the animation block.
use it only for iOS 7.