89

I'm trying to play an MP3 file that is passed to an UIView from a previous UIView (stored in a NSURL *fileURL variable).

I'm initializing an AVPlayer with:

player = [AVPlayer playerWithURL:fileURL];

NSLog(@"Player created:%d",player.status);

The NSLog prints Player created:0, which i figured means it is not ready to play yet.

When i click the play UIButton, the code i run is:

-(IBAction)playButtonClicked
{
    NSLog(@"Clicked Play. MP3:%@",[fileURL absoluteString]);

    if(([player status] == AVPlayerStatusReadyToPlay) && !isPlaying)
//  if(!isPlaying)
    {
        [player play];
        NSLog(@"Playing:%@ with %d",[fileURL absoluteString], player.status);
        isPlaying = YES;
    }
    else if(isPlaying)
    {

        [player pause];
        NSLog(@"Pausing:%@",[fileURL absoluteString]);
        isPlaying = NO;
    }
    else {
        NSLog(@"Error in player??");
    }

}

When i run this, I always get Error in player?? in the console. If i however replace the if condition that checks if AVPlayer is ready to play, with a simple if(!isPlaying)..., then the music plays the SECOND TIME I click on the play UIButton.

The console log is:

Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 0**

Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Pausing:http://www.nimh.nih.gov/audio/neurogenesis.mp3

Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
2011-03-23 11:06:43.674 Podcasts[2050:207] Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 1**

I see that the SECOND TIME, the player.status seems to hold 1, which I'm guessing is AVPlayerReadyToPlay.

What can I do to have the playing to work properly the first time i click the play UIButton? (ie, how can i make sure the AVPlayer is not just created, but also ready to play?)

mvishnu
  • 1,150
  • 1
  • 12
  • 15

10 Answers10

130

You are playing a remote file. It may take some time for the AVPlayer to buffer enough data and be ready to play the file (see AV Foundation Programming Guide)

But you don't seem to wait for the player to be ready before tapping the play button. What I would to is disable this button and enable it only when the player is ready.

Using KVO, it's possible to be notified for changes of the player status:

playButton.enabled = NO;
player = [AVPlayer playerWithURL:fileURL];
[player addObserver:self forKeyPath:@"status" options:0 context:nil];   

This method will be called when the status changes:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
    if (object == player && [keyPath isEqualToString:@"status"]) {
        if (player.status == AVPlayerStatusReadyToPlay) {
            playButton.enabled = YES;
        } else if (player.status == AVPlayerStatusFailed) {
            // something went wrong. player.error should contain some information
        }
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Jilouc
  • 12,684
  • 4
  • 46
  • 43
  • Thank you!! That worked like a charm. (should have guessed though when i saw it was playing offline files without a problem) – mvishnu Mar 23 '11 at 10:10
  • There are some URL that just don't play, they exists but they don't work (as example iTunes will not play them too). How do you manage that behavior? There's no timeout in AVPlayer. – Fabrizio Mar 19 '12 at 13:25
  • @Fabrizio `player.status` should change to `AVPlayerStatusFailed` then (see the edited answer). – Jilouc Mar 19 '12 at 14:03
  • AVPlayerStatusFailed or AVPlayerStatusUnknown is never called. Just to be clear the URL I try to play is: http://audioplayer.wunderground.com:80/RHBrant/Cheyenne.mp3.m3u. I've added an observer to my appDelegate.player but the status is always Ready To Play. – Fabrizio Mar 19 '12 at 17:39
  • 12
    In my experience `player.currentItem.status` is accurate when `player.status` is not. Not sure what the differences is. – bendytree Jul 14 '12 at 00:01
  • I am doing the same thing and it is working fine on IOS6 but on IOS7 the observeValueForKeyPath is not getting called ... any idea ? – Sosily Sep 29 '13 at 15:29
  • @Sosily Did you get solution. I am facing the same problem. in iOS 6 it works perfectly but in iOS 7 observeValueForKeyPath not getting called. – iOSAppDev Jan 07 '14 at 05:54
  • 1
    @iOSAppDev On IOS7 use AVPlayerItem addObserver – Peter Zhao May 31 '16 at 10:05
  • 7
    wow, this AVPlayer is so poorly designed that makes me cry. Why not adding a onLoad handler block? Come on Apple, simplify your stuff! – Duck Sep 28 '16 at 09:14
44

Swift Solution

var observer: NSKeyValueObservation?

func prepareToPlay() {
    let url = <#Asset URL#>
    // Create asset to be played
    let asset = AVAsset(url: url)
    
    let assetKeys = [
        "playable",
        "hasProtectedContent"
    ]
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    let playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: assetKeys)
    
    // Register as an observer of the player item's status property
    self.observer = playerItem.observe(\.status, options:  [.new, .old], changeHandler: { (playerItem, change) in
        if playerItem.status == .readyToPlay {
            //Do your work here
        }
    })

    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)
}

Also you can invalidate the observer this way

self.observer.invalidate()

Important: You must keep the observer variable retained otherwise it will deallocate and the changeHandler will no longer get called. So don't define the observer as a function variable but define it as a instance variable like the given example.

This key value observer syntax is new to Swift 4.

For more information, see here https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift

Community
  • 1
  • 1
Josh Bernfeld
  • 4,246
  • 2
  • 32
  • 35
32

I had a lot of trouble trying to figure out the status of an AVPlayer. The status property didn't always seem to be terribly helpful, and this led to endless frustration when I was trying to handle audio session interruptions. Sometimes the AVPlayer told me it was ready to play (with AVPlayerStatusReadyToPlay) when it didn't actually seem to be. I used Jilouc's KVO method, but it didn't work in all cases.

To supplement, when the status property wasn't being useful, I queried the amount of the stream that the AVPlayer had loaded by looking at the loadedTimeRanges property of the AVPlayer's currentItem (which is an AVPlayerItem).

It's all a little confusing, but here's what it looks like:

NSValue *val = [[[audioPlayer currentItem] loadedTimeRanges] objectAtIndex:0];
CMTimeRange timeRange;
[val getValue:&timeRange];
CMTime duration = timeRange.duration;
float timeLoaded = (float) duration.value / (float) duration.timescale; 

if (0 == timeLoaded) {
    // AVPlayer not actually ready to play
} else {
    // AVPlayer is ready to play
}
Tim Arnold
  • 8,359
  • 8
  • 44
  • 67
  • 2
    There are additions to the NSValue type coming with AV Foundation. Some of those helpers allow you to convert back and forth from NSValue to CMTimeXxx values. Like [CMTimeRangeValue](http://developer.apple.com/library/mac/documentation/AVFoundation/Reference/NSValue_AVFoundation_Additions/Reference/Reference.html#//apple_ref/occ/instm/NSValue/CMTimeRangeValue). – superjos May 17 '12 at 22:31
  • Similar story for getting seconds (I guess that's what `timeLoaded` is) out of CMTime: [CMTimeGetSeconds](https://developer.apple.com/library/mac/documentation/CoreMedia/Reference/CMTime/Reference/reference.html#//apple_ref/c/func/CMTimeGetSeconds) – superjos May 17 '12 at 22:34
  • 2
    Unfortunately, this should be an accepted answer. `AVPlayer` seems to set `status == AVPlayerStatusReadyToPlay` too early when it is not ready to play really. To make this work, you can wrap the above code in `NSTimer` invocation, for example. – maxkonovalov Nov 26 '15 at 14:15
  • Could it be the case where there are over (w.l.o.g) 2 seconds of loaded time range, but the player's or playerItem's status isn't ReadyToPlay? IOW, should it be confirmed as well? – danielhadar Jul 03 '18 at 15:48
17
private var playbackLikelyToKeepUpContext = 0

For register observer

avPlayer.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
        options: .new, context: &playbackLikelyToKeepUpContext)

Listen the observer

 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if context == &playbackLikelyToKeepUpContext {
        if avPlayer.currentItem!.isPlaybackLikelyToKeepUp {
           // loadingIndicatorView.stopAnimating() or something else
        } else {
           // loadingIndicatorView.startAnimating() or something else
        }
    }
}

For remove observer

deinit {
    avPlayer.removeObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp")
}

The key point in the code is instance property isPlaybackLikelyToKeepUp.

Harman
  • 426
  • 8
  • 14
  • 5
    Good answer. I would improve KVO with `forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)` – Miroslav Hrivik Jun 01 '18 at 12:57
  • For 2019, **this does work perfectly** - copy and paste :) I did use the mod of @MiroslavHrivik , thanks! – Fattie Aug 27 '19 at 22:00
  • @MiroslavHrivik with his correction in the implementation it works in 2021, thank you – StackGU Jan 16 '21 at 11:15
  • @StackGU If I have a player inside of a VideoCell item, should I put this answer's code inside of this VideoCell class or should I create a new class for the observer? I'm a newbie and somehow confused with KVO in Swift – Benjamín Cáceres Ramirez Jun 15 '21 at 14:22
  • @BenjamínCáceresRamirez put it inside the ```collectionViewCell``` but remember to remove the observer inside the ```prepareForReuse()``` method – StackGU Jun 15 '21 at 16:02
11

After researching a lot and try many ways I've noticed that normally the status observer is not the better for know really when AVPlayer object is ready to play, because the object can be ready for play but this not that mean it will be play immediately.

The better idea for know this is with loadedTimeRanges.

For Register observer

[playerClip addObserver:self forKeyPath:@"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

Listen the observer

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == playerClip && [keyPath isEqualToString:@"currentItem.loadedTimeRanges"]) {
        NSArray *timeRanges = (NSArray*)[change objectForKey:NSKeyValueChangeNewKey];
        if (timeRanges && [timeRanges count]) {
            CMTimeRange timerange=[[timeRanges objectAtIndex:0]CMTimeRangeValue];
            float currentBufferDuration = CMTimeGetSeconds(CMTimeAdd(timerange.start, timerange.duration));
            CMTime duration = playerClip.currentItem.asset.duration;
            float seconds = CMTimeGetSeconds(duration);

            //I think that 2 seconds is enough to know if you're ready or not
            if (currentBufferDuration > 2 || currentBufferDuration == seconds) {
                // Ready to play. Your logic here
            }
        } else {
            [[[UIAlertView alloc] initWithTitle:@"Alert!" message:@"Error trying to play the clip. Please try again" delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil] show];
        }
    }
}

For remove observer (dealloc, viewWillDissapear or before register observer) its a good places for called

- (void)removeObserverForTimesRanges
{
    @try {
        [playerClip removeObserver:self forKeyPath:@"currentItem.loadedTimeRanges"];
    } @catch(id anException){
        NSLog(@"excepcion remove observer == %@. Remove previously or never added observer.",anException);
        //do nothing, obviously it wasn't attached because an exception was thrown
    }
}
jose920405
  • 7,982
  • 6
  • 45
  • 71
  • thanks, this worked for me as well. I however did not use the "currentBufferDuration == seconds" evaluation. Could you please tell me what it's used for? – andrei Dec 20 '17 at 10:45
  • For cases when `currentBufferDuration < 2` – jose920405 Dec 20 '17 at 15:42
  • 1
    Could it be the case where there are over (w.l.o.g) 2 seconds of loaded time range, but the player's or playerItem's status isn't ReadyToPlay? IOW, should it be confirmed as well? – danielhadar Jul 03 '18 at 15:44
7

Based on Tim Camber answer, here is the Swift function I use :

private func isPlayerReady(_ player:AVPlayer?) -> Bool {

    guard let player = player else { return false }

    let ready = player.status == .readyToPlay

    let timeRange = player.currentItem?.loadedTimeRanges.first as? CMTimeRange
    guard let duration = timeRange?.duration else { return false } // Fail when loadedTimeRanges is empty
    let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
    let loaded = timeLoaded > 0

    return ready && loaded
}

Or, as an extension

extension AVPlayer {
    var ready:Bool {
        let timeRange = currentItem?.loadedTimeRanges.first as? CMTimeRange
        guard let duration = timeRange?.duration else { return false }
        let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
        let loaded = timeLoaded > 0

        return status == .readyToPlay && loaded
    }
}
Community
  • 1
  • 1
Axel Guilmin
  • 11,454
  • 9
  • 54
  • 64
5

I had issues with not getting any callbacks.

Turns out it depends on how you create the stream. In my case I used a playerItem to initialize, and thus I had to add the observer to the item instead.

For example:

- (void) setup
{
    ...
    self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
    ... 

     // add callback
     [self.player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
}

// the callback method
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                    change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"[VideoView] player status: %i", self.player.status);

    if (object == self.player.currentItem && [keyPath isEqualToString:@"status"])
    {
        if (self.player.currentItem.status == AVPlayerStatusReadyToPlay)
        {
           //do stuff
        }
    }
}

// cleanup or it will crash
-(void)dealloc
{
    [self.player.currentItem removeObserver:self forKeyPath:@"status"];
}
dac2009
  • 3,521
  • 1
  • 22
  • 22
5

Swift 4:

var player:AVPlayer!

override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, 
               selector: #selector(playerItemDidReadyToPlay(notification:)),
               name: .AVPlayerItemNewAccessLogEntry, 
               object: player?.currentItem)
}

@objc func playerItemDidReadyToPlay(notification: Notification) {
        if let _ = notification.object as? AVPlayerItem {
            // player is ready to play now!!
        }
}
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
3

Check the status of the player's currentItem:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
Kirby Todd
  • 11,254
  • 3
  • 32
  • 60
  • 3
    player.currentItem.status returns AVPlayerItemStatusUnkown. I dont know what to do next. :( – mvishnu Mar 23 '11 at 08:01
  • Initially this value is `AVPlayerItemStatusUnkown`. Only after some time, it will be able to know if it is `AVPlayerItemStatusReadyToPlay` or `AVPlayerItemStatusFailed` – Gustavo Barbosa Jan 23 '15 at 21:08
2

@JoshBernfeld's answer didn't work for me. Not sure why. He observed playerItem.observe(\.status. I had to observe player?.observe(\.currentItem?.status. Seems like they're the same thing, the playerItem status property.

var playerStatusObserver: NSKeyValueObservation?

player?.automaticallyWaitsToMinimizeStalling = false // starts faster

playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) { (player, change) in
        
    switch (player.status) {
    case .readyToPlay:
            // here is where it's ready to play so play player
            DispatchQueue.main.async { [weak self] in
                self?.player?.play()
            }
    case .failed, .unknown:
            print("Media Failed to Play")
    @unknown default:
         break
    }
}

when you are finished using the player set playerStatusObserver = nil

Lance Samaria
  • 17,576
  • 18
  • 108
  • 256