AVCaptureVideoPreviewLayer方向问题 - 需要横屏显示

71

我的应用只支持横屏。我是这样展示AVCaptureVideoPreviewLayer的:

self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
[self.previewLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];                    
NSLog(@"previewView: %@", self.previewView);
CALayer *rootLayer = [self.previewView layer];
[rootLayer setMasksToBounds:YES];
[self.previewLayer setFrame:[rootLayer bounds]];
    NSLog(@"previewlayer: %f, %f, %f, %f", self.previewLayer.frame.origin.x, self.previewLayer.frame.origin.y, self.previewLayer.frame.size.width, self.previewLayer.frame.size.height);
[rootLayer addSublayer:self.previewLayer];
[session startRunning];

self.previewView 的框架是 (0,0,568,320),是正确的。self.previewLayer 的框架记录为 (0,0,568,320),在理论上也是正确的。但是,相机显示作为纵向矩形出现在横向屏幕中央,相机预览图像的方向错误了90度。我做错了什么?我需要相机预览图层以全屏幕,横向模式出现,并且图像应该正确方向。

4
重要提示:对于2016年,请跳到下面正确的答案https://dev59.com/vGUp5IYBdhLWcg3wvpcM#36575423。与许多计算机主题一样,API细节会随着时间发生变化。随着自动布局等技术的引入,这里旧的答案(当时非常好)现在已经不正确了。(另外三四年后可能会有一个新的正确答案!) - Fattie
20个回答

90

Swift 5.5,Xcode 13.2

private func updatePreviewLayer(layer: AVCaptureConnection, orientation: AVCaptureVideoOrientation) {
    layer.videoOrientation = orientation
    self.previewLayer?.frame = view.bounds
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if let connection = self.previewLayer?.connection {
        let currentDevice = UIDevice.current
        let orientation: UIDeviceOrientation = currentDevice.orientation
        let previewLayerConnection: AVCaptureConnection = connection
        
        if previewLayerConnection.isVideoOrientationSupported {
            switch orientation {
            case .portrait: self.updatePreviewLayer(layer: previewLayerConnection, orientation: .portrait)
            case .landscapeRight: self.updatePreviewLayer(layer: previewLayerConnection, orientation: .landscapeLeft)
            case .landscapeLeft: self.updatePreviewLayer(layer: previewLayerConnection, orientation: .landscapeRight)
            case .portraitUpsideDown: self.updatePreviewLayer(layer: previewLayerConnection, orientation: .portraitUpsideDown)
            default: self.updatePreviewLayer(layer: previewLayerConnection, orientation: .portrait)
            }
        }
    }
}

Swift 2.2,Xcode 7.3

private func updatePreviewLayer(layer: AVCaptureConnection, orientation: AVCaptureVideoOrientation) {
    
    layer.videoOrientation = orientation

    previewLayer.frame = self.view.bounds

}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if let connection =  self.previewLayer?.connection  {
        
        let currentDevice: UIDevice = UIDevice.currentDevice()
        
        let orientation: UIDeviceOrientation = currentDevice.orientation
        
        let previewLayerConnection : AVCaptureConnection = connection
        
        if (previewLayerConnection.supportsVideoOrientation) {
            
            switch (orientation) {
            case .Portrait: updatePreviewLayer(previewLayerConnection, orientation: .Portrait)
                                
            case .LandscapeRight: updatePreviewLayer(previewLayerConnection, orientation: .LandscapeLeft)
                                
            case .LandscapeLeft: updatePreviewLayer(previewLayerConnection, orientation: .LandscapeRight)
                                
            case .PortraitUpsideDown: updatePreviewLayer(previewLayerConnection, orientation: .PortraitUpsideDown)
                                
            default: updatePreviewLayer(previewLayerConnection, orientation: .Portrait)
            
            }
        }
    }
}

2
这非常漂亮。 - Fattie
1
这里有一些很棒的代码...http://drivecurrent.com/devops/using-swift-and-avfoundation-to-create-a-custom-camera-view-for-an-ios-app/#comment-4686 - Fattie
7
如果您快速从一个横向模式旋转到另一个横向模式,则此答案将失败。在这种情况下,布局方法不被调用。我还使用viewWillLayout进行了测试,并得到了相同的结果。 - CodeBender
5
在这个开关中,两个情况(LandscapeRight和LandscapeLeft)被颠倒了,但它仍然很好地工作。 - Fox5150
2
在 Swift 的 switch 中,你不必像 C 语言那样写 break。 - Juguang
显示剩余5条评论

70
默认摄像头方向是横屏向左(Home键在左边)。您需要执行两个操作:

1- 将previewLayer框架更改为:

self.previewLayer.frame=self.view.bounds;

你需要将预览层的框架设置为屏幕的边界,以便在屏幕旋转时预览层的框架也会随之变化(不能使用根视图的框架,因为它不会随着旋转而改变,但根视图的边界会发生变化)。在你的示例中,你正在将预览层的框架设置为一个我看不到的previewView属性。
2- 你需要根据设备的旋转来旋转预览层连接。在viewDidAppear中添加以下代码:
-(void) viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:YES];

  //Get Preview Layer connection
  AVCaptureConnection *previewLayerConnection=self.previewLayer.connection;

  if ([previewLayerConnection isVideoOrientationSupported])
    [previewLayerConnection setVideoOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; 
}

希望这能解决问题。 完全披露:这是一个简化版本,因为您不关心横向右侧或横向左侧。

谢谢,这个可行。我之前用的是self.previewLayer.orientation = UIInterfaceOrientationLandscapeLeft,虽然它已经被弃用了,但我还是不太放心。请问如何同时支持左右横屏? - soleil
然而,此代码需在iOS 6.0以上版本运行,因为使用了self.previewLayer.connection。所以请使用captureVideoPreviewLayer.orientation = UIInterfaceOrientationLandscapeLeft; - alones
1
statusBarOrientation在iOS 9中已被弃用,因此不再明智使用。此外,它返回的UIDeviceOrientation类型与AVCaptureVideoOrientation不同,因此如果您处于未定义的设备方向或将设备平放在桌面上,则可能无法确定会发生什么。 - algal
根据algal的建议,现在我们可以通过UIApplication.shared.statusBarOrientation检查当前方向,然后通过AVCaptureVideoOrientation设置视频方向。它们是一一对应的。 - haxpor
self.previewLayer.connection 在某些 iPad 上为 null,有什么想法吗? - undefined

31
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    if let connection = self.previewLayer?.connection {
        let currentDevice: UIDevice = UIDevice.current
        let orientation: UIDeviceOrientation = currentDevice.orientation
        let previewLayerConnection : AVCaptureConnection = connection
        
        if (previewLayerConnection.isVideoOrientationSupported) {
            switch (orientation) {
            case .portrait:
                previewLayerConnection.videoOrientation = AVCaptureVideoOrientation.portrait
            case .landscapeRight:
                previewLayerConnection.videoOrientation = AVCaptureVideoOrientation.landscapeRight
            case .landscapeLeft:
                previewLayerConnection.videoOrientation = AVCaptureVideoOrientation.landscapeLeft
            case .portraitUpsideDown:
                previewLayerConnection.videoOrientation = AVCaptureVideoOrientation.portraitUpsideDown
                
            default:
                previewLayerConnection.videoOrientation = AVCaptureVideoOrientation.portrait
            }
        }
    }
}

当我在横向模式下使用这段代码时,预览层的框架位置不正确,而是出现在左下角,你能否建议我如何修复?谢谢。 - DrPatience
1
@DrPatience,当您重新设置previewLayer的框架时,您可以克服这个问题:previewLayer?.frame = CGRectMake(0, 0, size.width, size.height) - Olivier de Jonge
1
请注意 - UIDevice.currentDevice().orientation 返回设备的方向,无论用户界面是否支持它。最好比较 UIApplication.shared.statusBarOrientation,确保你的 videoPreviewLayer 与你的 UI 具有相同的方向。 - Dylan Hand
4
所有这些 break 语句都是不必要的。Swift 的 switch 语句默认不会贯穿下去,就像在 Objective-C 中一样。此外,previewLayerConnection.videoOrientation 已知为 AVCaptureVideoOrientation,因此您可以将语句简化为 previewLayerConnection.videoOrientation = .portrait - Rob

27

这个API似乎有所改变。 videoOrientation现在是预览层的connection属性的一个属性。此外,不需要使用switch语句。Swift 3.0的答案如下:


override func viewDidLayoutSubviews() {
    self.configureVideoOrientation()
}

private func configureVideoOrientation() {
    if let previewLayer = self.previewLayer,
        let connection = previewLayer.connection {
        let orientation = UIDevice.current.orientation

        if connection.isVideoOrientationSupported,
            let videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) {
            previewLayer.frame = self.view.bounds
            connection.videoOrientation = videoOrientation
        }
    }
}

2
这应该是答案。谢谢! - Nick Yap
1
非常好。只需要修复一下,最后一行应该是 connection.videoOrientation = videoOrientation(去掉 previewLayer.)。 - JKvr
应该是这个答案,它可以工作。previewLayer.connection 是可空的,应该在最后一行:previewLayer.connection?.videoOrientation = videoOrientation。 - Bradley
简单而美好。应该被接受。 - aheze
根据此帖子中的另一个评论,应使用UIApplication.shared.statusBarOrientation,因为视频可能与视图方向不同。 - Leon

19

我们不能使用

[previewLayerConnection setVideoOrientation:[[UIApplication sharedApplication] statusBarOrientation]]; 

因为 UIInterfaceOrientation != AVCaptureVideoOrientation

但我们可以测试数值... 并使用以下代码使其生效。

-(void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
    switch (orientation) {
        case UIInterfaceOrientationPortrait:
            [_videoPreviewLayer.connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
            break;
        case UIInterfaceOrientationPortraitUpsideDown:
            [_videoPreviewLayer.connection setVideoOrientation:AVCaptureVideoOrientationPortraitUpsideDown];
            break;
        case UIInterfaceOrientationLandscapeLeft:
            [_videoPreviewLayer.connection setVideoOrientation:AVCaptureVideoOrientationLandscapeLeft];
            break;
        case UIInterfaceOrientationLandscapeRight:
            [_videoPreviewLayer.connection setVideoOrientation:AVCaptureVideoOrientationLandscapeRight];
            break;
    }
}

添加一个默认值: break; - netshark1000

7

对于那些在完整的相机预览方面遇到困难的人,这是生产代码。当然缺点是方向变化时会有延迟。如果有更好的解决方法,请分享。

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self initCamera];
}

- (void)initCamera
{
    AVCaptureDeviceInput *captureInput = [AVCaptureDeviceInput deviceInputWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo] error:nil];
    if (captureInput) {
        mSession = [[AVCaptureSession alloc] init];
        [mSession addInput:captureInput];
    }
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
    if ([mSession isRunning]) {
        [mSession stopRunning];
        [mCameraLayer removeFromSuperlayer];

        [self initCamera];

        [self startCamera];
    }
}

- (void)startCamera
{
    [mSession startRunning];
    Settings::getInstance()->setClearColor(Color(0, 0, 0, 0));
    mCameraLayer = [AVCaptureVideoPreviewLayer layerWithSession: mSession];
    [self updateCameraLayer];
    [mCameraView.layer addSublayer:mCameraLayer];
}

- (void)stopCamera
{
    [mSession stopRunning];
    [mCameraLayer removeFromSuperlayer];
    Settings::getInstance()->setDefClearColor();
}

- (void)toggleCamera
{
    mSession.isRunning ? [self stopCamera] : [self startCamera];
    [mGLKView setNeedsDisplay];
}

- (void)updateCameraLayer
{
    mCameraLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    mCameraLayer.frame = mCameraView.bounds;
    float x = mCameraView.frame.origin.x;
    float y = mCameraView.frame.origin.y;
    float w = mCameraView.frame.size.width;
    float h = mCameraView.frame.size.height;
    CATransform3D transform = CATransform3DIdentity;
    if (UIDeviceOrientationLandscapeLeft == [[UIDevice currentDevice] orientation]) {
        mCameraLayer.frame = CGRectMake(x, y, h, w);
        transform = CATransform3DTranslate(transform, (w - h) / 2, (h - w) / 2, 0);
        transform = CATransform3DRotate(transform, -M_PI/2, 0, 0, 1);
    } else if (UIDeviceOrientationLandscapeRight == [[UIDevice currentDevice] orientation]) {
        mCameraLayer.frame = CGRectMake(x, y, h, w);
        transform = CATransform3DTranslate(transform, (w - h) / 2, (h - w) / 2, 0);
        transform = CATransform3DRotate(transform, M_PI/2, 0, 0, 1);
    } else if (UIDeviceOrientationPortraitUpsideDown == [[UIDevice currentDevice] orientation]) {
        mCameraLayer.frame = mCameraView.bounds;
        transform = CATransform3DMakeRotation(M_PI, 0, 0, 1);
    } else {
        mCameraLayer.frame = mCameraView.bounds;
    }
    mCameraLayer.transform  = transform;
}

    enter code here

解决这个问题的方法是在您的视图控制器中重写viewDidLayoutSubviews方法,并相应地更新边界。 - Matej
调用updateCameraLayer对我有用。我给你点赞 ;) - Ajay Sharma
1
我在viewDidAppear中调用了updateCameraLayer。需要进行一些小的更改:可以省略float x = mCameraView.bounds.origin.x和第一个mCameraLayer.frame = mCameraView.bounds; - Frank Hintsch
如何解决延迟问题?当我改变方向时,预览层出现了2秒的延迟。 - Lightsout

4

由于上述解决方案存在弃用和转换警告,并且在iOS7中设置videoOrientation似乎无效,因此我在获取AVCaptureVideoPreviewLayer时添加了方向检查:

- (AVCaptureVideoPreviewLayer *) previewLayer
{
    if(!_previewLayer)
    {
        _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: self.captureSession];

    [_previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];

    _previewLayer.frame = self.view.bounds; // Assume you want the preview layer to fill the view.

    [_previewLayer setPosition:CGPointMake(0,0)];

    if (UIDeviceOrientationLandscapeLeft == [[UIDevice currentDevice] orientation]) {
        _previewLayer.transform = CATransform3DMakeRotation(-M_PI/2, 0, 0, 1);
    }
    else if (UIDeviceOrientationLandscapeRight == [[UIDevice currentDevice] orientation])
    {
        _previewLayer.transform = CATransform3DMakeRotation(M_PI/2, 0, 0, 1);
    }
}

return _previewLayer;
}

4

Swift 5版本

  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    updatePreview()
  }

  func updatePreview() {
    let orientation: AVCaptureVideoOrientation
    switch UIDevice.current.orientation {
      case .portrait:
        orientation = .portrait
      case .landscapeRight:
        orientation = .landscapeLeft
      case .landscapeLeft:
        orientation = .landscapeRight
      case .portraitUpsideDown:
        orientation = .portraitUpsideDown
      default:
        orientation = .portrait
    }
    if previewLayer?.connection?.isVideoOrientationSupported == true {
      previewLayer?.connection?.videoOrientation = orientation
    }
    previewLayer.frame = view.bounds
  }

3

正确映射 UIDeviceOrientationAVCaptureVideoOrientation 是必要的。

如果您的应用程序支持 设备旋转,则还需要 调整预览框架大小,并且此 函数 应该从 viewDidLayoutSubviews()viewWillTransition() 中调用。

private func configureVideoOrientation() {
    if let preview = self.previewLayer,
        let connection = preview.connection {
        let orientation = UIDevice.current.orientation

        if connection.isVideoOrientationSupported {
            var videoOrientation: AVCaptureVideoOrientation
            switch orientation {
            case .portrait:
                videoOrientation = .portrait
            case .portraitUpsideDown:
                videoOrientation = .portraitUpsideDown
            case .landscapeLeft:
                videoOrientation = .landscapeRight
            case .landscapeRight:
                videoOrientation = .landscapeLeft
            default:
                videoOrientation = .portrait
            }
            preview.frame = self.view.bounds
            connection.videoOrientation = videoOrientation
        }
    }
}

3

以下是适用于Swift 4.2 - Xcode 10.0 - iOS 12.0的答案:

var videoPreviewLayer: AVCaptureVideoPreviewLayer?

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  if let previewLayerConnection =  self.videoPreviewLayer?.connection, previewLayerConnection.isVideoOrientationSupported {
    updatePreviewLayer(layer: previewLayerConnection, orientation: UIApplication.shared.statusBarOrientation.videoOrientation)
  }
}

private func updatePreviewLayer(layer: AVCaptureConnection, orientation: AVCaptureVideoOrientation) {
  layer.videoOrientation = orientation
  videoPreviewLayer?.frame = self.view.bounds
}

不要忘记从UIInterfaceOrientation到AVCaptureVideoOrientation的映射。

extension UIInterfaceOrientation {

  public var videoOrientation: AVCaptureVideoOrientation {
    switch self {
    case .portrait:
      return AVCaptureVideoOrientation.portrait
    case .landscapeRight:
      return AVCaptureVideoOrientation.landscapeRight
    case .landscapeLeft:
      return AVCaptureVideoOrientation.landscapeLeft
    case .portraitUpsideDown:
      return AVCaptureVideoOrientation.portraitUpsideDown
    default:
      return AVCaptureVideoOrientation.portrait
    }
  }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接