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)