AVPlayer对象何时准备好播放

90

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

我使用以下代码初始化了一个 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条件,即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 不仅仅是被创建,而且还准备好播放?)

10个回答

131

你正在播放一个远程文件。AVPlayer可能需要一些时间来缓冲足够的数据并准备好播放文件(请参阅AV Foundation编程指南)。

但是,在点击播放按钮之前,似乎并没有等待播放器准备就绪。我建议在播放器准备就绪后禁用此按钮,并仅在该时启用它。

使用KVO,可以接收到播放器状态的更改通知:

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

当状态改变时,将调用此方法:

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

12
根据我的经验,当player.status不准确时,player.currentItem.status是准确的。不确定两者之间的区别是什么。 - bendytree
我正在做同样的事情,在IOS6上运行良好,但在IOS7上observeValueForKeyPath没有被调用...有什么想法吗? - Sosily
@Sosily 你解决了吗?我也遇到了同样的问题。在iOS 6中它完美地工作,但在iOS 7中observeValueForKeyPath没有被调用。 - iOSAppDev
1
@iOSAppDev 在IOS7上使用AVPlayerItem addObserver。 - Peter Zhao
7
哇,这个AVPlayer设计得太糟糕了,让我感到很难过。为什么不加一个"onLoad"处理程序呢?拜托了,苹果,简化你的东西吧! - Duck
显示剩余4条评论

47

Swift解法

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

你也可以通过这种方式使观察者无效

self.observer.invalidate()

重要提示:您必须保留观察者变量,否则它将被释放并且changeHandler将不再被调用。因此,不要将观察者定义为函数变量,而应该像给定的示例一样将其定义为实例变量。

这个键值观察语法是Swift 4中新增的。

更多信息,请参见这里


谢谢,这个方法非常简单,可以用来移除KVO。 - ZAFAR007

32

我在尝试获取AVPlayer的状态时遇到了很多麻烦。 status属性似乎并不总是特别有用,这导致当我尝试处理音频会话中断时无尽的挫败感。有时候,AVPlayer告诉我它已经准备好播放(使用AVPlayerStatusReadyToPlay),但实际上似乎并没有准备好。我使用了Jilouc的KVO方法,但并不是所有情况下都有效。

为了补充说明,当状态属性不起作用时,我通过查看AVPlayercurrentItem(一个AVPlayerItem)的loadedTimeRanges属性来查询AVPlayer已加载的流的数量。

这有点令人困惑,但这是它的样子:

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
}

2
AV Foundation 中的 NSValue 类型有一些新增内容。其中一些辅助功能允许您在 NSValue 和 CMTimeXxx 值之间进行相互转换,例如 CMTimeRangeValue - superjos
获取CMTime中的秒数(我猜这就是timeLoaded的含义)也有类似的方法:CMTimeGetSeconds - superjos
2
不幸的是,这应该是一个被接受的答案。当AVPlayer没有真正准备好播放时,它似乎会过早地设置status == AVPlayerStatusReadyToPlay。为了使其工作,您可以将上述代码包装在NSTimer调用中。 - maxkonovalov
是否可能出现这种情况:已加载的时间范围超过(w.l.o.g)2秒,但播放器或播放器项的状态不是ReadyToPlay?换句话说,它也应该被确认吗? - danielhadar

18
private var playbackLikelyToKeepUpContext = 0

注册观察者

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

倾听观察者

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

移除观察者

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

代码中的关键点是实例属性isPlaybackLikelyToKeepUp。


5
好的回答。我会使用forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)来改进KVO。 - Miroslav Hrivik
对于2019年,这个确实可以完美地工作 - 复制并粘贴 :) 我使用了@MiroslavHrivik的mod,谢谢! - Fattie
@MiroslavHrivik在实现上做了修正后,它在2021年可以工作,谢谢。 - StackGU
如果我在VideoCell项中有一个播放器,那么我应该将这个答案的代码放在VideoCell类中还是应该为观察者创建一个新类?我是新手,对Swift中的KVO感到有些困惑。 - Benjamín Cáceres Ramirez
@BenjamínCáceresRamirez 把它放到 collectionViewCell 里,但是记得在 prepareForReuse() 方法里移除观察者。 - StackGU
我在上下文中使用了静态变量,否则编译器会报错。就像下面这样: private static var playbackLikelyToKeepUpContext = 0 - undefined

11

经过大量研究和尝试,我发现通常使用 status 观察者并不能很好地知道 AVPlayer 对象何时准备好播放,因为该对象可能已准备好播放,但这并不意味着它会立即播放。

更好的方法是使用 loadedTimeRanges

注册观察者

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

在移除观察者(dealloc、viewWillDissapear 或在注册观察者前)时,这是一个很好的调用位置。
- (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
    }
}

谢谢,这个方法对我也起作用了。不过我没有使用“currentBufferDuration == seconds”的判断条件。请问这个条件是用来做什么的? - andrei
对于 currentBufferDuration < 2 的情况 - jose920405
1
是否存在这样的情况:已经有超过(w.l.o.g)2秒的加载时间范围,但播放器或播放器项目的状态还没有准备好?换句话说,也应该进行确认吗? - danielhadar

7

基于 Tim Camber的回答,下面是我使用的Swift函数:

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
}

或者,作为一个扩展。
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
    }
}

有了这个扩展,我猜就不可能使用KVO观察ready属性了。有什么解决办法吗? - Jonny
我在我的项目中监听通知AVPlayerItemNewAccessLogEntryAVPlayerItemDidPlayToEndTime。据我所知,它可以正常工作。 - Axel Guilmin
好的,我最终监听了 loadedTimeRanges - Jonny

5

我遇到了没有收到任何回调的问题。

事实证明这取决于你如何创建流。在我的情况下,我使用了playerItem进行初始化,因此我必须将观察者添加到该项中。

例如:

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

if应该不应该与AVPlayerItemStatusReadyToPlay一起使用吗? - jose920405
@jose920405 我可以确认上面的解决方案是有效的,但这是一个好问题。我真的不知道。如果你测试了,请告诉我。 - dac2009

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

我试过了,不起作用。 - undefined

4
JoshBernfeld的答案对我没有用。不确定原因。他观察到playerItem.observe(\.status)。我必须观察player?.observe(\.currentItem?.status)。看起来它们是一样的,playerItem status属性。
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
    }
}

当您完成使用播放器后,请设置playerStatusObserver = nil


太棒了,它完美地起作用了。谢谢。 - undefined

3

检查播放器当前项目的状态:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)

3
player.currentItem.status 返回 AVPlayerItemStatusUnkown。 我不知道该怎么办。:( - mvishnu
最初这个值是AVPlayerItemStatusUnknown。只有经过一段时间,它才能知道它是AVPlayerItemStatusReadyToPlay还是AVPlayerItemStatusFailed - Gustavo Barbosa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接