Detect volume button press - ios

Volume button notification function is not being called.
Code:
func listenVolumeButton(){
// Option #1
NSNotificationCenter.defaultCenter().addObserver(self, selector: "volumeChanged:", name: "AVSystemController_SystemVolumeDidChangeNotification", object: nil)
// Option #2
var audioSession = AVAudioSession()
audioSession.setActive(true, error: nil)
audioSession.addObserver(self, forKeyPath: "volumeChanged", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "volumeChanged"{
print("got in here")
}
}
func volumeChanged(notification: NSNotification){
print("got in here")
}
listenVolumeButton() is being called in viewWillAppear
The code is not getting to the print statement "got in here", in either case.
I am trying two different ways to do it, neither way is working.
I have followed this: Detect iPhone Volume Button Up Press?

Using the second method, the value of the key path should be "outputVolume". That is the property we are observing.
So change the code to,
var outputVolumeObserve: NSKeyValueObservation?
let audioSession = AVAudioSession.sharedInstance()
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {}
outputVolumeObserve = audioSession.observe(\.outputVolume) { (audioSession, changes) in
/// TODOs
}
}

The code above won't work in Swift 3, in that case, try this:
func listenVolumeButton() {
do {
try audioSession.setActive(true)
} catch {
print("some error")
}
audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
print("got in here")
}
}

With this code you can listen whenever the user taps the volume hardware button.
class VolumeListener {
static let kVolumeKey = "volume"
static let shared = VolumeListener()
private let kAudioVolumeChangeReasonNotificationParameter = "AVSystemController_AudioVolumeChangeReasonNotificationParameter"
private let kAudioVolumeNotificationParameter = "AVSystemController_AudioVolumeNotificationParameter"
private let kExplicitVolumeChange = "ExplicitVolumeChange"
private let kSystemVolumeDidChangeNotificationName = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
private var hasSetup = false
func start() {
guard !self.hasSetup else {
return
}
self.setup()
self.hasSetup = true
}
private func setup() {
guard let rootViewController = UIApplication.shared.windows.first?.rootViewController else {
return
}
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.clipsToBounds = true
rootViewController.view.addSubview(volumeView)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.volumeChanged),
name: kSystemVolumeDidChangeNotificationName,
object: nil
)
volumeView.removeFromSuperview()
}
#objc func volumeChanged(_ notification: NSNotification) {
guard let userInfo = notification.userInfo,
let volume = userInfo[kAudioVolumeNotificationParameter] as? Float,
let changeReason = userInfo[kAudioVolumeChangeReasonNotificationParameter] as? String,
changeReason == kExplicitVolumeChange
else {
return
}
NotificationCenter.default.post(name: "volumeListenerUserDidInteractWithVolume", object: nil,
userInfo: [VolumeListener.kVolumeKey: volume])
}
}
And to listen you just need to add the observer:
NotificationCenter.default.addObserver(self, selector: #selector(self.userInteractedWithVolume),
name: "volumeListenerUserDidInteractWithVolume", object: nil)
You can access the volume value by checking the userInfo:
#objc private func userInteractedWithVolume(_ notification: Notification) {
guard let volume = notification.userInfo?[VolumeListener.kVolumeKey] as? Float else {
return
}
print("volume: \(volume)")
}

import AVFoundation
import MediaPlayer
override func viewDidLoad() {
super.viewDidLoad()
let volumeView = MPVolumeView(frame: CGRect.zero)
for subview in volumeView.subviews {
if let button = subview as? UIButton {
button.setImage(nil, for: .normal)
button.isEnabled = false
button.sizeToFit()
}
}
UIApplication.shared.windows.first?.addSubview(volumeView)
UIApplication.shared.windows.first?.sendSubview(toBack: volumeView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
AVAudioSession.sharedInstance().addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
do { try AVAudioSession.sharedInstance().setActive(true) }
catch { debugPrint("\(error)") }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
AVAudioSession.sharedInstance().removeObserver(self, forKeyPath: "outputVolume")
do { try AVAudioSession.sharedInstance().setActive(false) }
catch { debugPrint("\(error)") }
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let key = keyPath else { return }
switch key {
case "outputVolume":
guard let dict = change, let temp = dict[NSKeyValueChangeKey.newKey] as? Float, temp != 0.5 else { return }
let systemSlider = MPVolumeView().subviews.first { (aView) -> Bool in
return NSStringFromClass(aView.classForCoder) == "MPVolumeSlider" ? true : false
} as? UISlider
systemSlider?.setValue(0.5, animated: false)
guard systemSlider != nil else { return }
debugPrint("Either volume button tapped.")
default:
break
}
}
When observing a new value, I set the system volume back to 0.5. This will probably anger users using music simultaneously, therefore I do not recommend my own answer in production.

If interested here is a RxSwift version.
func volumeRx() -> Observable<Void> {
Observable<Void>.create {
subscriber in
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
} catch let e {
subscriber.onError(e)
}
let outputVolumeObserve = audioSession.observe(\.outputVolume) {
(audioSession, changes) in
subscriber.onNext(Void())
}
return Disposables.create {
outputVolumeObserve.invalidate()
}
}
}
Usage
volumeRx()
.subscribe(onNext: {
print("Volume changed")
}).disposed(by: disposeBag)

Related

I want to add activity indicator to my avplayer when its buffering

private func startVideo() {
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/VolkswagenGTIReview.mp4") {
player = AVPlayer(url: url)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
playerViewController.view.frame = avPlayerView.bounds
addChild(playerViewController)
avPlayerView.addSubview(playerViewController.view)
playerViewController.didMove(toParent: self)
player?.play()
}
}
need to add a activity loader whenever the video is buffering
You can get this working using the following code. Observing the KeyPath on the Player can make you achieve this.
var activityIndicator: UIActivityIndicatorView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if player != nil {
player.play()
player.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
}
}
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 {
if #available(iOS 10.0, *) {
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!.activityIndicator.stopAnimating()
} else {
self!.activityIndicator.startAnimating()
}
}
}
} else {
// Fallback on earlier versions
self.activityIndicator.stopAnimating()
}
}
}

Swift AVAudioPlayer's playing status always be true?

Here's my code:
#objc func playSmusic1() {
guard let url = Bundle.main.url(forResource: "Snote6", withExtension: "wav") else { return }
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
player = try AVAudioPlayer(contentsOf: url, fileTypeHint: AVFileType.wav.rawValue)
guard let player = player else { return }
while (true) {
player.play()
player.enableRate = true;
player.rate = playrate
if !player.isPlaying {
break
}
}
} catch let error {
print(error.localizedDescription)
}
}
I found player.isPlaying properties always be true, so sometimes a tone will be play for 2 or 3 times. How to fix this bug? Thanks a lot!
First: Don't use while(true) for checking because it blocks the main thread!
You should add KVO observer to check this property asynchronously e.g.:
class YourController : UIViewController {
var player: AVAudioPlayer?
//...
deinit {
player?.removeObserver(self, forKeyPath: #keyPath(AVAudioPlayer.isPlaying))
}
func play() {
//...
player?.addObserver(self,
forKeyPath: #keyPath(AVAudioPlayer.isPlaying),
options: .new,
context: nil)
player?.play()
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(AVAudioPlayer.isPlaying) {
if let isPlaying = player?.isPlaying {
print(isPlaying)
}
}
else {
self.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

how to detect the changes of volume in iOS?

I‘ve read a lot solution provided by stackOverFlow and gitHub, and all of them doesn's work for me.I've tried the following tow way:
using audioSession
override func viewWillAppear(_ animated: Bool) {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true, options: [])
} catch {
print("error")
}
// this print shows, got the current volume.
//but what I need is the notification of the change of volume
print("view will appear ,this volume is \(audioSession.outputVolume)")
audioSession.addObserver(self,
forKeyPath: "outputVolume",
options: [.new],
context: nil)
}
override class func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
if keyPath == "outputVolume"{
let audioSession = AVAudioSession.sharedInstance()
let volume = audioSession.outputVolume
//this print doesn't shows up
print("observed volume is \(volume)")
}
}
using notification
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(observed(_:)), name: Notification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
}
#objc func observed(_ notification: Notification){
print("observed")
}
by the way, I'm using simulator(iPhone11, iOS13.1),Xcode version is 11.1
Try to use following:
import UIKit
import MediaPlayer
import AVFoundation
class ViewController: UIViewController {
private var audioLevel: Float = 0.0
deinit {
audioSession.removeObserver(self, forKeyPath: "outputVolume")
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
listenVolumeButton()
}
func listenVolumeButton() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true, options: [])
audioSession.addObserver(self, forKeyPath: "outputVolume",
options: NSKeyValueObservingOptions.new, context: nil)
audioLevel = audioSession.outputVolume
} catch {
print("Error")
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "outputVolume" {
let audioSession = AVAudioSession.sharedInstance()
if audioSession.outputVolume > audioLevel {
print("Hello")
audioLevel = audioSession.outputVolume
}
if audioSession.outputVolume < audioLevel {
print("Goodbye")
audioLevel = audioSession.outputVolume
}
if audioSession.outputVolume > 0.999 {
(MPVolumeView().subviews.filter { NSStringFromClass($0.classForCoder) == "MPVolumeSlider" }.first as? UISlider)?.setValue(0.9375, animated: false)
audioLevel = 0.9375
}
if audioSession.outputVolume < 0.001 {
(MPVolumeView().subviews.filter { NSStringFromClass($0.classForCoder) == "MPVolumeSlider"}.first as? UISlider)?.setValue(0.0625, animated: false)
audioLevel = 0.0625
}
}
}
}
Hope this will help you!
You need to ensure your app's audio session is active for this to work:
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(true)
startObservingVolumeChanges()
} catch {
print(“Failed to activate audio session")
}
So if all you need is to query the current system volume:
let volume = audioSession.outputVolume
Or we can be notified of changes like so:
private struct Observation {
static let VolumeKey = "outputVolume"
static var Context = 0
}
func startObservingVolumeChanges() {
audioSession.addObserver(self, forKeyPath: Observation.VolumeKey, options:
[.Initial, .New], context: &Observation.Context)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object:
AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>)
{
if context == &Observation.Context {
if keyPath == Observation.VolumeKey, let volume = (change?
[NSKeyValueChangeNewKey] as? NSNumber)?.floatValue {
// `volume` contains the new system output volume...
print("Volume: \(volume)")
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change,
context: context)
}
}
Don't forget to stop observing before being deallocated:
func stopObservingVolumeChanges() {
audioSession.removeObserver(self, forKeyPath: Observation.VolumeKey,
context: &Observation.Context)
}

Observing AVPlayer: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled

Hi I'm trying to observe an AVPlayerItem and an AVPlayer in my UIViewController.
But I got some trouble doing it. I've googled a lot and found lot's of examples but none of these worked for me.
Here is my ViewController:
class VideoAudioViewController: UIViewController {
var audioPlayer: AVPlayer!
var playerItem: AVPlayerItem!
private var audio: Audio
private let ITEM_STATUS_PATH = #keyPath(AVPlayerItem.status)
private let PLAYER_STATUS_PATH = #keyPath(AVPlayer.status)
private let PLAYER_RATE_PATH = #keyPath(AVPlayer.rate)
init(for audio: Audio) {
self.audio = audio
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
// Play audio
playerItem = AVPlayerItem(url: audio.url)
playerItem.addObserver(self, forKeyPath: ITEM_STATUS_PATH, options: [.old, .new], context: nil)
audioPlayer = AVPlayer(playerItem: playerItem)
audioPlayer!.play()
// audioPlayer!.
var audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
} catch {
print("error")
}
NotificationCenter.default.addObserver(
self, selector: #selector(handleInterruption), name: .AVAudioSessionInterruption, object: audioSession)
audioPlayer.addObserver(self, forKeyPath: PLAYER_STATUS_PATH, options: [.old, .new], context: nil)
audioPlayer.addObserver(self, forKeyPath: PLAYER_RATE_PATH, options: [.old, .new], context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "status" {
switch (audioPlayer.status) {
case AVPlayerStatus.readyToPlay:
print("player status ready to play")
audioPlayer.play()
break
case AVPlayerStatus.failed:
print("player status failed")
break
default:
break
}
return
} else if keyPath == "rate" {
if audioPlayer.rate != 0 {
playPauseButton.setImage(#imageLiteral(resourceName: "ic_pause_circle_outline_white"), for: .normal)
} else {
playPauseButton.setImage(#imageLiteral(resourceName: "ic_play_circle_outline_white"), for: .normal)
}
} else if keyPath == "status" {
switch (playerItem.status) {
case AVPlayerItemStatus.readyToPlay:
print("player item ready to play")
break
case AVPlayerItemStatus.failed:
print("player item failed")
break
default:
break
}
return
}
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
// MARK: - AVAudioPlayer delegate
func handleInterruption(_ notification: Notification) {
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSessionInterruptionType(rawValue: typeValue) else {
return
}
if type == .began {
playPauseButton.setImage(#imageLiteral(resourceName: "ic_play_circle_outline_white"), for: .normal)
}
else if type == .ended {
// guard let optionsValue =
// userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else {
// return
// }
// let options = AVAudioSessionInterruptionOptions(rawValue: optionsValue)
// if options.contains(.shouldResume) {
// Interruption Ended - playback should resume
audioPlayer?.play()
// }
}
}
}
My problem is this:
Terminati
ng app due to uncaught exception 'NSInternalInconsistencyException', reason: '<TheSimpleClub_Nachhilfe.VideoAudioViewController: 0x7fa6ee2b0c10>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
Key path: rate
Observed object: <AVPlayer: 0x60000021c8c0>
Change: {
kind = 1;
new = 1;
old = 1;
}
Context: 0
x0'
The error occurrs in this line:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
What can I do to resolve this?

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