Basic AVPlayerViewController
Tags: ios, swift, video, avfoundation
Published: 2021-01-30 18:02:55

# 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.

```jsx
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
}()
```

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:

```jsx
player.replaceCurrentItem(with: AVPlayerItem(url: newUrl))
```

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:

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

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.

```swift
func presentPlayerController() {
    player.play()
    self.present(playerController, animated: true, completion: nil)
}
```

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`.

```swift
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)
	}
}
```

# 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](https://dev-to-uploads.s3.amazonaws.com/i/kea7jh7jexufqhe76vui.png) 

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`:

```swift
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.")
    }
  }
}
```

And finally call it in the `AppDelegate`:

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

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:

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

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

```swift
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)
        }
    }
}
```