6
votes

When embedding an AVPlayerViewController in a portrait only iOS app it seems the app can get stuck in a weird layout when the player exits full screen if the video is full-screened while the device is held in a landscape orientation.

Is this a bug or am I doing something incorrectly?

Here's how to reproduce with a clean project using Xcode 9.4.1, swift 4, iOS 11.4, simulator or physical device.

ViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()

    //Create the player and add as child view controller
    let playerVC = AVPlayerViewController()
    self.addChildViewController(playerVC)

    //Place player's view in self
    playerVC.view.frame = CGRect(x: 10, y: 40, width: 355, height: 200)
    self.view.addSubview(playerVC.view)

    //Load example video
    playerVC.player = AVPlayer(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!)
}

How it works normally:

  • Play video, hit full screen
  • Rotate to landscape, video rotates
  • Close full screen, app returns to portrait regardless of screen or device orientation
  • ex: https://imgur.com/a/MPFmzyH

How it breaks:

  • Play video, rotate device to landscape (screen does not rotate)
  • Hit full screen
  • Exit full screen
  • Screen breaks, rotating does not fix
  • ex: https://imgur.com/a/hDdmu20
3
did you find a solution?Rafaela Lourenço
@RafaelaLourenço no actually, it seems like a platform bug OR you're just not supposed to embed the AVPlayerViewController (which doesn't make sense to me because it has a fullscreen button on it). I think the answer by Nabil works but it's definitely a hack.toemat
Thanks for the answer.. It seems really a bug. I hope apple fix soon.Rafaela Lourenço
For what it's worth, it seems iOS12 has fixed this issue.toemat

3 Answers

5
votes

When you leave player full screen you enter in viewWillApper of your View Controller. So in viewWillAppear try to set your window frame to be equal to your screen bounds.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
    appDelegate.window?.rootViewController?.view.frame = UIScreen.main.bounds
}
0
votes

You can identify when the player become fullscreen and force the portrait or landscape for your view controller.

It is not the best solution, but at least avoids the broken UI:

    import AVFoundation
    import AVKit

    var isPlayerFullscreen: Bool = false

    override func viewDidLoad() {
        super.viewDidLoad()

        //Create the player and add as child view controller
        let playerVC = AVPlayerViewController()
        self.addChildViewController(playerVC)

        //Place player's view in self
        playerVC.view.frame = CGRect(x: 10, y: 40, width: 355, height: 200)
        self.view.addSubview(playerVC.view)

        //Load example video
        playerVC.player = AVPlayer(url: URL(string: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!)

        //Add an observer for playerVC object
        playerVC.addObserver(self, forKeyPath: "videoBounds", options: NSKeyValueObservingOptions.new, context: nil)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
    {
        if keyPath == "videoBounds", let rect = change?[.newKey] as? NSValue
        {
            let playerRect: CGRect = rect.cgRectValue
            if playerRect.size.height <= 200 {
                print("Video not in full screen")                
                UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
isPlayerFullscreen = false
            } else {
                print("Video in full screen")
                isPlayerFullscreen = true
            }
        }
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return isPlayerFullscreen ? UIInterfaceOrientationMask.landscape: UIInterfaceOrientationMask.portrait
    }

    override var shouldAutorotate: Bool {
        return true
    }
0
votes

I tried to achieve using willTransitionToTraitCollection method. I have a viewController which has reference of AVPlayerViewController and a AVPlayer and inline playerView 1.Lock the orientation to allButUpsideDown 2. On load, load the AVPlayerViewController as a child and snap it to PlayerView 3. And override willTransitionToNewTraitCollection method


override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)
    coordinator.animate(alongsideTransition: { [unowned self] _ in

      if newCollection.verticalSizeClass == .compact {

        self.videoPlayerController.willMove(toParent: nil)
        self.videoPlayerController.view.removeFromSuperview()
        self.videoPlayerController.removeFromParent()

        DispatchQueue.main.async {
          self.present(self.videoPlayerController,
                       animated: false) {
                        // Do some stuff if needed
          }
        }
      }
      else if newCollection.verticalSizeClass == .regular {
        self.dismiss(animated: false) {
          DispatchQueue.main.async {

            self.videoPlayerController.view.frame = self.playerView.bounds
            self.playerView.addSubview(self.videoPlayerController.view)
          }
        }
      }
    }) { _ in
        // Lock the orientation to allButUpSideDown after animation.
    }
  }