AVPlayer show and hide loading indicator when buffering
Asked Answered
L

3

5

(NOTE: I am running iOS 9 and am doing HTTP Live Streaming)

I have an AVPlayer and want to show a loading indicator while the player is buffering. When the player starts playing with a slow connection KVO gets called for these properties: isPlaybackBufferEmpty

isPlaybackLikelyToKeepUp

isPlaybackBufferFull

The problem is that these same properties do not get called again when buffering is done (when I say done I mean that the video is ok to play again.) My goal is to hide the loading indicator at the correct time but these don't get called again.

I searched online and found this radar: http://www.openradar.me/25931165 Not sure if it's 100% related

Any thoughts?

// MARK: - Key-Value Observing Method

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == #keyPath(AVPlayerItem.status) {

        switch playerItem.status {
        case .unknown:
            break

        case .readyToPlay:
            player.play()

        case .failed:
            postPlaybackDidFailWithErrorNotification(error: error)
        }
    }
    else if keyPath == #keyPath(AVPlayer.currentItem.isPlaybackBufferEmpty) {
        guard let currentItem = player.currentItem else {
            return
        }

        if currentItem.isPlaybackBufferEmpty {
            print("isPlaybackBufferEmpty = YES")
        } else {
            print("isPlaybackBufferEmpty = NO")
        }
    }
    else if keyPath == #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp) {
        guard let currentItem = player.currentItem else {
            return
        }

        if currentItem.isPlaybackLikelyToKeepUp {
            print("isPlaybackLikelyToKeepUp = YES")
            //player.play()
        } else {
            print("isPlaybackLikelyToKeepUp = NO")
        }

    }
    else if keyPath == #keyPath(AVPlayer.currentItem.isPlaybackBufferFull) {
        guard let currentItem = player.currentItem else {
            return
        }

        if currentItem.isPlaybackBufferFull {
            print("isPlaybackBufferFull = YES")
            //player.play()
        } else {
            print("isPlaybackBufferFull = NO")
        }
    }
else if keyPath == #keyPath(AVPlayer.currentItem) {
    // Cleanup if needed.
    if player.currentItem == nil {
        video = nil
        playerItem = nil
    }        
}
else if keyPath == #keyPath(AVPlayer.rate) {
    updateMetadata()
    NotificationCenter.default.post(name: AssetPlaybackManager.NotificationName.playerRateDidChangeNotification, object: nil)
}
else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    }
}
Lakeesha answered 12/3, 2017 at 3:21 Comment(1)
Any progress on this? In the meantime, I found this #38867690Terms
S
12

You can use timeControlStatus but it is available only above iOS 10.

According to apple's official documentation

A status that indicates whether playback is currently in progress, paused indefinitely, or suspended while waiting for appropriate network conditions

Add this observer to the player.

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

Then,Observe the changes in

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

method.Use below code inside above method

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

This is tested on iOS 11 above with swift 4 and It is working.

Schismatic answered 13/5, 2019 at 15:20 Comment(0)
C
0

Use the below function for setting the progress as well as status of buffering for AVPlayer:

func addObserver() {
    let intervel : CMTime = CMTimeMake(value: 10, timescale: 10)
    observer = player?.addPeriodicTimeObserver(forInterval: intervel, queue: DispatchQueue.main) { [weak self] time in

        guard let `self` = self,
            let playerItem = self.player?.currentItem else { return }

        let currentTime : Float64 = CMTimeGetSeconds(time)
        let totalDuration = CMTimeGetSeconds(playerItem.asset.duration)

        //this is the slider value update if you are using UISlider.
        let sliderValue = (currentTime/totalDuration)

        if currentTime >= totalDuration {
            if let observer = self.observer{
                //removing time observer
                self.player?.removeTimeObserver(observer)
                self.observer = nil
            }
        }

        let playbackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp
        if playbackLikelyToKeepUp == false{
            print(self.player?.rate)
                print("IsBuffering")
                self.lock()
        } else {
            //stop the activity indicator
            print("Buffering completed")
            self.unlock()
        }
    }
}
Confinement answered 5/2, 2019 at 7:34 Comment(0)
L
0

Here is the complete implementation

import SwiftUI
import AVKit

struct MyVideoPlayer:UIViewControllerRepresentable{
    var player:AVPlayer
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let controller = AVPlayerViewController()
        controller.player = player
        return controller
    }
    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
        uiViewController.player = player
    }
}

struct PlayerView: View {
    
    @State private var player:AVPlayer = AVPlayer(url: URL(string: "https://embed-ssl.wistia.com/deliveries/cc8402e8c16cc8f36d3f63bd29eb82f99f4b5f88/accudvh5jy.mp4")!)
    @State private var isLoading:Bool = true
    
    var body: some View {
        ZStack{
            MyVideoPlayer(player:player).onAppear(){
                player.play()
                self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main, using: { [self] time in
                    if self.player.timeControlStatus == .playing {
                        isLoading = false
                        debugPrint("#player - info: isPlaying")
                        
                    } else if(player.currentItem?.isPlaybackLikelyToKeepUp == false ) {
                        isLoading = true
                        debugPrint("#player - info: isWaiting") //Buffering
                    }
                })
            }
            .onDisappear(){
                player.pause()
            }
            if(isLoading){
                ProgressView().tint(.white)
            }
        }
        .ignoresSafeArea(.all)
    }
}

#Preview {
    PlayerView()
}
Lupercalia answered 26/6 at 9:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.