So I have figured out a solution by watching some of the WWDC 2016 talks where they describe a treadmill pattern and looking at sample code.
Essentially, you load up the videos you want to play and then using Key Value Observing
you respond to when a video has been played and then add that played video back to the end of the stack.
First create a protocol:
protocol BackgroundLooper {
/// Loops the videos specified forever.
///
/// - Parameter urls: The url where the video is located at.
init (urls: [URL])
/// Starts looping the videos in a specified layer.
///
/// - Parameter layer: The layer where the video should be displayed.
func start(in layer: CALayer)
/// Stops the video playback.
func stop()
}
Then create a BackgroundQueuePlayerLooper
that conforms to the protocol.
import AVFoundation
/// Repeats a set of videos forever (ideally for use in a background view).
class BackgroundQueuePlayerLooper: NSObject, BackgroundLooper {
// MARK: - Observer contexts
/// The context required for observing.
private struct ObserverContexts {
static var playerStatus = 0
static var playerStatusKey = "status"
static var currentItem = 0
static var currentItemKey = "currentItem"
static var currentItemStatus = 0
static var currentItemStatusKey = "currentItem.status"
static var urlAssetDurationKey = "duration"
static var urlAssetPlayableKey = "playable"
}
// MARK: - Properties
private var player: AVQueuePlayer?
private var playerLayer: AVPlayerLayer?
private var isObserving = false
private let videoURLs: [URL]
// MARK: - Initialization
required init(urls: [URL]) {
self.videoURLs = urls
}
// MARK: - Looper
func start(in layer: CALayer) {
stop()
player = AVQueuePlayer()
player?.externalPlaybackVideoGravity = .resizeAspectFill
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = .resizeAspectFill
guard let playerLayer = playerLayer else { fatalError("There was an error creating the player layer!") }
playerLayer.frame = layer.bounds
layer.addSublayer(playerLayer)
let assets = videoURLs.map { AVURLAsset(url: $0) }
assets.forEach { player?.insert(AVPlayerItem(asset: $0), after: nil) }
startObserving()
player?.play()
}
func stop() {
player?.pause()
stopObserving()
player?.removeAllItems()
player = nil
playerLayer?.removeFromSuperlayer()
playerLayer = nil
}
// MARK: - Key value observing
/// Starts observing the player.
private func startObserving() {
guard let player = player else { return }
guard !isObserving else { return }
player.addObserver(self, forKeyPath: ObserverContexts.playerStatusKey, options: .new, context: &ObserverContexts.playerStatus)
player.addObserver(self, forKeyPath: ObserverContexts.currentItemKey, options: .old, context: &ObserverContexts.currentItem)
player.addObserver(self, forKeyPath: ObserverContexts.currentItemStatusKey, options: .new, context: &ObserverContexts.currentItemStatus)
isObserving = true
}
/// Stops observing the player.
private func stopObserving() {
guard let player = player else { return }
guard isObserving else { return }
player.removeObserver(self, forKeyPath: ObserverContexts.playerStatusKey, context: &ObserverContexts.playerStatus)
player.removeObserver(self, forKeyPath: ObserverContexts.currentItemKey, context: &ObserverContexts.currentItem)
player.removeObserver(self, forKeyPath: ObserverContexts.currentItemStatusKey, context: &ObserverContexts.currentItemStatus)
isObserving = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &ObserverContexts.playerStatus {
guard let newPlayerStatus = change?[.newKey] as? AVPlayerStatus else { return }
guard newPlayerStatus == .failed else { return }
// End looping since player has failed
stop()
} else if context == &ObserverContexts.currentItem {
guard let player = player else { return }
// Play queue emptied out due to bad player item. End looping.
guard !player.items().isEmpty else { stop(); return }
/*
Append the previous current item to the player's queue. An initial
change from a nil currentItem yields NSNull here. Check to make
sure the class is AVPlayerItem before appending it to the end
of the queue.
*/
guard let itemRemoved = change?[.oldKey] as? AVPlayerItem else { return }
itemRemoved.seek(to: kCMTimeZero, completionHandler: nil)
stopObserving()
player.insert(itemRemoved, after: nil)
startObserving()
} else if context == &ObserverContexts.currentItemStatus {
guard let newPlayerItemStatus = change?[.newKey] as? AVPlayerItemStatus else { return }
guard newPlayerItemStatus == .failed else { return }
// End looping since player item has failed.
stop()
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
Essentially, we setup the AVPlayer
and AVPlayerLayer
objects. Then KVO listens for when a video has finished playing and adds it to the end of the videos to be played.
player.currentItem
is actually different as it looks for the old value – Hypochondriac