120

Is there a way to know whether an AVPlayer playback has stalled or reached the end?

hariszaman
  • 8,202
  • 2
  • 40
  • 59
Egil
  • 4,265
  • 4
  • 20
  • 13

12 Answers12

358

You can tell it's playing using:

AVPlayer *player = ...
if ((player.rate != 0) && (player.error == nil)) {
    // player is playing
}

Swift 3 extension:

extension AVPlayer {
    var isPlaying: Bool {
        return rate != 0 && error == nil
    }
}
kelin
  • 11,323
  • 6
  • 67
  • 104
maz
  • 8,056
  • 4
  • 26
  • 25
  • 34
    Not necessarily, it doesn't handle situations where the player stalls due an error in the file. Something I found out the hard way... – Dermot May 07 '13 at 12:53
  • 7
    As Dermot said, if you try to play something while in airplane mode, the AVPlayer rate is still set to 1.0, since it implies the intention to play. – phi May 31 '13 at 13:13
  • Is there a better solution? – openfrog Oct 11 '13 at 10:06
  • 4
    The AVPlayer has an error property, just check that it isn't nil as well as checking the rate isn't 0 :) – James Campbell Mar 07 '14 at 09:56
  • 26
    Note that the answer has been updated to prevent @Dermot scenario. So this one actually works. – jomafer Jun 03 '15 at 14:40
  • 2
    Will not be working when playing video footage reverse (`- [AVPlayer setRate:-1.0]`), because `-1.0` is less than 0. – Julian F. Weinert Jul 23 '15 at 15:06
  • note that when using the above example with Swift, `player.error` is an optional not a bool, and so for this to compile, you'll need to change it to `player.rate > 0 && (player.error == nil)` – Jesse Dec 27 '15 at 05:12
  • 1
    This answer does NOT distinguish between "stalled" and "ended"! I think combine this answer with maxkonovalov's answer. And note Julian's comment, the test should be `player.rate != 0`, so that playing in reverse isn't considered stopped. – ToolmakerSteve Feb 05 '16 at 17:20
  • This will also not work after `player.replaceCurrentItem(with: nil)` when player is actually not playing, but `player.rate` will still be != 0 – oleynikd Nov 02 '16 at 14:38
  • 1
    I had the player stuck at rat 1.0 although I was not in airplane mode. So there some other condition that need to be included in the test. – Alex Nov 14 '16 at 07:26
  • Very useful addition to my `Macros.m`, thank you. `#define isAVPlayerPlaying(player) ((player.rate != 0) && (player.error == nil))` – Albert Renshaw Oct 27 '17 at 23:22
77

In iOS10, there's a built in property for this now: timeControlStatus

For example, this function plays or pauses the avPlayer based on it's status and updates the play/pause button appropriately.

@IBAction func btnPlayPauseTap(_ sender: Any) {
    if aPlayer.timeControlStatus == .playing {
        aPlayer.pause()
        btnPlay.setImage(UIImage(named: "control-play"), for: .normal)
    } else if aPlayer.timeControlStatus == .paused {
        aPlayer.play()
        btnPlay.setImage(UIImage(named: "control-pause"), for: .normal)
    }
}

As for your second question, to know if the avPlayer reached the end, the easiest thing to do would be to set up a notification.

NotificationCenter.default.addObserver(self, selector: #selector(self.didPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)

When it gets to the end, for example, you can have it rewind to the beginning of the video and reset the Pause button to Play.

@objc func didPlayToEnd() {
    aPlayer.seek(to: CMTimeMakeWithSeconds(0, 1))
    btnPlay.setImage(UIImage(named: "control-play"), for: .normal)
}

These examples are useful if you're creating your own controls, but if you use a AVPlayerViewController, then the controls come built in.

Travis M.
  • 10,930
  • 1
  • 56
  • 72
59

To get notification for reaching the end of an item (via Apple):

[[NSNotificationCenter defaultCenter] 
      addObserver:<self>
      selector:@selector(<#The selector name#>)
      name:AVPlayerItemDidPlayToEndTimeNotification 
      object:<#A player item#>];

And to track playing you can:

"track changes in the position of the playhead in an AVPlayer object" by using addPeriodicTimeObserverForInterval:queue:usingBlock: or addBoundaryTimeObserverForTimes:queue:usingBlock:.

Example is from Apple:

// Assume a property: @property (retain) id playerObserver;

Float64 durationSeconds = CMTimeGetSeconds([<#An asset#> duration]);
CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 1);
CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 1);
NSArray *times = [NSArray arrayWithObjects:[NSValue valueWithCMTime:firstThird], [NSValue valueWithCMTime:secondThird], nil];

self.playerObserver = [<#A player#> addBoundaryTimeObserverForTimes:times queue:NULL usingBlock:^{
    // Passing NULL for the queue specifies the main queue.

    NSString *timeDescription = (NSString *)CMTimeCopyDescription(NULL, [self.player currentTime]);
    NSLog(@"Passed a boundary at %@", timeDescription);
    [timeDescription release];
}];
Todd Hopkinson
  • 6,803
  • 5
  • 32
  • 34
  • You could throw in a Boolean flag to check the play status. –  Jun 19 '15 at 23:01
  • Notifications might cause problems if you change player's item, for example, with `-replaceCurrentItemWithPlayerItem:`, and don't handle it properly. A more reliable way for me is to track `AVPlayer`'s status using KVO. See my answer below for more details: http://stackoverflow.com/a/34321993/3160561 – maxkonovalov Dec 16 '15 at 21:04
  • 2
    Also need notification of error? `AVPlayerItemFailedToPlayToEndTimeNotification` – ToolmakerSteve Feb 05 '16 at 17:32
25

rate is NOT the way to check whether a video is playing (it could stalled). From documentation of rate:

Indicates the desired rate of playback; 0.0 means "paused", 1.0 indicates a desire to play at the natural rate of the current item.

Key words "desire to play" - a rate of 1.0 does not mean the video is playing.

The solution since iOS 10.0 is to use AVPlayerTimeControlStatus which can be observed on AVPlayer timeControlStatus property.

The solution prior to iOS 10.0 (9.0, 8.0 etc.) is to roll your own solution. A rate of 0.0 means that the video is paused. When rate != 0.0 it means that the video is either playing or is stalled.

You can find out the difference by observing player time via: func addPeriodicTimeObserver(forInterval interval: CMTime, queue: DispatchQueue?, using block: @escaping (CMTime) -> Void) -> Any

The block returns the current player time in CMTime, so a comparison of lastTime (the time that was last received from the block) and currentTime (the time that the block just reported) will tell whether the player is playing or is stalled. For example, if lastTime == currentTime and rate != 0.0, then the player has stalled.

As noted by others, figuring out whether playback has finished is indicated by AVPlayerItemDidPlayToEndTimeNotification.

kgaidis
  • 14,259
  • 4
  • 79
  • 93
18

For Swift:

AVPlayer:

let player = AVPlayer(URL: NSURL(string: "http://www.sample.com/movie.mov"))
if (player.rate != 0 && player.error == nil) {
   println("playing")
}

Update:
player.rate > 0 condition changed to player.rate != 0 because if video is playing in reverse it can be negative thanks to Julian for pointing out.
Note: This might look same as above(Maz's) answer but in Swift '!player.error' was giving me a compiler error so you have to check for error using 'player.error == nil' in Swift.(because error property is not of 'Bool' type)

AVAudioPlayer:

if let theAudioPlayer =  appDelegate.audioPlayer {
   if (theAudioPlayer.playing) {
       // playing
   }
}

AVQueuePlayer:

if let theAudioQueuePlayer =  appDelegate.audioPlayerQueue {
   if (theAudioQueuePlayer.rate != 0 && theAudioQueuePlayer.error == nil) {
       // playing
   }
}
Aks
  • 8,181
  • 5
  • 37
  • 38
  • 3
    AVPlayer != AVAudioPlayer – quemeful Jan 27 '15 at 12:44
  • 2
    Caveats: all mentioned above in the answer that showed up two years before this one. – Dan Rosenstark Feb 18 '15 at 03:23
  • 1
    @Yar I know also upvoted above answer but this is for swift and in Swift '!player.error' was not working for me it is allowed only for bool types so I have added the answer oops upvoted your comment by mistake to give reply using this stack overflow app :) – Aks Feb 18 '15 at 03:43
  • My concern is that I don't know how well the player.error not being nil actually works. – Dan Rosenstark Feb 18 '15 at 03:46
  • 1
    Will not be working when playing video footage reverse (`player.rate = -1.0`), because -1.0 is less than 0. – Julian F. Weinert Jul 23 '15 at 15:07
18

A more reliable alternative to NSNotification is to add yourself as observer to player's rate property.

[self.player addObserver:self
              forKeyPath:@"rate"
                 options:NSKeyValueObservingOptionNew
                 context:NULL];

Then check if the new value for observed rate is zero, which means that playback has stopped for some reason, like reaching the end or stalling because of empty buffer.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"rate"]) {
        float rate = [change[NSKeyValueChangeNewKey] floatValue];
        if (rate == 0.0) {
            // Playback stopped
        } else if (rate == 1.0) {
            // Normal playback
        } else if (rate == -1.0) {
            // Reverse playback
        }
    }
}

For rate == 0.0 case, to know what exactly caused the playback to stop, you can do the following checks:

if (self.player.error != nil) {
    // Playback failed
}
if (CMTimeGetSeconds(self.player.currentTime) >=
    CMTimeGetSeconds(self.player.currentItem.duration)) {
    // Playback reached end
} else if (!self.player.currentItem.playbackLikelyToKeepUp) {
    // Not ready to play, wait until enough data is loaded
}

And don't forget to make your player stop when it reaches the end:

self.player.actionAtItemEnd = AVPlayerActionAtItemEndPause;

maxkonovalov
  • 3,651
  • 34
  • 36
  • I think also need notification of failure to reach end - `AVPlayerItemFailedToPlayToEndTimeNotification`. If this error happens, will never reach the end time. Or reading maz's answer, add to your code a check for `player.error != nil`, in which case playback has "ended" due to error. – ToolmakerSteve Feb 05 '16 at 17:15
  • BTW, what did you find "not reliable" about using NSNotification of `AVPlayerItemDidPlayToEndTimeNotification`? – ToolmakerSteve Feb 05 '16 at 17:31
  • ToolmakerSteve, thanks for your note, added error check to my answer. For not reliable notifications - I came across the issue myself while implementing repeating and reusable player views in collection view, and KVO made things more clear, as it allowed to keep it all more local without different players' notifications interfering with each other. – maxkonovalov Feb 08 '16 at 16:05
15

Currently with swift 5 the easiest way to check if the player is playing or paused is to check the .timeControlStatus variable.

player.timeControlStatus == .paused
player.timeControlStatus == .playing
azemi
  • 341
  • 3
  • 7
  • 1
    This is not the right way to do this. The question wants to know when the player has reached the end. The user pausing playback will also activate this function, which will lead to unintended effects. – FontFamily Jun 01 '21 at 17:34
10

Swift extension based on the answer by maz

extension AVPlayer {

    var isPlaying: Bool {
        return ((rate != 0) && (error == nil))
    }
}
Mark Bridges
  • 8,228
  • 4
  • 50
  • 65
7

Answer in Objective C

if (player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
    //player is playing
}
else if (player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
    //player is pause
}
else if (player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate) {
    //player is waiting to play
}
iOS Lifee
  • 2,091
  • 23
  • 32
6

The Swift version of maxkonovalov's answer is this:

player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.New, context: nil)

and

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    if keyPath == "rate" {
        if let rate = change?[NSKeyValueChangeNewKey] as? Float {
            if rate == 0.0 {
                print("playback stopped")
            }
            if rate == 1.0 {
                print("normal playback")
            }
            if rate == -1.0 {
                print("reverse playback")
            }
        }
    }
}

Thank you maxkonovalov!

Mr Stanev
  • 1,662
  • 1
  • 19
  • 26
  • Hello Mr Stanev, thanks for your answer. This doesn't work for me (aka doesn't print anything in the console?) – Cesare Aug 21 '17 at 12:51
  • Nevermind, it does work – my player was `nil`! But I need to know when the view starts (not ends). Any way to do that? – Cesare Aug 21 '17 at 12:53
4
player.timeControlStatus == AVPlayer.TimeControlStatus.playing
  • 2
    This provided answer may be correct, but **it could benefit from an explanation**. Code only answers are not considered "good" answers. Here are some guidelines for [How do I write a good answer?](https://stackoverflow.com/help/how-to-answer). From [review](https://stackoverflow.com/review). – MyNameIsCaleb Sep 28 '19 at 19:44
0

You can check if the player is playing with a timer 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