Knowing when AVPlayer object is ready to play
Solution 1
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
}
}
}
Solution 2
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
Solution 3
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
}
Solution 4
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.
Solution 5
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
}
}
mvishnu
Updated on October 28, 2020Comments
-
mvishnu over 3 years
I'm trying to play an
MP3
file that is passed to anUIView
from a previousUIView
(stored in aNSURL *fileURL
variable).I'm initializing an
AVPlayer
with:player = [AVPlayer playerWithURL:fileURL]; NSLog(@"Player created:%d",player.status);
The
NSLog
printsPlayer 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 theif
condition that checks ifAVPlayer
is ready to play, with a simpleif(!isPlaying)
..., then the music plays the SECOND TIME I click on the playUIButton
.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 isAVPlayerReadyToPlay
.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 theAVPlayer
is not just created, but also ready to play?) -
mvishnu about 13 yearsplayer.currentItem.status returns AVPlayerItemStatusUnkown. I dont know what to do next. :(
-
mvishnu about 13 yearsThank you!! That worked like a charm. (should have guessed though when i saw it was playing offline files without a problem)
-
Fabrizio about 12 yearsThere 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.
-
Jilouc about 12 years@Fabrizio
player.status
should change toAVPlayerStatusFailed
then (see the edited answer). -
Fabrizio about 12 yearsAVPlayerStatusFailed or AVPlayerStatusUnknown is never called. Just to be clear the URL I try to play is: 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.
-
superjos almost 12 yearsThere 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.
-
superjos almost 12 yearsSimilar story for getting seconds (I guess that's what
timeLoaded
is) out of CMTime: CMTimeGetSeconds -
bendytree almost 12 yearsIn my experience
player.currentItem.status
is accurate whenplayer.status
is not. Not sure what the differences is. -
Sosily over 10 yearsI am doing the same thing and it is working fine on IOS6 but on IOS7 the observeValueForKeyPath is not getting called ... any idea ?
-
iOSAppDev over 10 years@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.
-
Gustavo Barbosa over 9 yearsInitially this value is
AVPlayerItemStatusUnkown
. Only after some time, it will be able to know if it isAVPlayerItemStatusReadyToPlay
orAVPlayerItemStatusFailed
-
jose920405 over 8 yearsThe if should not be with
AVPlayerItemStatusReadyToPlay
? -
dac2009 over 8 years@jose920405 I can confirm the solution above works, but its a good question. I really don´t know. Let me know if you test it.
-
maxkonovalov over 8 yearsUnfortunately, this should be an accepted answer.
AVPlayer
seems to setstatus == AVPlayerStatusReadyToPlay
too early when it is not ready to play really. To make this work, you can wrap the above code inNSTimer
invocation, for example. -
Peter Zhao almost 8 years@iOSAppDev On IOS7 use AVPlayerItem addObserver
-
Duck over 7 yearswow, this AVPlayer is so poorly designed that makes me cry. Why not adding a onLoad handler block? Come on Apple, simplify your stuff!
-
Jonny about 7 yearsWith the extension, I guess it's not possible to KVO observe the ready property. Any way around?
-
Axel Guilmin about 7 yearsI listen to the notifications
AVPlayerItemNewAccessLogEntry
andAVPlayerItemDidPlayToEndTime
in my project. Afaik it works. -
Jonny about 7 yearsOK, I ended up listening to
loadedTimeRanges
. -
andrei over 6 yearsthanks, 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?
-
jose920405 over 6 yearsFor cases when
currentBufferDuration < 2
-
Miroslav Hrivik almost 6 yearsGood answer. I would improve KVO with
forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
-
danielhadar almost 6 yearsCould 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 almost 6 yearsCould 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?
-
ZAFAR007 over 5 yearsThanks, This method is very simple for removing KVO.
-
Fattie over 4 yearsFor 2019, this does work perfectly - copy and paste :) I did use the mod of @MiroslavHrivik , thanks!
-
StackGU over 3 years@MiroslavHrivik with his correction in the implementation it works in 2021, thank you
-
Benjamín Cáceres Ramirez almost 3 years@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
-
StackGU almost 3 years@BenjamínCáceresRamirez put it inside the
collectionViewCell
but remember to remove the observer inside theprepareForReuse()
method