知道 AVPlayer 对象何时可以播放

我正在尝试播放一个 MP3文件,它从以前的 UIView(存储在 NSURL *fileURL变量中)传递给 UIView

我正在用以下命令初始化 AVPlayer:

player = [AVPlayer playerWithURL:fileURL];


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

NSLog打印 Player created:0,,我认为这意味着它还没有准备好发挥。

当我点击播放 UIButton,我运行的代码是:

-(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??");
}


}

当我运行这个命令时,我总是在控制台中得到 Error in player??。 然而,如果我替换的 if条件,检查如果 AVPlayer是准备发挥,与一个简单的 if(!isPlaying)... ,然后音乐播放第二次我点击发挥 UIButton

控制台日志是:

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**

我看到第二次,player.status似乎持有1,我猜是 AVPlayerReadyToPlay

我可以做什么,有发挥正常工作时,我第一次点击发挥 UIButton? (即,我如何才能确保 AVPlayer不仅是创建的,而且还准备好发挥?)

91849 次浏览

Check the status of the player's currentItem:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)

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
}
}
}

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
}

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"];
}

After researching a lot and try many ways I've noticed that normally the ABC0 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
}
}

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
}
}
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.

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

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!!
}
}

@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