Adding Picture in Picture Playback Functionality - Multitasking - iOS 9 Swift Programming Cookbook (2015)

iOS 9 Swift Programming Cookbook (2015)

Chapter 7. Multitasking

iOS 9 added some really cool multitasking functionalities to select devices, such as the latest iPads. One of these functionalities is PiP or Picture in Picture. In this chapter, we’ll have a look at some of these exciting features.

7.1 Adding Picture in Picture Playback Functionality

Problem

You want to let a user shrink a video to occupy a portion of the screen, so that she can view and interact with other content in other apps.

Solution

I’ll break the process down into small and digestible steps:

1. You need a view that has a layer of type AVPlayerLayer. This layer will be used by a view controller to display the video.

2. Instantiate an item of type VPlayerItem that represents the video.

3. Take the player item and place it inside an instance of AVPlayer.

4. Assign this player to our view’s layer’s player object. (Don’t worry if this sounds confusing. I’ll explain it soon.)

5. Assign this view to our view controller’s main view and issue the play() function on the player to start normal playback.

6. Using KVO, listen to changes to the currentItem.status property of your player and wait until the status becomes ReadyToPlay, at which point you create an instance of the AVPictureInPictureController class.

7. Start a KVO listener on the pictureInPicturePossible property of our controller. Once this value becomes true, let the user know that now they can go into picture in picture mode.

8. Now when the user presses a button to start picture in picture, read the value of pictureInPicturePossible from your controller for safety’s sake, and if it checks out, call the startPictureInPicture() function on the controller to start the picture in picture eventually.

Discussion

Picture in picture is finally here. Let’s get started. Armed with what you learned in the solution section of this recipe, let’s start defining our view. Create a view class and call it PipView. Go into the PipView.swift file and start importing the right frameworks:

import Foundation

import UIKit

import AVFoundation

Then define what a “pippable” item is. It is any type that has a PiP layer and a PiP player:

protocol Pippable{

var pipLayer: AVPlayerLayer{get}

var pipLayerPlayer: AVPlayer? {get set}

}

Extend UIView to make it pippable:

extension UIView : Pippable{

var pipLayer: AVPlayerLayer{

get{return layer as! AVPlayerLayer}

}

//shortcut into pipLayer.player

var pipLayerPlayer: AVPlayer?{

get{return pipLayer.player}

set{pipLayer.player = newValue}

}

override public func awakeFromNib() {

super.awakeFromNib()

backgroundColor = .blackColor()

}

}

Last but not least for this view, change the view’s layer class to AVPlayerLayer:

class PipView : UIView{

override class func layerClass() -> AnyClass{

return AVPlayerLayer.self

}

}

Go to your view controller’s storyboard and change the main view’s class to PipView. Also embed your view controller in a navigation controller and put two bar button items on the nav bar, namely:

§ Play (give it a play button style)

§ PiP (by pressing this we enable PiP. Disable this button by default and hook it to an outlet in your code.)

Hook up the two buttons to your view controller’s code. The Play button will be hooked to a method called play() and the PiP button to beginPip(). Now let’s head to our view controller and import some frameworks we need:

import UIKit

import AVKit

import AVFoundation

import SharedCode

Define the KVO context for watching the properties of our player:

private var kvoContext = 0

let pipPossible = "pictureInPicturePossible"

let currentItemStatus = "currentItem.status"

Then our view controller becomes pippable:

protocol PippableViewController{

var pipView: PipView {get}

}

extension ViewController : PippableViewController{

var pipView: PipView{

return view as! PipView

}

}

NOTE

If you want to, you can define your view controller as conformant to AVPictureInPictureControllerDelegate to get delegate messages from the PiP view controller.

I’ll also define a property for the PiP button on my view controller so that I can enable this button when PiP is available:

@IBOutlet var beginPipBtn: UIBarButtonItem!

We also need a player of type AVPlayer. Don’t worry about its URL. We will set its URL later.

lazy var player: AVPlayer = {

let p = AVPlayer()

p.addObserver(self, forKeyPath: currentItemStatus,

options: .New, context: &kvoContext)

return p

}()

Here we define the PiP controller and the video URL. As soon as the URL is set, we construct an asset to hold the URL, place it inside the player, and set the player on our view’s layer:

var pipController: AVPictureInPictureController?

var videoUrl: NSURL? = nil{

didSet{

if let u = videoUrl{

let asset = AVAsset(URL: u)

let item = AVPlayerItem(asset: asset,

automaticallyLoadedAssetKeys: ["playable"])

player.replaceCurrentItemWithPlayerItem(item)

pipView.pipLayerPlayer = player

}

}

}

I also need a method that returns the URL of the video I am going to play. I’ve embedded a public domain video to my app and it resides in my app bundle. Check out this book’s GitHub repo for sample code:

var embeddedVideo: NSURL?{

return NSBundle.mainBundle().URLForResource("video", withExtension: "mp4")

}

We need to find out whether PiP is supported by calling the isPictureInPictureSupported() class method of the AVPictureInPictureController class.

func isPipSupported() -> Bool{

guard AVPictureInPictureController.isPictureInPictureSupported() else{

//no pip

return false

}

return true

}

When we start our pip controller, we also need to make sure that the audio plays well even though the player is detached from our app. For that, we have to set our app’s audio playback category:

func setAudioCategory() -> Bool{

//set the audio category

do{

try AVAudioSession.sharedInstance().setCategory(

AVAudioSessionCategoryPlayback)

return true

} catch {

return false

}

}

When PiP playback is available, we can finally construct our PiP controller with our player’s layer. Remember, if the layer is not ready yet to play PiP, constructing the PiP view controller will fail:

func startPipController(){

pipController = AVPictureInPictureController(playerLayer: pipView.pipLayer)

guard let controller = pipController else{

return

}

controller.addObserver(self, forKeyPath: pipPossible,

options: .New, context: &kvoContext)

}

Write the code for play() now. We don’t have to check for availability of PiP just because we want to play a video:

@IBAction func play() {

guard setAudioCategory() else{

alert("Could not set the audio category")

return

}

guard let u = embeddedVideo else{

alert("Cannot find the embedded video")

return

}

videoUrl = u

player.play()

}

As soon as the user presses the PiP button, we start PiP if the pictureInPicturePossible() method of our PiP controller returns true:

@IBAction func beginPip() {

guard isPipSupported() else{

alert("PiP is not supported on your machine")

return

}

guard let controller = pipController else{

alert("Could not instantiate the pip controller")

return

}

controller.addObserver(self, forKeyPath: pipPossible,

options: .New, context: &kvoContext)

if controller.pictureInPicturePossible{

controller.startPictureInPicture()

} else {

alert("Pip is not possible")

}

}

Last but not least, we listen for KVO messages:

override func observeValueForKeyPath(keyPath: String?,

ofObject object: AnyObject?,

change: [NSObject : AnyObject]?,

context: UnsafeMutablePointer<Void>) {

guard context == &kvoContext else{

return

}

if keyPath == pipPossible{

guard let possibleInt = change?[NSKeyValueChangeNewKey]

as? NSNumber else{

beginPipBtn.enabled = false

return

}

beginPipBtn.enabled = possibleInt.boolValue

}

else if keyPath == currentItemStatus{

guard let statusInt = change?[NSKeyValueChangeNewKey] as? NSNumber,

let status = AVPlayerItemStatus(rawValue: statusInt.integerValue)

where status == .ReadyToPlay else{

return

}

startPipController()

}

}

NOTE

Give this a go in an iPad Air 2 or a similar device that has PiP support.

See Also