AVPlayer.status doesn't run when wrapped in a DispatchWorkItem with a Delay - ios

Because I'm playing videos in cells I have an AVPlayer that plays videos in certain circumstances immediately and others it runs a few seconds later. When it runs immediately the .status works fine. But when I wrap it in a DispatchWorkItem with a .asyncAfter delay that same exact .status is never called. I also tried to use a perform(_:, with:, afterDelay:) and a Timer but this didn't work either.
var player: AVPlayer?
var playerItem: AVPlayerItem?
var observer: NSKeyValueObservation? = nil
var workItem: DispatchWorkItem? = nil
var startImmediately = false
var timer: Timer?
viewDidLoad() {
// add asset to playerItem, add playerItem to player ...
}
func someCircumstance() {
if startImmediately {
setNSKeyValueObserver() // this works fine and .status is called
} else {
createWorkItem()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.33, execute: executeWorkItem) // this delay runs but .status is never called
// perform(#selector(executeWorkItem), with: nil, afterDelay: 0.33) // same issue
// timer = Timer.scheduledTimer(timeInterval: 0.33, target: self, selector: #selector(executeWorkItem), userInfo: nil, repeats: false) // same issue
// RunLoop.current.add(timer!, forMode: .common)
}
}
func createWorkItem() {
workItem = DispatchWorkItem {
DispatchQueue.main.async { [weak self] in
self?.setNSKeyValueObserver()
}
}
}
#objc func executeWorkItem() {
guard let workItem = workItem else { return }
workItem.perform()
}
func setNSKeyValueObserver() {
// without a long explanation this sometimes has to start with a delay because of scrolling reason I also might have to cancel it
observer = player?.observe(\.status, options: [.new, .old]) { [weak self] (player, change) in
switch (player.status) {
case .readyToPlay:
print("Media Ready to Play")
case .failed, .unknown:
print("Media Failed to Play")
#unknown default:
print("Unknown Error")
}
}
}
I also tried to use the older KVO API .status observer instead but the same issue occurred when using a delay
private var keepUpContext = 0
viewDidLoad() {
// add asset to playerItem, add playerItem to player ...
player?.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp", options: [.old, .new], context: &keepUpContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &keepUpContext {
if let player = player, let item = player.currentItem, item.isPlaybackLikelyToKeepUp {
print("Media Ready to Play")
}
}
}
The problem seems to be the delay.
Update
I just tried the following code and and this doesn't work either:
if startImmediately {
setNSKeyValueObserver()
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
self?.setNSKeyValueObserver()
}
}

It looks like you're adding your observer after the player has loaded the content. It likely loads between the viewDidLoad and the delay. If you add .initial to the list of options when adding the observer, you'll be sure to get the state notification even if the player is already ready.

Can you try to change the forKeyPath parameter value to #keyPath(AVPlayerItem.status)

Related

How to handle AVPlayer .AVPlayerItemPlaybackStalled Notification

I'm using AVPlayer and sometimes the player randomly pauses. I'm observing \.timeControlStatus but the only response I get .paused. I'm also observing \.isPlaybackLikelyToKeepUp, \.isPlaybackBufferEmpty, and \.isPlaybackBufferFull but nothing fires for those. However using Notification.Name.AVPlayerItemPlaybackStalled I do get a print statement that says "stalled".
Even though \.rate fires when the the player pauses, that fires before .AVPlayerItemPlaybackStalled fires, sometimes \.timeControlStatus .paused fires after .AVPlayerItemPlaybackStalled and the player just sits there.
What should I do once .AVPlayerItemPlaybackStalled runs and the player is sitting paused?
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled(_:)),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: playerItem)
#objc fileprivate func playerItemPlaybackStalled(_ notification: Notification) {
print("playerItemPlaybackStalled")
}
Here is my full code:
let player = AVPlayer()
player.automaticallyWaitsToMinimizeStalling = false
playerLayer = AVPlayerLayer(player: player)
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
// add KVO observers and NotificationCenter observers
// playerItem keys are already loaded
playerItem.preferredForwardBufferDuration = TimeInterval(1.0)
player.replaceCurrentItem(with: playerItem)
playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) { [weak self] (player, change) in
switch (player.status) {
case .readyToPlay:
DispatchQueue.main.async {
// play video
}
case .failed, .unknown:
print("Video Failed to Play")
#unknown default:
break
}
}
playerRateObserver = player?.observe(\.rate, options: [.new, .old], changeHandler: { [weak self](player, change) in
if player.rate == 1 {
DispatchQueue.main.async {
// if player isn't playing play it
}
} else {
DispatchQueue.main.async {
// is player is playing pause it
}
}
})
playerTimeControlStatusObserver = player?.observe(\.timeControlStatus, options: [.new, .old]) { [weak self](player, change) in
switch (player.timeControlStatus) {
case .playing:
DispatchQueue.main.async { [weak self] in
// if player isn't playing pay it
}
case .paused:
print("timeControlStatus is paused") // *** SOMETIMES PRINTS after .AVPlayerItemPlaybackStalled runs***
case .waitingToPlayAtSpecifiedRate:
print("timeControlStatus- .waitingToPlayAtSpecifiedRate")
if let reason = player.reasonForWaitingToPlay {
switch reason {
case .evaluatingBufferingRate:
print("timeControlStatus- .evaluatingBufferingRate") // never prints
case .toMinimizeStalls:
print("timeControlStatus- .toMinimizeStalls") // never prints
case .noItemToPlay:
print("timeControlStatus- .noItemToPlay") // never prints
default:
print("Unknown \(reason)")
}
}
#unknown default:
break
}
}
playbackLikelyToKeepUpObserver = player?.currentItem?.observe(\.isPlaybackLikelyToKeepUp, options: [.old, .new]) { (playerItem, change) in
print("isPlaybackLikelyToKeepUp") // never prints
}
playbackBufferEmptyObserver = player?.currentItem?.observe(\.isPlaybackBufferEmpty, options: [.old, .new]) { (playerItem, change) in
print("isPlaybackBufferEmpty") // never prints
}
playbackBufferFullObserver = player?.currentItem?.observe(\.isPlaybackBufferFull, options: [.old, .new]) { (playerItem, change) in
print("isPlaybackBufferFull") // never prints
}
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd(_:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemFailedToPlayToEndTime(_:)),
name: .AVPlayerItemFailedToPlayToEndTime,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemNewError(_:)),
name: .AVPlayerItemNewErrorLogEntry,
object: playerItem)
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled(_:)),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: playerItem)
#objc func playerItemDidReachEnd(_ notification: Notification) {
// show replay button
}
#objc func playerItemFailedToPlayToEndTime(_ notification: Notification) {
print("playerItemFailedToPlayToEndTime") // never prints
if let error = notification.userInfo?["AVPlayerItemFailedToPlayToEndTime"] as? Error {
print(error.localizedDescription) // never prints
}
}
#objc func playerItemNewError(_ notification: Notification) {
print("playerItemNewErrorLogEntry") // never prints
}
#objc func playerItemPlaybackStalled(_ notification: Notification) {
print("playerItemPlaybackStalled") // *** PRINTS ***
}
I found the answer here. Basically inside the stalled notification I check to see if the buffer is full or not. If not I run the code from the link.

How to get Scrubbing to work on UISlider using AVAudioPlayer

So I'm using a rather unique version of audio player and everything works except for track scrubbing. The tracks, it should be noted, are buffered and collected from a website. I thought it might have to do with the observers I'm using. But I don't know if it's that really. I'm able to call the duration of the track and I'm able to get the actual UISlider to animate in conjunction with the track progression. But I can't scrub forward or backward. What ends up happening is I can move the slider but the track's progress doesn't move along with it. Here's what I've got:
override func viewDidLoad() {
audio.initialize(config: ["loop": true]) { (playerItem, change) in
var duration = CMTimeGetSeconds(playerItem.duration)
self.scrubSlider.maximumValue = Float(duration)
}
scrubSlider.setThumbImage(UIImage(named: "position_knob"), for: .normal)
}
override func viewDidAppear(_ animated: Bool) {
scrubSlider.value = 0.0
print(audio.seekDuration())
self.scrubSlider.maximumValue = audio.seekDuration()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.trackAudio), userInfo: nil, repeats: true)
print(self.scrubSlider.maximumValue)
}
#objc func trackAudio() {
var currentTime = audio.seekCurrentTime()
print(currentTime)
scrubSlider.value = currentTime
var duration = audio.seekDuration()
let remainingTimeInSeconds = duration - currentTime
timeElapsedLabel.text = audio.createTimeString(time: currentTime)
timeRemainingLabel.text = audio.createTimeString(time: remainingTimeInSeconds)
}
#IBAction func scrubberValueChanged(_ sender: UISlider) {
print("Entro")
var currentTime = (CMTimeGetSeconds((audio.playerQueue?.currentItem!.currentTime())!))
currentTime = (Float64)((scrubSlider.value))
}
Here's the observer where I collect the duration:
/**
Setup observers to monitor playback flow
*/
private func setupObservers(completion: #escaping (AVPlayerItem, Any) -> ()) {
// listening for current item change
self.audioQueueObserver = self.playerQueue?.observe(\.currentItem, options: [.new]) { [weak self] (player, _) in
print("media item changed...")
print("media number ", self?.playerQueue?.items() as Any, self?.playerQueue?.items().count as Any, self?.playerQueue?.currentItem as Any)
// loop here if needed //
if self?.audioPlayerConfig["loop"] as! Bool == true && self?.playerQueue?.items().count == 0 && self?.playerQueue?.currentItem == nil {
self?.playerQueue?.removeAllItems()
self?.playerQueue?.replaceCurrentItem(with: nil)
for item:AVPlayerItem in (self?.AVItemPool)! {
item.seek(to: CMTime.zero)
self?.playerQueue?.insert(item, after: nil)
}
self?.playerQueue?.play()
}
}
// listening for current item status change
self.audioQueueStatusObserver = self.playerQueue?.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
guard let currentItemDuration = self.playerQueue?.currentItem?.duration else { return }
if playerItem.status == .readyToPlay {
print("current item status is ready")
print("media Queue ", self.playerQueue?.items() as Any, self.playerQueue?.items().count as Any)
print("item duration ", CMTimeGetSeconds(playerItem.duration))
print("itemD ", CMTimeGetSeconds(currentItemDuration))
print(currentItemDuration)
completion(playerItem, change)
}
})

DispatchQueue.main.asyncAfter with on/off switch

I whipped up the below struct as a way to alert the user when there's a slow network connection.
When a function is going to make a call to the server, it creates a ResponseTimer. This sets a delayed notification, which only fires if the responseTimer var isOn = true. When my function get's a response back from the server, set responseTimer.isOn = false.
Here's the struct:
struct ResponseTimer {
var isOn: Bool
init() {
self.isOn = true
self.setDelayedAlert()
}
func setDelayedAlert() {
let timer = DispatchTime.now() + 8
DispatchQueue.main.asyncAfter(deadline: timer) {
if self.isOn {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: toastErrorNotificationKey), object: self, userInfo: ["toastErrorCase" : ToastErrorCase.poorConnection])
}
}
}
And here's how I'd use it
func getSomethingFromFirebase() {
var responseTimer = ResponseTimer()
ref.observeSingleEvent(of: .value, with: { snapshot in
responseTimer.isOn = false
//do other stuff
})
}
Even in cases where the response comes back before the 8 second delay completes, the notification is still fired. What am I doing wrong here??? Is there a better pattern to use for something like this?
Thanks for the help!
A better way is to use DispatchSourceTimer which can be cancelled
var timer : DispatchSourceTimer?
func startTimer()
{
if timer == nil {
timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
timer!.schedule(deadline: .now() + .seconds(8))
timer!.setEventHandler {
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: toastErrorNotificationKey), object: self, userInfo: ["toastErrorCase" : ToastErrorCase.poorConnection])
}
self.timer = nil
}
timer!.resume()
}
}
func getSomethingFromFirebase() {
startTimer()
ref.observeSingleEvent(of: .value, with: { snapshot in
self.timer?.cancel()
self.timer = nil
//do other stuff
})
}
There are a few approaches:
invalidate a Timer in deinit
Its implementation might look like:
class ResponseTimer {
private weak var timer: Timer?
func schedule() {
timer = Timer.scheduledTimer(withTimeInterval: 8, repeats: false) { _ in // if you reference `self` in this block, make sure to include `[weak self]` capture list, too
// do something
}
}
func invalidate() {
timer?.invalidate()
}
// you might want to make sure you `invalidate` this when it’s deallocated just in
// case you accidentally had path of execution that failed to call `invalidate`.
deinit {
invalidate()
}
}
And then you can do:
var responseTimer: ResponseTimer?
func getSomethingFromFirebase() {
responseTimer = ResponseTimer()
responseTimer.schedule()
ref.observeSingleEvent(of: .value) { snapshot in
responseTimer?.invalidate()
//do other stuff
}
}
Use asyncAfter with DispatchWorkItem, which you can cancel:
class ResponseTimer {
private var item: DispatchWorkItem?
func schedule() {
item = DispatchWorkItem { // [weak self] in // if you reference `self` in this block, uncomment this `[weak self]` capture list, too
// do something
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: item!)
}
func invalidate() {
item?.cancel()
item = nil
}
deinit {
invalidate()
}
}
Use DispatchTimerSource, which cancels automatically when it falls out of scope:
struct ResponseTimer {
private var timer: DispatchSourceTimer?
mutating func schedule() {
timer = DispatchSource.makeTimerSource(queue: .main)
timer?.setEventHandler { // [weak self] in // if you reference `self` in the closure, uncomment this
NotificationCenter.default.post(name: notification, object: nil)
}
timer?.schedule(deadline: .now() + 8)
timer?.activate()
}
mutating func invalidate() {
timer = nil
}
}
In all three patterns, the timer will be canceled when ResponseTimer falls out of scope.

How can I check if my AVPlayer is buffering? for AVPlayer [duplicate]

I want to detect if my AVPlayer is buffering for the current location, so that I can show a loader or something. But I can't seem to find anything in the documentation for AVPlayer.
You can observe the values of your 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)
then
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
}
}
}
For me above accepted answer didn't worked but this method does.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.
The accepted answer didn't work for me, I used the code below to show the loader efficiently.
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.
}
}
}
Swift 4 observations:
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
}
}
Observers need to be removed after we are done observing.
To remove these three observers just set playbackBufferEmptyObserver, playbackLikelyToKeepUpKeyPathObserver and playbackBufferFullObserver to nil.
No need to remove them manually (this is specific for observe<Value>(_ keyPath:, options:, changeHandler:) method.
#Updated in Swift 4 and worked fine
As through i have gone with accepted answer but didn't work in swift 4 for me so after certain research i have found this thinks from apple doc. There are two way to determine AVPlayer states that are,
addPeriodicTimeObserverForInterval:queue:usingBlock: and
addBoundaryTimeObserverForTimes:queue:usingBlock:
and using ways is like this
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
}
}
}
And Don't forget to kill time observer to save from memory leak. method for killing instance, add this method according to your need but i have used it in viewWillDisappear method.
if let observer = self.observer{
self.avPlayer?.removeTimeObserver(observer)
observer = nil
}
Updated for 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)
}
}
})
Hmm, the accepted solution didn't work for me and the periodic observer solutions seem heavy handed.
Here's my suggestion, observe timeControlerStatus on AVPlayer.
// 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)
}
}
In Swift 5.3
Vars:
private var playerItemBufferEmptyObserver: NSKeyValueObservation?
private var playerItemBufferKeepUpObserver: NSKeyValueObservation?
private var playerItemBufferFullObserver: NSKeyValueObservation?
AddObservers
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()
}
RemoveObservers
playerItemBufferEmptyObserver?.invalidate()
playerItemBufferEmptyObserver = nil
playerItemBufferKeepUpObserver?.invalidate()
playerItemBufferKeepUpObserver = nil
playerItemBufferFullObserver?.invalidate()
playerItemBufferFullObserver = nil
We can directly Observe Playback State using the state observer method once is there any playback state changes it will be notified, it's a really easy way and it's tested with swift 5 and iOS 13.0+
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
Solution for Xamarin inspired by Marco's answer
// KVO registrations
private void Initialize()
{
playbackBufferEmptyObserver?.Dispose();
playbackBufferEmptyObserver = (NSObject)playerItem.AddObserver("playbackBufferEmpty",
NSKeyValueObservingOptions.New,
AVPlayerItem_BufferUpdated);
playbackLikelyToKeepUpObserver?.Dispose();
playbackLikelyToKeepUpObserver = (NSObject)playerItem.AddObserver("playbackLikelyToKeepUp",
NSKeyValueObservingOptions.New,
AVPlayerItem_BufferUpdated);
playbackBufferFullObserver?.Dispose();
playbackBufferFullObserver = (NSObject)playerItem.AddObserver("playbackBufferFull",
NSKeyValueObservingOptions.New,
AVPlayerItem_BufferUpdated);
}
private void AVPlayerItem_BufferUpdated(NSObservedChange e)
{
ReportVideoBuffering();
}
private void ReportVideoBuffering()
{
// currentPlayerItem is the current AVPlayerItem of AVPlayer
var isBuffering = !currentPlayerItem.PlaybackLikelyToKeepUp;
// NOTE don't make "buffering" as one of your PlayerState.
// Treat it as a separate property instead. Learned this the hard way.
Buffering?.Invoke(this, new BufferingEventArgs(isBuffering));
}
Please note that
Use a weak reference to self in the callback block to prevent creating a retain cycle.
func playRemote(url: URL) {
showSpinner()
let playerItem = AVPlayerItem(url: url)
avPlayer = AVPlayer(playerItem: playerItem)
avPlayer.rate = 1.0
avPlayer.play()
self.avPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1,
timescale: 600), queue: DispatchQueue.main, using: { [weak self] time in
if self?.avPlayer.currentItem?.status == AVPlayerItem.Status.readyToPlay {
if let isPlaybackLikelyToKeepUp = self?.avPlayer.currentItem?.isPlaybackLikelyToKeepUp {
self?.removeSpinner()
}
}
})
}
}
Using Combine you can easily subscribe to the publisher for when an AVPlayerItem is buffering or not like so:
// 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
})
Here is a simple method, that works with Swift 5.
This will add the loadingIndicator when your player is stalled
NotificationCenter.default.addObserver(self, selector:
#selector(playerStalled(_:)), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: self.player?.currentItem)
#objc func playerStalled(_ notification: Notification){
self.loadingIndicator.isHidden = false
self.playPauseButton.isHidden = true
}
This will show loader Indicator when buffer is empty:
if let isPlayBackBufferEmpty = self.player?.currentItem?.isPlaybackBufferEmpty{
if isPlayBackBufferEmpty{
self.loadingIndicator.isHidden = false
self.playPauseButton.isHidden = true
}
}
This will hide the loader when player is ready to play:
if self.playerItem?.status == AVPlayerItem.Status.readyToPlay{
if let isPlaybackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp {
if isPlaybackLikelyToKeepUp{
self.loadingIndicator.isHidden = true
self.playPauseButton.isHidden = false
}
}
}

How can I check if my AVPlayer is buffering?

I want to detect if my AVPlayer is buffering for the current location, so that I can show a loader or something. But I can't seem to find anything in the documentation for AVPlayer.
You can observe the values of your 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)
then
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
}
}
}
For me above accepted answer didn't worked but this method does.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.
The accepted answer didn't work for me, I used the code below to show the loader efficiently.
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.
}
}
}
Swift 4 observations:
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
}
}
Observers need to be removed after we are done observing.
To remove these three observers just set playbackBufferEmptyObserver, playbackLikelyToKeepUpKeyPathObserver and playbackBufferFullObserver to nil.
No need to remove them manually (this is specific for observe<Value>(_ keyPath:, options:, changeHandler:) method.
#Updated in Swift 4 and worked fine
As through i have gone with accepted answer but didn't work in swift 4 for me so after certain research i have found this thinks from apple doc. There are two way to determine AVPlayer states that are,
addPeriodicTimeObserverForInterval:queue:usingBlock: and
addBoundaryTimeObserverForTimes:queue:usingBlock:
and using ways is like this
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
}
}
}
And Don't forget to kill time observer to save from memory leak. method for killing instance, add this method according to your need but i have used it in viewWillDisappear method.
if let observer = self.observer{
self.avPlayer?.removeTimeObserver(observer)
observer = nil
}
Updated for 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)
}
}
})
Hmm, the accepted solution didn't work for me and the periodic observer solutions seem heavy handed.
Here's my suggestion, observe timeControlerStatus on AVPlayer.
// 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)
}
}
In Swift 5.3
Vars:
private var playerItemBufferEmptyObserver: NSKeyValueObservation?
private var playerItemBufferKeepUpObserver: NSKeyValueObservation?
private var playerItemBufferFullObserver: NSKeyValueObservation?
AddObservers
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()
}
RemoveObservers
playerItemBufferEmptyObserver?.invalidate()
playerItemBufferEmptyObserver = nil
playerItemBufferKeepUpObserver?.invalidate()
playerItemBufferKeepUpObserver = nil
playerItemBufferFullObserver?.invalidate()
playerItemBufferFullObserver = nil
We can directly Observe Playback State using the state observer method once is there any playback state changes it will be notified, it's a really easy way and it's tested with swift 5 and iOS 13.0+
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
Solution for Xamarin inspired by Marco's answer
// KVO registrations
private void Initialize()
{
playbackBufferEmptyObserver?.Dispose();
playbackBufferEmptyObserver = (NSObject)playerItem.AddObserver("playbackBufferEmpty",
NSKeyValueObservingOptions.New,
AVPlayerItem_BufferUpdated);
playbackLikelyToKeepUpObserver?.Dispose();
playbackLikelyToKeepUpObserver = (NSObject)playerItem.AddObserver("playbackLikelyToKeepUp",
NSKeyValueObservingOptions.New,
AVPlayerItem_BufferUpdated);
playbackBufferFullObserver?.Dispose();
playbackBufferFullObserver = (NSObject)playerItem.AddObserver("playbackBufferFull",
NSKeyValueObservingOptions.New,
AVPlayerItem_BufferUpdated);
}
private void AVPlayerItem_BufferUpdated(NSObservedChange e)
{
ReportVideoBuffering();
}
private void ReportVideoBuffering()
{
// currentPlayerItem is the current AVPlayerItem of AVPlayer
var isBuffering = !currentPlayerItem.PlaybackLikelyToKeepUp;
// NOTE don't make "buffering" as one of your PlayerState.
// Treat it as a separate property instead. Learned this the hard way.
Buffering?.Invoke(this, new BufferingEventArgs(isBuffering));
}
Please note that
Use a weak reference to self in the callback block to prevent creating a retain cycle.
func playRemote(url: URL) {
showSpinner()
let playerItem = AVPlayerItem(url: url)
avPlayer = AVPlayer(playerItem: playerItem)
avPlayer.rate = 1.0
avPlayer.play()
self.avPlayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1,
timescale: 600), queue: DispatchQueue.main, using: { [weak self] time in
if self?.avPlayer.currentItem?.status == AVPlayerItem.Status.readyToPlay {
if let isPlaybackLikelyToKeepUp = self?.avPlayer.currentItem?.isPlaybackLikelyToKeepUp {
self?.removeSpinner()
}
}
})
}
}
Using Combine you can easily subscribe to the publisher for when an AVPlayerItem is buffering or not like so:
// 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
})
Here is a simple method, that works with Swift 5.
This will add the loadingIndicator when your player is stalled
NotificationCenter.default.addObserver(self, selector:
#selector(playerStalled(_:)), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: self.player?.currentItem)
#objc func playerStalled(_ notification: Notification){
self.loadingIndicator.isHidden = false
self.playPauseButton.isHidden = true
}
This will show loader Indicator when buffer is empty:
if let isPlayBackBufferEmpty = self.player?.currentItem?.isPlaybackBufferEmpty{
if isPlayBackBufferEmpty{
self.loadingIndicator.isHidden = false
self.playPauseButton.isHidden = true
}
}
This will hide the loader when player is ready to play:
if self.playerItem?.status == AVPlayerItem.Status.readyToPlay{
if let isPlaybackLikelyToKeepUp = self.player?.currentItem?.isPlaybackLikelyToKeepUp {
if isPlaybackLikelyToKeepUp{
self.loadingIndicator.isHidden = true
self.playPauseButton.isHidden = false
}
}
}

Resources