41

I want to detect if my AVPlayer is buffering for the current location, so that I can show a loader or something. But I can't seem to find anything in the documentation for AVPlayer.

vrwim
  • 13,020
  • 13
  • 63
  • 118

15 Answers15

53

You can observe the values of your player.currentItem:

playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .New, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .New, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .New, context: nil)

then

override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    if object is AVPlayerItem {
        switch keyPath {
            case "playbackBufferEmpty":
               // Show loader

            case "playbackLikelyToKeepUp":
                // Hide loader

            case "playbackBufferFull":
                // Hide loader
        }
    }
}
Marco Santarossa
  • 4,058
  • 1
  • 29
  • 49
19

The accepted answer didn't work for me, I used the code below to show the loader efficiently.

Swift 3

//properties 
var observer:Any!
var player:AVPlayer!


self.observer = self.player.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 600), queue: DispatchQueue.main) {
    [weak self] time in

    if self?.player.currentItem?.status == AVPlayerItemStatus.readyToPlay {

        if let isPlaybackLikelyToKeepUp = self?.player.currentItem?.isPlaybackLikelyToKeepUp {
            //do what ever you want with isPlaybackLikelyToKeepUp value, for example, show or hide a activity indicator.
        }
    }
}
aytek
  • 1,842
  • 24
  • 32
  • Dear @aytek, would you please be so kind and translate your solution to `Swift 4`? :) – ixany Aug 31 '17 at 09:41
  • Dear @ixany, I didn't have a chance to install the latest build of Xcode. I'll add the Swift 4 version as soon as possible. Thanks for your comment. – aytek Sep 25 '17 at 12:30
  • What I have observed is that you have to register to those observers on the AVPlayerItem instance and not the AVPlayer's one, otherwise it doesn't work. In fact, the accepted answer does that. – fasteque Nov 07 '17 at 14:42
19

For me above accepted answer didn't worked but this method does.You can use timeControlStatus but it is available only above iOS 10.

According to apple's official documentation

A status that indicates whether playback is currently in progress, paused indefinitely, or suspended while waiting for appropriate network conditions

Add this observer to the player.

player.addObserver(self, forKeyPath: “timeControlStatus”, options: [.old, .new], context: nil)

Then,Observe the changes in

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

method.Use below code inside above method

override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "timeControlStatus", let change = change, let newValue = change[NSKeyValueChangeKey.newKey] as? Int, let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
        let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
        let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)
        if newStatus != oldStatus {
            DispatchQueue.main.async {[weak self] in
                if newStatus == .playing || newStatus == .paused {
                    self?.loaderView.isHidden = true
                } else {
                    self?.loaderView.isHidden = false
                }
            }
        }
    }
}

This is tested on iOS 11 above with swift 4 and It is working.

Mohit Kumar
  • 669
  • 5
  • 8
7

Swift 4 observations:

var playerItem: AVPlayerItem?
var playbackLikelyToKeepUpKeyPathObserver: NSKeyValueObservation?
var playbackBufferEmptyObserver: NSKeyValueObservation?
var playbackBufferFullObserver: NSKeyValueObservation?

private func observeBuffering() {
    let playbackBufferEmptyKeyPath = \AVPlayerItem.playbackBufferEmpty
    playbackBufferEmptyObserver = playerItem?.observe(playbackBufferEmptyKeyPath, options: [.new]) { [weak self] (_, _) in
        // show buffering
    }

    let playbackLikelyToKeepUpKeyPath = \AVPlayerItem.playbackLikelyToKeepUp
    playbackLikelyToKeepUpKeyPathObserver = playerItem?.observe(playbackLikelyToKeepUpKeyPath, options: [.new]) { [weak self] (_, _) in
        // hide buffering
    }

    let playbackBufferFullKeyPath = \AVPlayerItem.playbackBufferFull
    playbackBufferFullObserver = playerItem?.observe(playbackBufferFullKeyPath, options: [.new]) { [weak self] (_, _) in
        // hide buffering
    }
}

Observers need to be removed after we are done observing.

To remove these three observers just set playbackBufferEmptyObserver, playbackLikelyToKeepUpKeyPathObserver and playbackBufferFullObserver to nil.

No need to remove them manually (this is specific for observe<Value>(_ keyPath:, options:, changeHandler:) method.

Crt Gregoric
  • 392
  • 1
  • 5
  • 12
Roman Barzyczak
  • 3,785
  • 1
  • 30
  • 44
7

#Updated in Swift 4 and worked fine

As through i have gone with accepted answer but didn't work in swift 4 for me so after certain research i have found this thinks from apple doc. There are two way to determine AVPlayer states that are,

  1. addPeriodicTimeObserverForInterval:queue:usingBlock: and
  2. addBoundaryTimeObserverForTimes:queue:usingBlock:

and using ways is like this

var observer:Any?
var avplayer : AVPlayer?

func preriodicTimeObsever(){

        if let observer = self.observer{
            //removing time obse
            avplayer?.removeTimeObserver(observer)
            observer = nil
        }

        let intervel : CMTime = CMTimeMake(1, 10)
        observer = avplayer?.addPeriodicTimeObserver(forInterval: intervel, queue: DispatchQueue.main) { [weak self] time in

            guard let `self` = self else { return }

            let sliderValue : Float64 = CMTimeGetSeconds(time)
           //this is the slider value update if you are using UISlider.

            let playbackLikelyToKeepUp = self.avPlayer?.currentItem?.isPlaybackLikelyToKeepUp
            if playbackLikelyToKeepUp == false{

               //Here start the activity indicator inorder to show buffering
            }else{
                //stop the activity indicator 
            }
        }
    }

And Don't forget to kill time observer to save from memory leak. method for killing instance, add this method according to your need but i have used it in viewWillDisappear method.

       if let observer = self.observer{

            self.avPlayer?.removeTimeObserver(observer)
            observer = nil
        }
Amrit Tiwari
  • 922
  • 7
  • 21
5

In Swift 5.3

Vars:

private var playerItemBufferEmptyObserver: NSKeyValueObservation?
private var playerItemBufferKeepUpObserver: NSKeyValueObservation?
private var playerItemBufferFullObserver: NSKeyValueObservation?

AddObservers

playerItemBufferEmptyObserver = player.currentItem?.observe(\AVPlayerItem.isPlaybackBufferEmpty, options: [.new]) { [weak self] (_, _) in
    guard let self = self else { return }
    self.showLoadingIndicator(over: self)
}
    
playerItemBufferKeepUpObserver = player.currentItem?.observe(\AVPlayerItem.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] (_, _) in
    guard let self = self else { return }
    self.dismissLoadingIndicator()
}
    
playerItemBufferFullObserver = player.currentItem?.observe(\AVPlayerItem.isPlaybackBufferFull, options: [.new]) { [weak self] (_, _) in
    guard let self = self else { return }
    self.dismissLoadingIndicator()
}

RemoveObservers

playerItemBufferEmptyObserver?.invalidate()
playerItemBufferEmptyObserver = nil
    
playerItemBufferKeepUpObserver?.invalidate()
playerItemBufferKeepUpObserver = nil
    
playerItemBufferFullObserver?.invalidate()
playerItemBufferFullObserver = nil
Reimond Hill
  • 4,278
  • 40
  • 52
  • These observer are never trigger. Do you know why ? – Makaille Mar 10 '22 at 13:31
  • Where do you declare the properties? – Reimond Hill Mar 11 '22 at 08:40
  • In init of the class that manage the AvPlayer . I call a function that init all observers then I call the call logic to create an AVPlayerItem to stream into the player. The observers (currentItem.status, actionItemAdded and rate) are triggering but not these. – Makaille Mar 14 '22 at 15:38
  • Only solution that worked for me on iOS 16 and AVPlayer wrapped in SwiftUI's VideoPlayer. Without this, could not find a way to show an indicator. Thank you. – jusko Apr 23 '23 at 23:01
4

Updated for Swift 4.2

    var player : AVPlayer? = nil

    let videoUrl = URL(string: "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.mp4")
    self.player = AVPlayer(url: videoUrl!)
    self.player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 600), queue: DispatchQueue.main, using: { time in

        if self.player?.currentItem?.status == AVPlayerItem.Status.readyToPlay {

            if let isPlaybackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp {
                //do what ever you want with isPlaybackLikelyToKeepUp value, for example, show or hide a activity indicator.

                //MBProgressHUD.hide(for: self.view, animated: true)
            }
        }
    })
xuzepei
  • 1,119
  • 11
  • 9
2

Hmm, the accepted solution didn't work for me and the periodic observer solutions seem heavy handed.

Here's my suggestion, observe timeControlerStatus on AVPlayer.

// Add observer
player.addObserver(self,
                   forKeyPath: #keyPath(AVPlayer.timeControlStatus),
                   options: [.new],
                   context: &playerItemContext)

// At some point you'll need to remove yourself as an observer otherwise
// your app will crash 
self.player?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.timeControlStatus))

// handle keypath callback
if keyPath == #keyPath(AVPlayer.timeControlStatus) {
    guard let player = self.player else { return }
    if let isPlaybackLikelyToKeepUp = player.currentItem?.isPlaybackLikelyToKeepUp,
        player.timeControlStatus != .playing && !isPlaybackLikelyToKeepUp {
        self.playerControls?.loadingStatusChanged(true)
    } else {
        self.playerControls?.loadingStatusChanged(false)
    }
}
SamB
  • 2,621
  • 4
  • 34
  • 39
2

We can directly Observe Playback State using the state observer method once is there any playback state changes it will be notified, it's a really easy way and it's tested with swift 5 and iOS 13.0+

var player: AVPlayer!

player.currentItem!.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)

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

    if (player.currentItem?.isPlaybackLikelyToKeepUp ?? false) {
        // End Buffering
    } else {
        // Buffering is in progress
    }
}

Apple Doc Reference

Nimantha
  • 6,405
  • 6
  • 28
  • 69
chirag05k
  • 39
  • 5
1

Solution for Xamarin inspired by Marco's answer

// KVO registrations
private void Initialize()
{
    playbackBufferEmptyObserver?.Dispose();
    playbackBufferEmptyObserver = (NSObject)playerItem.AddObserver("playbackBufferEmpty",
        NSKeyValueObservingOptions.New,
        AVPlayerItem_BufferUpdated);

    playbackLikelyToKeepUpObserver?.Dispose();
    playbackLikelyToKeepUpObserver = (NSObject)playerItem.AddObserver("playbackLikelyToKeepUp",
        NSKeyValueObservingOptions.New,
        AVPlayerItem_BufferUpdated);

    playbackBufferFullObserver?.Dispose();
    playbackBufferFullObserver = (NSObject)playerItem.AddObserver("playbackBufferFull",
        NSKeyValueObservingOptions.New,
        AVPlayerItem_BufferUpdated);
}

private void AVPlayerItem_BufferUpdated(NSObservedChange e)
{
    ReportVideoBuffering();
}

private void ReportVideoBuffering()
{
    // currentPlayerItem is the current AVPlayerItem of AVPlayer
    var isBuffering = !currentPlayerItem.PlaybackLikelyToKeepUp;
    // NOTE don't make "buffering" as one of your PlayerState.
    // Treat it as a separate property instead. Learned this the hard way.
    Buffering?.Invoke(this, new BufferingEventArgs(isBuffering));
}
mr5
  • 3,438
  • 3
  • 40
  • 57
1

Please note that

Use a weak reference to self in the callback block to prevent creating a retain cycle.

func playRemote(url: URL) {
            showSpinner()
            let playerItem = AVPlayerItem(url: url)
            avPlayer = AVPlayer(playerItem: playerItem)
            avPlayer.rate = 1.0
            avPlayer.play()
            self.avPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1,
     timescale: 600), queue: DispatchQueue.main, using: { [weak self] time in
                if self?.avPlayer.currentItem?.status == AVPlayerItem.Status.readyToPlay {
                    if let isPlaybackLikelyToKeepUp = self?.avPlayer.currentItem?.isPlaybackLikelyToKeepUp { 
                        self?.removeSpinner()
                    }
                }
            })
        }
}
fullmoon
  • 8,030
  • 5
  • 43
  • 58
1

Using Combine you can easily subscribe to the publisher for when an AVPlayerItem is buffering or not like so:

// Subscribe to this and update your `View` appropriately
@Published var isBuffering = false
private var observation: AnyCancellable?

observation = avPlayer?.currentItem?.publisher(for: \.isPlaybackBufferEmpty).sink(receiveValue: { [weak self] isBuffering in
  self?.isBuffering = isBuffering
})
M3nd3z
  • 316
  • 2
  • 12
0

Here is a simple method, that works with Swift 5.

This will add the loadingIndicator when your player is stalled

NotificationCenter.default.addObserver(self, selector:
#selector(playerStalled(_:)), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: self.player?.currentItem)

@objc func playerStalled(_ notification: Notification){
    self.loadingIndicator.isHidden = false
    self.playPauseButton.isHidden = true
}

This will show loader Indicator when buffer is empty:

if let isPlayBackBufferEmpty = self.player?.currentItem?.isPlaybackBufferEmpty{
    if isPlayBackBufferEmpty{
        self.loadingIndicator.isHidden = false
        self.playPauseButton.isHidden = true
    }
}

This will hide the loader when player is ready to play:

if self.playerItem?.status == AVPlayerItem.Status.readyToPlay{
    if let isPlaybackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp {
        if isPlaybackLikelyToKeepUp{
            self.loadingIndicator.isHidden = true
            self.playPauseButton.isHidden = false
        }
    }
}
Asis
  • 683
  • 3
  • 23
0

You can check if the player is buffering/loading like this:

let playerObserver = self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main, using: { [weak self] time in
        if self?.player.timeControlStatus == .playing {
            debugPrint("#player - info: isPlaying")
            self?.playButton.isSelected = true
        } else if self?.player.timeControlStatus == .paused {
            debugPrint("#player - info: isPaused")
            self?.playButton.isSelected = false
        } else if self?.player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
            debugPrint("#player - info: isWaiting") //Buffering
        }
    })
Mikkel Cortnum
  • 481
  • 4
  • 11
0

For RXswift fans, you can check AVPlayer's buffering state by adding an extension to the Reactive class:

extension Reactive where Base: AVPlayerItem {
    public var playbackBufferEmpty: Observable<Bool> {
        return self.observe(Bool.self, #keyPath(AVPlayerItem.isPlaybackBufferEmpty))
            .map { $0 ?? false }
    }
}

And use it as follows:

   avPlayerItem.rx.playbackBufferEmpty
   .subscribe(onNext: {isLoading in

      //Do whatever you want

    }).disposed(by: disposeBag)