iosswiftgrand-central-dispatchavplayeravplayeritem

AVPlayer.status doesn't run when wrapped in a DispatchWorkItem with a Delay


Because I'm playing videos in cells I have an AVPlayer that plays videos in certain circumstances immediately and others it runs a few seconds later. When it runs immediately the .status works fine. But when I wrap it in a DispatchWorkItem with a .asyncAfter delay that same exact .status is never called. I also tried to use a perform(_:, with:, afterDelay:) and a Timer but this didn't work either.

var player: AVPlayer?
var playerItem: AVPlayerItem?
var observer: NSKeyValueObservation? = nil
var workItem: DispatchWorkItem? = nil
var startImmediately = false
var timer: Timer?

viewDidLoad() {
    // add asset to playerItem, add playerItem to player ... 
}

func someCircumstance() {

    if startImmediately {

        setNSKeyValueObserver() // this works fine and .status is called

    } else {

        createWorkItem()

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.33, execute: executeWorkItem) // this delay runs but .status is never called

        // perform(#selector(executeWorkItem), with: nil, afterDelay: 0.33) // same issue

        // timer = Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(executeWorkItem), userInfo: nil, repeats: false) // same issue
        // RunLoop.current.add(timer!, forMode: .common)
    }
}

func createWorkItem() {

    workItem = DispatchWorkItem {
        DispatchQueue.main.async { [weak self] in
            self?.setNSKeyValueObserver()
        }
    }
}

@objc func executeWorkItem() {

    guard let workItem = workItem else { return }

    workItem.perform()
}

func setNSKeyValueObserver() {

    // without a long explanation this sometimes has to start with a delay because of scrolling reason I also might have to cancel it
    observer = player?.observe(\.status, options: [.new, .old]) { [weak self] (player, change) in
                
        switch (player.status) {
        case .readyToPlay:
            print("Media Ready to Play")
                    
        case .failed, .unknown:
            print("Media Failed to Play")
        @unknown default:
            print("Unknown Error")
        }
    }
}

I also tried to use the older KVO API .status observer instead but the same issue occurred when using a delay

private var keepUpContext = 0

viewDidLoad() {

    // add asset to playerItem, add playerItem to player ...
    player?.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp", options: [.old, .new], context: &keepUpContext)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

    if context == &keepUpContext {

        if let player = player, let item = player.currentItem, item.isPlaybackLikelyToKeepUp {
            print("Media Ready to Play")
        }
    }
}

The problem seems to be the delay.

Update

I just tried the following code and and this doesn't work either:

if startImmediately {

    setNSKeyValueObserver()

} else {

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
        self?.setNSKeyValueObserver()
    }
}

Solution

  • It looks like you're adding your observer after the player has loaded the content. It likely loads between the viewDidLoad and the delay. If you add .initial to the list of options when adding the observer, you'll be sure to get the state notification even if the player is already ready.