Using AVPlayerLooper to loop through multiple videos
Asked Answered
J

3

2

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
   }
}
Jackshaft answered 9/8, 2017 at 5:14 Comment(0)
J
5

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.

Jackshaft answered 9/8, 2017 at 18:52 Comment(1)
it could be worth to highlight with a well-visible comment that, out of those 3 KVO registrations, two look for the new value, but player.currentItem is actually different as it looks for the old valueHypochondriac
D
0

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.

Detriment answered 26/5, 2020 at 20:32 Comment(0)
C
-1

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

let myPlayer = AVQueuePlayer([AVPlayerItem])

AVPlayerLooper(player: myPlayer, templateItem: oneoftheitems)
Chamberlin answered 9/8, 2017 at 6:55 Comment(5)
Ahh, but will it loop through all of the videos? I want it to play all of the videos and then start over playing the videos again (not just play one of the videos again).Jackshaft
Ok, so I had a different solution that did work for me. I'll post it :)Jackshaft
In my experience using AvPlayerLooperCauses a slight glitch at the beginning of the loop as oppose to doing it with NSNotification + seekLillylillywhite
You know, thats odd because for my use case I would always get a stutter doing that. Even making an intermediate format for that section being played and couldn't find a way to get it to loop cleanly. I actually ended up using developer.apple.com/library/archive/samplecode/avloopplayer/… modified a bunch, and I think code ended up being AVPlayerLooper in the next ios release (I think 10). (The apple code is obviously way more refined than what I was making even if it was the same basic concepts)Chamberlin
This does not answer the question of looping over multiple videos.Tobias

© 2022 - 2024 — McMap. All rights reserved.