Harish
Harish

Reputation: 1408

Using AVPlayerLooper to loop through multiple videos

I've been trying to figure out how to loop over multiple videos with AVPlayerLooper, but their templateItem takes an argument of type AVPlayerItem and not [AVPlayerItem]. I'm currently using an AVQueuePlayer to display the video but I need to loop through it.

Here's my code so far:

class MyVC: UIViewController {

     @IBOutlet weak var playerView: UIView!

     lazy var backgroundVideoPlayer = AVQueuePlayer()

     // View Controller related code (viewDidLoad, etc.) is taken out for brevity.

     private func loadBackgroundVideosRandomly() -> [AVPlayerItem] {
         let mainBundle = Bundle.main
         let movieURLs = [mainBundle.url(forResource: "Boop Burj Al Arab", withExtension: "mov"),
                          mainBundle.url(forResource: "Boop Dubai", withExtension: "mov"),
                          mainBundle.url(forResource: "Boop Dubai Clock", withExtension: "mov"),
                          mainBundle.url(forResource: "Boop Dubai Lake", withExtension: "mov")].shuffled()

         let items = movieURLs.map { AVPlayerItem(url: $0!) }
         return items
    }

    private func playBackgroundVideos() {
         let playerLayer = AVPlayerLayer(player: backgroundVideoPlayer)
         playerLayer.videoGravity = .resizeAspectFill
         playerLayer.frame = playerView.bounds
         playerView.layer.addSublayer(playerLayer)

         // Configure the player.
         backgroundVideoPlayer.seek(to: kCMTimeZero)
         backgroundVideoPlayer.actionAtItemEnd = .advance
   }
}

Upvotes: 2

Views: 4202

Answers (3)

Jared Moskowitz
Jared Moskowitz

Reputation: 1

For anyone that is still looking for an answer, Ray Wenderlich to the rescue:

https://www.raywenderlich.com/5191-video-streaming-tutorial-for-ios-getting-started#toc-anchor-009

Basically, observe for when there is one more playerItem in the queue, then reinsert all playerItems.

Upvotes: 0

Harish
Harish

Reputation: 1408

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.

Upvotes: 5

solenoid
solenoid

Reputation: 1022

AVPlayerLooper takes a player as the first argument, so you can go like:

let myPlayer = AVQueuePlayer([AVPlayerItem])

AVPlayerLooper(player: myPlayer, templateItem: oneoftheitems)

Upvotes: -1

Related Questions