Tracking AVPlayer starting stream playback

If you ever worked with streaming audio on iOS you definitely used AVAudioPlayer or AVPlayer as one of the low-hanging and robust options provided by Apple.

AVPlayer

Outside of the stellar performance and versatility the problem with both of them is a weak delegation system, especially for medium/pro use. It's true that you can go far with AVPlayer and even use its basic status changes to react to its behavior but they have few limitations:

  1. You actually have to work with them via KVO (Key-Value Observation) which is not ideal
  2. KVO still doesn't give you answers to everything you need

I had this basic need for a while: do some stuff in the app once the AVPlayer starts playing music from a stream. KVO'ing 'status', 'rate' and 'currentItem.status' was advised everywhere but none of them worked properly. And relying on AVPlayer's 'readyToPlay' status was incorrect as well - the player changes its status to it as soon as possible and not when actually starting playing the stream.

Thankfully I got to know about another key to look after, the 'currentItem.loadedTimeRanges'. And here's how you use it:

First, you add the observer (and then don't forget to remove it at the end of the player's lifecycle!):

avPlayer.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)

And then you just observe it!

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if object as? AVPlayer === avPlayer && keyPath == "currentItem.loadedTimeRanges" {
            if let timeRanges = avPlayer.currentItem?.loadedTimeRanges, timeRanges.count > 0 {
                let timeRange = timeRanges.first as! CMTimeRange
                let currentBufferDuration = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration))
                let duration = avPlayer.currentItem!.asset.duration
                let seconds = CMTimeGetSeconds(duration)
                
                if currentBufferDuration > 2 || currentBufferDuration == seconds {
                    delegate.startedPlayingStream()
                }
                
            }
}

Next the player calls its delegate when the stream actually started playing. Neat! 🙂