【问题标题】:Knowing when AVPlayer object is ready to play知道 AVPlayer 对象何时准备好播放
【发布时间】:2011-07-21 01:28:57
【问题描述】:

我正在尝试播放 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??。 但是,如果我将检查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 不仅已创建,而且可以播放?)

【问题讨论】:

    标签: iphone avfoundation audio-streaming avplayer


    【解决方案1】:

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

    但您似乎没有等到播放器准备好后才点击播放按钮。我要做的是禁用此按钮并仅在播放器准备好时启用它。

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

    【讨论】:

    • 谢谢!!这就像一个魅力。 (当我看到它播放离线文件时应该猜到了)
    • 有些 URL 不能播放,它们存在但不能正常工作(例如 iTunes 也不会播放)。你如何管理这种行为? AVPlayer 没有超时。
    • 根据我的经验,player.currentItem.status 准确,而player.status 则不准确。不知道有什么区别。
    • @iOSAppDev 在 IOS7 上使用 AVPlayerItem addObserver
    • 哇,这个AVPlayer设计的太差了,让我哭了。为什么不添加 onLoad 处理程序块?加油 Apple,简化您的工作!
    【解决方案2】:

    迅捷解决方案

    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/Contents.swift

    【讨论】:

    • 谢谢,这个方法去除KVO很简单。
    【解决方案3】:

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

    作为补充,当 status 属性没有用时,我通过查看 AVPlayercurrentItemloadedTimeRanges 属性来查询 AVPlayer 加载的流的数量。 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
    }
    

    【讨论】:

    • AV Foundation 中添加了 NSValue 类型。其中一些助手允许您在 NSValue 到 CMTimeXxx 值之间来回转换。喜欢CMTimeRangeValue
    • 从 CMTime 中获得秒数的类似故事(我猜这就是 timeLoaded 是):CMTimeGetSeconds
    • 不幸的是,这应该是一个公认的答案。 AVPlayer 似乎设置status == AVPlayerStatusReadyToPlay 太早了,因为它还没有真正准备好玩。为了使这项工作,您可以将上述代码包装在NSTimer 调用中,例如。
    • 会不会有超过 (w.l.o.g) 2 秒的加载时间范围,但播放器或 playerItem 的状态不是 ReadyToPlay? IOW,也应该确认吗?
    【解决方案4】:
    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。

    【讨论】:

    • 好答案。我会用forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)改进KVO@
    • 对于 2019 年,这确实工作得很好 - 复制和粘贴 :) 我确实使用了 @MiroslavHrivik 的 mod,谢谢!
    • @MiroslavHrivik 对 2021 年实施的更正,谢谢
    • @StackGU 如果我在 VideoCell 项目中有一个播放器,我应该把这个答案的代码放在这个 VideoCell 类中,还是应该为观察者创建一个新类?我是一个新手,不知何故对 Swift 中的 KVO 感到困惑
    • @BenjamínCáceresRamirez 将其放在 collectionViewCell 中,但请记住删除 prepareForReuse() 方法中的观察者
    【解决方案5】:

    经过大量研究并尝试多种方法后,我注意到通常 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”评估。你能告诉我它是做什么用的吗?
    • 适用于currentBufferDuration &lt; 2
    • 会不会有超过 (w.l.o.g) 2 秒的加载时间范围,但播放器或 playerItem 的状态不是 ReadyToPlay? IOW,也应该确认吗?
    【解决方案6】:

    基于Tim Camber answer,这是我使用的 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属性。有办法吗?
    • 我在我的项目中收听通知AVPlayerItemNewAccessLogEntryAVPlayerItemDidPlayToEndTime。 Afaik 它有效。
    • 好的,我最后听了loadedTimeRanges
    【解决方案7】:

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

    事实证明,这取决于您如何创建流。在我的例子中,我使用 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 我可以确认上述解决方案有效,但这是一个好问题。我真的不知道。如果你测试它,请告诉我。
    【解决方案8】:

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

    【讨论】:

      【解决方案9】:

      查看玩家当前物品的状态:

      if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
      

      【讨论】:

      • player.currentItem.status 返回 AVPlayerItemStatusUnkown。我不知道下一步该做什么。 :(
      • 最初这个值为AVPlayerItemStatusUnkown。只有过一段时间,它才能知道是AVPlayerItemStatusReadyToPlay还是AVPlayerItemStatusFailed
      【解决方案10】:

      @JoshBernfeld's answer 对我不起作用。不知道为什么。他观察到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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-21
        • 2013-04-18
        • 2012-10-19
        相关资源
        最近更新 更多