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
}()
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))
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
}()
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)
}
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)
}
}
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.
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.")
}
}
}
And finally call it in the AppDelegate:
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:
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:
- (optional) Check if you're currently presenting the
playerViewController. This will fix some weird crashes. - (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)
}
}
}

Top comments (0)