知道何时可以播放AVPlayer对象


79

我正在尝试播放从上一个MP3传递到的文件(存储在变量中)。UIViewUIViewNSURL *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(!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不仅创建了,而且还可以播放?)

Answers:


127

您正在播放远程文件。可能需要一些时间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
        }
    }
}

谢谢!!就像魅力一样。(本来应该猜,但是当我看到它正在播放脱机文件而没有问题时)
mvishnu 2011年

有一些URL只是不播放,它们存在但不起作用(例如iTunes也不会播放它们)。您如何处理这种行为?AVPlayer中没有超时。
Fabrizio 2012年

10
以我的经验player.currentItem.status,准确的player.status是不正确的。不知道有什么区别。
bentytree

1
@iOSAppDev在IOS7上使用AVPlayerItem addObserver
Peter Zhao

4
哇,这个AVPlayer的设计太差了,让我哭了。为什么不添加onLoad处理程序块?快来Apple,简化您的工作!
鸭子

30

我很难弄清电脑的状态AVPlayer。该status属性似乎并不总是非常有用,当我尝试处理音频会话中断时,这导致了无尽的挫败感。有时,当AVPlayer我告诉我它AVPlayerStatusReadyToPlay实际上似乎并没有准备好与一起玩时。我使用了Jilouc的KVO方法,但并非在所有情况下都行得通。

来补充,当状态财产不是有用的,我问的是AVPlayer已经通过查看加载的数据流的数量loadedTimeRanges的财产AVPlayercurrentItem(这是AVPlayerItem)。

一切都让人有些困惑,但是看起来是这样的:

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 2012年

timeLoadedCMTime获得秒数(我想就是这样)的类似故事:CMTimeGetSeconds
superjos 2012年

2
不幸的是,这应该是一个可以接受的答案。尚未真正准备就绪时,AVPlayer似乎设定status == AVPlayerStatusReadyToPlay得太早了。为此,您可以在NSTimer调用中包装上面的代码。
maxkonovalov 2015年

可能出现超过2秒钟的载入时间范围,但玩家的状态不是ReadyToPlay的情况吗?IOW,也应该确认吗?
danielhadar '18

29

快速解决方案

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的新功能。

有关更多信息,请参见此处https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/内容目录


谢谢,此方法对于删除KVO非常简单。
ZAFAR007 '18

11

经过大量研究并尝试了许多方法之后,我注意到通常status观察者并不是真正知道何时AVPlayer可以播放对象的,因为对象可以随时播放,但这并不意味着它将立即被播放。

知道这一点的更好的主意是loadedTimeRanges

对于注册观察员

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

听观察者

- (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 '17

可能是超过2秒钟的载入时间范围,但播放器或playerItem的状态不是ReadyToPlay?IOW,也应该确认吗?
danielhadar

11
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。


3
好答案。我会用forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
Miroslav Hrivik '18

对于2019年,这确实可以正常工作-复制并粘贴:)我确实使用了@MiroslavHrivik的mod,谢谢!
Fattie

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属性。可以吗
乔尼

我听的通知AVPlayerItemNewAccessLogEntry,并AVPlayerItemDidPlayToEndTime在我的项目。Afaik它的工作原理。
Axel Guilmin

好的,我最后听了loadedTimeRanges
乔尼

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

如果不应该与AVPlayerItemStatusReadyToPlay
jose920405

@ jose920405我可以确认上述解决方案有效,但是这是一个很好的问题。我真的不知道 让我知道您是否进行测试。
dac2009年

3

检查播放器currentItem的状态:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)

2
player.currentItem.status返回AVPlayerItemStatusUnkown。我不知道下一步该怎么做。:(
mvishnu 2011年

最初,此值为AVPlayerItemStatusUnkown。只有经过一段时间后,它就能知道它是否AVPlayerItemStatusReadyToPlayAVPlayerItemStatusFailed
古斯塔沃·巴博萨

2

斯威夫特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!!
        }
}

1

@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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.