DEV Community

Fernando Martín Ortiz
Fernando Martín Ortiz

Posted on

Basic AVPlayerViewController

Introduction

AVPlayerViewController is a class that belongs to AVKit and wraps a AVPlayer (which in turn belongs to AVFoundation), making it much easier and convenient to implement.

This week I implemented a AVPlayerViewController to reproduce a video in an app I'm currently working on and, as an experiment, I added the Picture in Picture (PiP) feature.

The code, after finding the right answers on StackOverflow, articles, videos and docs, was incredibly easy and brief. Much easier than I first imagined.

Implementing an AVPlayer

AVPlayer is the heart of the video playback. Creating a AVPlayer is as simple as adding this to your view controller, where the URL is where your video is located.

private lazy var player: AVPlayer = {
    let videoUrl = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!
    let player = AVPlayer(url: videoUrl)
    return player
}()
Enter fullscreen mode Exit fullscreen mode

There are lots of things you can configure on your player, but this is the most basic configuration. You can then replace the current playing track by doing this:

player.replaceCurrentItem(with: AVPlayerItem(url: newUrl))
Enter fullscreen mode Exit fullscreen mode

What the AVPlayer really plays is the AVPlayerItem object. Initializing it with a URL is just a shorthand.

Implementing a AVPlayerViewController

Once we have an AVPlayer in our view controller, we can create a AVPlayerViewController. We can do so by adding these lines:

private lazy var playerController: AVPlayerViewController = {
    let playerController = AVPlayerViewController()
    playerController.player = player
    return playerController
}()
Enter fullscreen mode Exit fullscreen mode

As I've said, AVPlayerViewController wraps our AVPlayer instance and adds lots of useful features at no cost.

Presenting the AVPlayerViewController

Doing this is just presenting the AVPlayerViewController modally, as we would do with any other UIViewController subclass.

func presentPlayerController() {
    player.play()
    self.present(playerController, animated: true, completion: nil)
}
Enter fullscreen mode Exit fullscreen mode

When to do this is up to you, probably when a button is clicked or a row is selected in a table or collection view.

Convenience

Maybe there are better alternatives than this one, but I found creating a class called VideoController or similar is very handy. Inside our VideoController we can add all of our code and instantiate it with a root UIViewController.

final class VideoController: NSObject {
    private weak var viewController: UIViewController!

    // MARK: - AV -
    private let player: AVPlayer

    private lazy var playerController: AVPlayerViewController = {
        let playerController = AVPlayerViewController()
        playerController.player = player
        return playerController
    }()

    // MARK: - Init -
    init(viewController: UIViewController, url: URL) {
        self.viewController = viewController
        self.player = AVPlayer(url: url)
        super.init()
    }

    // MARK: - Public -
    func play() {
        player.play()
        viewController.present(playerController, animated: true, completion: nil)
    }
}
Enter fullscreen mode Exit fullscreen mode

Picture in Picture

This is one the coolest features in AVPlayerViewController. There are two prerequisites:

First, you'll need to configure the Audio, Airplay, and Picture in Picture background mode as a Capability in Signing & Capabilities for our target.

capabilities

This will allow us to reproduce the video in Picture in Picture when the app is in the background.

The other thing we need to do is to configure the playback background mode in the didFinishLaunchingWithOptions method in the AppDelegate.

Let's add this static method to our VideoController:

final class VideoController: NSObject {
    // ...

    static func enableBackgroundMode() {
    let audioSession = AVAudioSession.sharedInstance()
    do {
      try audioSession.setCategory(.playback, mode: .moviePlayback)
    }
    catch {
      print("Setting category to AVAudioSessionCategoryPlayback failed.")
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And finally call it in the AppDelegate:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // ...
    VideoController.enableBackgroundMode()
    // ...
  return true
}
Enter fullscreen mode Exit fullscreen mode

As said, AVPlayerViewController allow us to implement lots of useful features for free, and PiP is one of them. Doing that is just setting a property:

private lazy var playerController: AVPlayerViewController = {
  let playerController = AVPlayerViewController()
  playerController.player = player
  playerController.allowsPictureInPicturePlayback = true
  return playerController
}()
Enter fullscreen mode Exit fullscreen mode

This will work as intended.

There is a simple step missing. What happens when the video returns to the app after playing PiP. Right now, the video will just dismiss. To fix that, we'd need to set the delegate of the AVPlayerViewController and implement func playerViewController(_:, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:).

In this method, we'll need to do two things:

  1. (optional) Check if you're currently presenting the playerViewController. This will fix some weird crashes.
  2. (mandatory) Present the playerViewController.

The final code

final class VideoController: NSObject {
    private weak var viewController: UIViewController!

    // MARK: - AV -
    private lazy var player: AVPlayer = {
        let videoUrl = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!
        let player = AVPlayer(url: videoUrl)
        return player
    }()

    private lazy var playerController: AVPlayerViewController = {
        let playerController = AVPlayerViewController()
        playerController.delegate = self
        playerController.player = player
        playerController.allowsPictureInPicturePlayback = true
        return playerController
    }()

    init(viewController: UIViewController) {
        self.viewController = viewController
        super.init()
    }

    func play() {
        player.play()
        viewController.present(playerController, animated: true, completion: nil)
    }

    static func enableBackgroundMode() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.playback, mode: .moviePlayback)
        }
        catch {
            print("Setting category to AVAudioSessionCategoryPlayback failed.")
        }
    }
}

// MARK: - AVPlayerViewControllerDelegate -
extension VideoController: AVPlayerViewControllerDelegate {
    func playerViewController(_ playerViewController: AVPlayerViewController,
                              restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
        if playerViewController === viewController.presentedViewController {
            return
        }

        viewController.present(playerViewController, animated: true) {
            completionHandler(false)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)