如何检查我的AVPlayer是否正在缓冲?

42

我希望能检测到我的 AVPlayer 是否在当前位置进行缓冲,以便我可以显示加载器或者其他提示。但是我在 AVPlayer 的文档中找不到相关信息。

15个回答

53

您可以观察您的player.currentItem的值:

playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .New, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .New, context: nil)
playerItem.addObserver(self, forKeyPath: "playbackBufferFull", options: .New, context: nil)

然后

override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    if object is AVPlayerItem {
        switch keyPath {
            case "playbackBufferEmpty":
               // Show loader

            case "playbackLikelyToKeepUp":
                // Hide loader

            case "playbackBufferFull":
                // Hide loader
        }
    }
}

11
当AVPlayer寻找到特定位置时,它无法工作。 - Frank Cheng
@Marco_Santarossa 我使用了上面的代码,它可以工作。但是在视图控制器弹出后,代码崩溃了,你能帮我解决一下吗? - Arun K
2
在我的情况下,“playbackBufferFull”从未被调用。你能否给我建议原因?@Marco - Harish Singh
1
@FrankCheng 尝试将观测选项设置为0,而不是.new。 - Patrick Cary
代码一直崩溃。 - Kudos
显示剩余2条评论

19

对我来说,上面接受的答案没有起作用,但这种方法可以。您可以使用timeControlStatus,但仅适用于iOS 10及以上版本。

根据苹果官方文档

  

指示当前是否正在播放、无限暂停或在等待合适的网络条件时暂停的状态。

将此观察器添加到播放器中。

player.addObserver(self, forKeyPath: “timeControlStatus”, options: [.old, .new], context: nil)

然后,观察变化

func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)

方法。在上述方法中使用以下代码。

override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "timeControlStatus", let change = change, let newValue = change[NSKeyValueChangeKey.newKey] as? Int, let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
        let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
        let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)
        if newStatus != oldStatus {
            DispatchQueue.main.async {[weak self] in
                if newStatus == .playing || newStatus == .paused {
                    self?.loaderView.isHidden = true
                } else {
                    self?.loaderView.isHidden = false
                }
            }
        }
    }
}

这是在iOS 11以上使用Swift 4测试,并且它正常工作。


3
这是在iOS 13和Swift 5上唯一有效的东西。 - Radu Ursache

19

对我来说,接受的答案没用,我使用下面的代码有效地展示了加载程序。

Swift 3

//properties 
var observer:Any!
var player:AVPlayer!


self.observer = self.player.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 600), queue: DispatchQueue.main) {
    [weak self] time in

    if self?.player.currentItem?.status == AVPlayerItemStatus.readyToPlay {

        if let isPlaybackLikelyToKeepUp = self?.player.currentItem?.isPlaybackLikelyToKeepUp {
            //do what ever you want with isPlaybackLikelyToKeepUp value, for example, show or hide a activity indicator.
        }
    }
}

亲爱的@aytek,您能否请好心将您的解决方案翻译成Swift 4? :) - ixany
亲爱的@ixany,我还没有安装最新版本的Xcode。我会尽快添加Swift 4版本。感谢您的评论。 - aytek
我观察到的是,你必须在AVPlayerItem实例上注册这些观察者,而不是AVPlayer的实例,否则它不起作用。事实上,被接受的答案就是这样做的。 - fasteque

8

#已更新至Swift 4并且正常运行

虽然我按照所接受的答案进行尝试,但在Swift 4中无法正常工作,因此在一定的研究后,我发现了苹果文档中的这些内容。有两种方式来确定AVPlayer状态,即:

  1. addPeriodicTimeObserverForInterval:queue:usingBlock: 和
  2. addBoundaryTimeObserverForTimes:queue:usingBlock:

使用方式如下:

var observer:Any?
var avplayer : AVPlayer?

func preriodicTimeObsever(){

        if let observer = self.observer{
            //removing time obse
            avplayer?.removeTimeObserver(observer)
            observer = nil
        }

        let intervel : CMTime = CMTimeMake(1, 10)
        observer = avplayer?.addPeriodicTimeObserver(forInterval: intervel, queue: DispatchQueue.main) { [weak self] time in

            guard let `self` = self else { return }

            let sliderValue : Float64 = CMTimeGetSeconds(time)
           //this is the slider value update if you are using UISlider.

            let playbackLikelyToKeepUp = self.avPlayer?.currentItem?.isPlaybackLikelyToKeepUp
            if playbackLikelyToKeepUp == false{

               //Here start the activity indicator inorder to show buffering
            }else{
                //stop the activity indicator 
            }
        }
    }

不要忘记杀死时间观察器以避免内存泄漏。这是一个杀死实例的方法,根据您的需要添加此方法,但我已经在viewWillDisappear方法中使用它。
       if let observer = self.observer{

            self.avPlayer?.removeTimeObserver(observer)
            observer = nil
        }

Amrit Tiwari,为什么您使用强连接? - Max
在addPeriodicTimeObserver中,我们必须使用弱引用,所以我正在包装self以使用self属性!!! - Amrit Tiwari

7

Swift 4 观察:

var playerItem: AVPlayerItem?
var playbackLikelyToKeepUpKeyPathObserver: NSKeyValueObservation?
var playbackBufferEmptyObserver: NSKeyValueObservation?
var playbackBufferFullObserver: NSKeyValueObservation?

private func observeBuffering() {
    let playbackBufferEmptyKeyPath = \AVPlayerItem.playbackBufferEmpty
    playbackBufferEmptyObserver = playerItem?.observe(playbackBufferEmptyKeyPath, options: [.new]) { [weak self] (_, _) in
        // show buffering
    }

    let playbackLikelyToKeepUpKeyPath = \AVPlayerItem.playbackLikelyToKeepUp
    playbackLikelyToKeepUpKeyPathObserver = playerItem?.observe(playbackLikelyToKeepUpKeyPath, options: [.new]) { [weak self] (_, _) in
        // hide buffering
    }

    let playbackBufferFullKeyPath = \AVPlayerItem.playbackBufferFull
    playbackBufferFullObserver = playerItem?.observe(playbackBufferFullKeyPath, options: [.new]) { [weak self] (_, _) in
        // hide buffering
    }
}

在观察完成后,需要删除观察者。

要删除这三个观察者,只需将playbackBufferEmptyObserverplaybackLikelyToKeepUpKeyPathObserverplaybackBufferFullObserver设置为nil

无需手动删除它们(这适用于observe<Value>(_ keyPath:, options:, changeHandler:)方法)。


6
在Swift 5.3中,
变量:
private var playerItemBufferEmptyObserver: NSKeyValueObservation?
private var playerItemBufferKeepUpObserver: NSKeyValueObservation?
private var playerItemBufferFullObserver: NSKeyValueObservation?

添加观察者

playerItemBufferEmptyObserver = player.currentItem?.observe(\AVPlayerItem.isPlaybackBufferEmpty, options: [.new]) { [weak self] (_, _) in
    guard let self = self else { return }
    self.showLoadingIndicator(over: self)
}
    
playerItemBufferKeepUpObserver = player.currentItem?.observe(\AVPlayerItem.isPlaybackLikelyToKeepUp, options: [.new]) { [weak self] (_, _) in
    guard let self = self else { return }
    self.dismissLoadingIndicator()
}
    
playerItemBufferFullObserver = player.currentItem?.observe(\AVPlayerItem.isPlaybackBufferFull, options: [.new]) { [weak self] (_, _) in
    guard let self = self else { return }
    self.dismissLoadingIndicator()
}

移除观察者

playerItemBufferEmptyObserver?.invalidate()
playerItemBufferEmptyObserver = nil
    
playerItemBufferKeepUpObserver?.invalidate()
playerItemBufferKeepUpObserver = nil
    
playerItemBufferFullObserver?.invalidate()
playerItemBufferFullObserver = nil

这些观察者从未被触发。你知道为什么吗? - Makaille
你在哪里声明属性? - Reimond Hill
在管理AvPlayer的类的init中,我调用一个函数来初始化所有观察者,然后调用调用逻辑来创建AVPlayerItem以流式传输到播放器中。 观察者(currentItem.status、actionItemAdded和rate)正在触发,但不是这些。 - Makaille
在 iOS 16 上,我唯一找到可行的解决方案是使用 SwiftUI 的 VideoPlayer 包装 AVPlayer。如果没有这个,我找不到显示指示器的方法。谢谢。 - jusko

4

更新至Swift 4.2

    var player : AVPlayer? = nil

    let videoUrl = URL(string: "https://wolverine.raywenderlich.com/content/ios/tutorials/video_streaming/foxVillage.mp4")
    self.player = AVPlayer(url: videoUrl!)
    self.player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 600), queue: DispatchQueue.main, using: { time in

        if self.player?.currentItem?.status == AVPlayerItem.Status.readyToPlay {

            if let isPlaybackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp {
                //do what ever you want with isPlaybackLikelyToKeepUp value, for example, show or hide a activity indicator.

                //MBProgressHUD.hide(for: self.view, animated: true)
            }
        }
    })

可以,谢谢。但是在 block 中使用 weak self 是推荐的做法。我已经发布了我的答案。 - fullmoon

2
我们可以使用状态观察器方法直接观察播放状态。如果有任何播放状态变化,它将被通知。这是一种非常简单的方法,已经在swift 5和iOS 13.0+上进行了测试。请注意保留HTML标签。
var player: AVPlayer!

player.currentItem!.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)

func observeValue(forKeyPath keyPath: String?,
                  of object: Any?,
                  change: [NSKeyValueChangeKey : Any]?,
                  contexts: UnsafeMutableRawPointer?) {

    if (player.currentItem?.isPlaybackLikelyToKeepUp ?? false) {
        // End Buffering
    } else {
        // Buffering is in progress
    }
}

Apple Doc Reference


2

嗯,被接受的解决方案对我没用,而周期性观察者方案似乎有些笨重。

这是我的建议,观察AVPlayer上的timeControlerStatus

// Add observer
player.addObserver(self,
                   forKeyPath: #keyPath(AVPlayer.timeControlStatus),
                   options: [.new],
                   context: &playerItemContext)

// At some point you'll need to remove yourself as an observer otherwise
// your app will crash 
self.player?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.timeControlStatus))

// handle keypath callback
if keyPath == #keyPath(AVPlayer.timeControlStatus) {
    guard let player = self.player else { return }
    if let isPlaybackLikelyToKeepUp = player.currentItem?.isPlaybackLikelyToKeepUp,
        player.timeControlStatus != .playing && !isPlaybackLikelyToKeepUp {
        self.playerControls?.loadingStatusChanged(true)
    } else {
        self.playerControls?.loadingStatusChanged(false)
    }
}

1
使用Combine,您可以轻松订阅发布者以了解AVPlayerItem何时缓冲或未缓冲,如下所示:
// Subscribe to this and update your `View` appropriately
@Published var isBuffering = false
private var observation: AnyCancellable?

observation = avPlayer?.currentItem?.publisher(for: \.isPlaybackBufferEmpty).sink(receiveValue: { [weak self] isBuffering in
  self?.isBuffering = isBuffering
})

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