AVPlayer, notification for play/pause state? - ios

I'm searching for a way to get notified the exact moment when AVPlayer starts playing. There's the "rate" property, but currently I am checking it periodically with an NSTimer to get updates.
I tried KVO, but apparently it's not KVO compliant.
I know that there are events when the player ENDED. But i'm talking about pause here.
I also KVO subscribed to AVPlayerItem's "status", but it's showing me when the HTTP asset has finished caching, no play/pause. I also started collecting all calls of play/pause, requesting an instant UI update afterwards, but it takes some more runloops before AVPlayer really starts playing. I'd just love to update my button instantly.

Why do you say that "rate" is not KVO complaint?
It works for me.
Here is what I did:
- (void)viewDidLoad
{
...
[self.player addObserver:self forKeyPath:#"rate" options:0 context:nil];
}
And then:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"rate"]) {
if ([self.player rate]) {
[self changeToPause]; // This changes the button to Pause
}
else {
[self changeToPlay]; // This changes the button to Play
}
}
}

For iOS 10 onwards You can check new property of AVPlayer timeControlStatus.
if(avPlayerObject.timeControlStatus==AVPlayerTimeControlStatusPaused)
{
//Paused mode
}
else if(avPlayerObject.timeControlStatus==AVPlayerTimeControlStatusPlaying)
{
//Play mode
}

AVPalyer as default observer to track the current duration of the video ,when you pause or resume the video you can get paused time by using one global variable (inside observer update that variable)
CMTime interval = CMTimeMake(1, 1);
//The capture of self here is coming in with your implicit property access of self.currentduration - you can't refer to self or properties on self from within a block that will be strongly retained by self.
//You can get around this by creating a weak reference to self before accessing timerDisp inside your block
__weak typeof(self) weakSelf = self;
self.timeObserverToken = [_player addPeriodicTimeObserverForInterval:interval queue:NULL usingBlock: ^(CMTime time)
{
_currentDuration = (int)CMTimeGetSeconds (_player.currentTime);
if(!_isPlaying)
{
_pausedDuration = _currentDuration;
}
}

If you're targeting iOS 13 and up, you can pull this off elegantly using Combine:
cancellable = myAVPlayerInstance.publisher(for: \.timeControlStatus)
.sink { [unowned self] status in
...
}
where status is any case of AVPlayer.TimeControlStatus

player = AVPlayer(url: URL(fileURLWithPath: path))
player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.new, context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
if player.rate > 0 {
print("video started")
}
}
}
in swift

Add an observer to your AVPlayer object's rate value:
player.addObserver(self, forKeyPath: "rate", options: [], context: nil)
And override the method that will be called when the rate changes:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate", let player = object as? AVPlayer {
if player.rate == 1 {
print("Playing")
} else {
print("Paused")
}
}
}

Need to add an observer to AVPlayer object's rate value:
player?.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.new, context: nil)
Override below method to observe changes in rate property
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
if let status = player?.timeControlStatus {
switch status{
case .paused:
//Paused mode
print("paused")
case .waitingToPlayAtSpecifiedRate:
//Resumed
print("resumed")
case .playing:
//Video Ended
print("ended")
#unknown default:
print("For future versions")
}
}
}
}

Related

Why does observer for status change of AVAsset not work?

I implemented the following code from Apple. It is meant to observe for a change in the status of a playerItem. The problem is that for some reason it does not work. The observe function does not run when ready.
All relevant code is below:
func preloadVideo(media: Media) {
//setup code, and then:
media.playerItem1!.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
}
Observe method:
private var playerItemContext = 0
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
print("jdslfhjkfdhaldfahjkflhajfldashkjfdshkjlas")
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath, of: object,change: change, context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItem.Status
// Get the status change from the change dictionary
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
print("dsfdgddgdggdgdgdgddsafdsaf434fdfdg433444343")
// Switch over the status
switch status {
case .readyToPlay:
//Stop animation, and go to p3
print("dsfdsafdsaf434433444343")
if goToVideo {
print("Runinfdsh sahkbdsfhbsadfbadsfl")
goToP3()
}
break
// Player item is ready to play.
case .failed: break
// Player item failed. See error.
case .unknown: break
// Player item is not yet ready.
#unknown default: break
//fatal error
}
}
}
This method will run first:
#objc func viewHandleTap(_ sender: UITapGestureRecognizer) {
if selectedPost?.interimMedia.first?.imageURLString != nil || selectedPost?.interimMedia.first!.playerQueue?.status.rawValue == 1 {
//do something
} else {
//status is 0 (video has not loaded yet)//load animation
//this runs...
goToVideo = true
}
}
How can I make it work? Currently nothing in the observe method is printing.
If there is another method to accomplish this go ahead and post as answer.
Try by updating addObserver options to [.old, .new, .initial, .prior].

AVPlayer on selected rate only once

I've got an issue with AVPlayer. The case is: I've got one function, that sets the rate of AVPlayer
func applyEffect(effectNumber : Int) {
switch effectNumber {
case 0:
self.AvPlayer.setRate(Float(1.0), time: kCMTimeInvalid, atHostTime: kCMTimeInvalid)
case 1:
self.AvPlayer.setRate(Float(1.5), time: kCMTimeInvalid, atHostTime: kCMTimeInvalid)
case 2:
self.AvPlayer.setRate(Float(0.5), time: kCMTimeInvalid, atHostTime: kCMTimeInvalid)
default:
self.AvPlayer.setRate(Float(1.0), time: kCMTimeInvalid, atHostTime: kCMTimeInvalid)
}
}
For every response I download audio file into AVPlayerItem and save them as array. I've got an observer for item which calls applyEffect function if AVPlayerItem.status is changed, otherwise it applies effect immediately:
if AVCurrentItem.status != AVPlayerItemStatus.readyToPlay {
AVCurrentItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
pushMessageLoading()
} else {
self.applyEffect(effectNumber: currentEffect)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "status" {
popMessageLoading()
self.applyEffect(effectNumber: currentEffect)
AVCurrentItem.removeObserver(self, forKeyPath: "status")
}
}
The AVPlayer property "automaticallyWaitsToMinimizeStalling" is set to false during the initialisation. So, first time audio plays on a requested rate, but all times after - on rate like 1.0. What's the problem?
The root of evil is simple: I've used AVPlayer.play which equals to AVPlayer.rate = 1.0, instead of this you need to use AVPlayer.rate = desired value

Detect when AVPlayer is playing

How do you detect when an AVPlayer is playing?. There appears to be a slight delay between when the play() function is called and the video actually plays.
For Video:
AVPlayer have an argument called rate (Float), if the rate is greater than 0.0, there a video that currently playing.
You can check the if the rate is !=0: (the rate can be negative if the player goes backwards)
if vidPlayer != nil && vidPlayer.rate != 0 {
println("playing")
}
AVPlayer class reference
As far as I know, I agree with you that there is a slight delay between when the play() function is called and the video actually plays (In another word, the time that the first frame of the video has been rendered). The delay depends on some criteria such as video types (VOD or live streaming), the network condition, ... However, fortunately, we are able to know whenever the first frame of the video rendered, I mean exactly when the video actually plays.
By observing the status of the current AVPlayerItem and whenever it is AVPlayerItemStatusReadyToPlay, that should be the first frame has been rendered.
[self.playerItem addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
-(void)observeValueForKeyPath:(NSString*)path ofObject:(id)object change:(NSDictionary*)change context:(void*) context {
if([self.playerItem status] == AVPlayerStatusReadyToPlay){
NSLog(#"The video actually plays")
}
}
By the way, there is another solution where we observe readyForDisplay status of AVPlayerLayer, it also indicates whenever the video rendered. However, this solution has a drawback as mentioned in Apple document
/*!
#property readyForDisplay
#abstract Boolean indicating that the first video frame has been made ready for display for the current item of the associated AVPlayer.
#discusssion Use this property as an indicator of when best to show or animate-in an AVPlayerLayer into view.
An AVPlayerLayer may be displayed, or made visible, while this propoerty is NO, however the layer will not have any
user-visible content until the value becomes YES.
This property remains NO for an AVPlayer currentItem whose AVAsset contains no enabled video tracks.
*/
#property(nonatomic, readonly, getter=isReadyForDisplay) BOOL readyForDisplay;
Here is the sample code
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[self.playerLayer addObserver:self forKeyPath:#"readyForDisplay" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
-(void)observeValueForKeyPath:(NSString*)path ofObject:(id)object change:(NSDictionary*)change context:(void*) context {
if([self.playerLayer isReadyForDisplay]){
NSLog(#"Ready to display");
}
}
Thereotically, [self.playerLayer isReadyForDisplay] should return YES, however, as the document, it is not guaranted.
I hope this would be helpful.
Swift 4
Method 1
var rateObserver: NSKeyValueObservation?
self.rateObserver = myPlayer.observe(\.rate, options: [.new, .old], changeHandler: { (player, change) in
if player.rate == 1 {
print("Playing")
}else{
print("Stop")
}
})
// Later You Can Remove Observer
self.rateObserver?.invalidate()
Method 2
myPlayer.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions(rawValue: NSKeyValueObservingOptions.new.rawValue | NSKeyValueObservingOptions.old.rawValue), context: nil)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
if player.rate == 1 {
print("Playing")
}else{
print("Stop")
}
}
}
There are two things you might want to detect:
AVPlayerLayer's isReadyForDisplay is when the first frame has been received.
AVPlayerItem's readyToPlay is when the video actually starts playing.
In order to check both status you can use observability.
You're going to have three objects defined in your class:
var player: AVPlayer?
var playerItem: AVPlayerItem?
var playerLayer: AVPlayerLayer?
The player implementation is going to be like the following:
guard let videoURL = URL(string: "<videoPath>") else {
return
}
asset = AVAsset(url: videoURL)
guard let asset = asset else {
return
}
playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: requiredAssetKeys)
guard let playerItem = playerItem else {
return
}
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
player = AVPlayer(playerItem: playerItem)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.addObserver(self,
forKeyPath: #keyPath(AVPlayerLayer.isReadyForDisplay),
options: [.old, .new],
context: &playerLayerContext)
avpController.player = player
avpController.view.frame.size.height = playerView.frame.size.height
avpController.view.frame.size.width = playerView.frame.size.width
playerView.addSubview(avpController.view)
avpController.player?.play()
Here, the context are either simple integers or enum.
You can handle the events by overriding the observeValue method.
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
guard (context == &playerItemContext) ||
(context == &playerLayerContext) else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItem.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
switch status {
case .readyToPlay:
print("Ready to play")
case .failed:
navigationController?.popViewController(animated: true)
case .unknown:
print("Unknown")
#unknown default:
print("Unknown default")
}
} else if keyPath == #keyPath(AVPlayerLayer.isReadyForDisplay) {
if playerLayer?.isReadyForDisplay ?? false {
print("Ready to display")
}
}
}
Don't forget to remove the observers.
deinit {
playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status))
playerLayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayerLayer.isReadyForDisplay))
}

Progress of playbackLikelyToKeepUp

When using an AVPlayer, is there a way to get the progress of playbackLikelyToKeepUp? I was thinking I could look at loadedTimeRanges to see how much has been buffered so far, but from what I understand, the playbackLikelyToKeepUp property is some internally made prediction and does not provide a value of how much data is needed for it to be true.
To put this into perspective, what I'm trying to do is to have a progress view that reaches 100% just as the video starts playing.
To begin playing when AVPlayer will play continuously, you can observe the Key-Value changes on the playbackLikelyToKeepUp, like this:
let PlayerKeepUp = "playbackLikelyToKeepUp"
var isPlayerReady:Bool = false
and then in your Initialiser you add:
// adding the Observers for Status:
self.player?.currentItem?.addObserver(self, forKeyPath: PlayerKeepUp, options: ([NSKeyValueObservingOptions.New, NSKeyValueObservingOptions.Old]), context: &PlayerItemObserverContext)
And Finally, to track when the player is ready to play without getting stuck:
// MARK: KVO Observing Methods:
override public func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
switch (keyPath, context) {
case (PlayerKeepUp as String, &PlayerItemObserverContext):
if(self.player.currentItem?.playbackLikelyToKeepUp == true) {
self.isPlayerReady = true
// HERE YOU FILL UP YOUR PROGRESS VIEW :-)
self.delegate?.playerReady(self.playerURL! as String)
}
break
default:
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
Last but not least, remember to Remove your KVO observance or else you'll crash when de-allocating the player:
deinit {
// Remove observer:
self.player?.currentItem?.removeObserver(self, forKeyPath: PlayerKeepUp, context: &PlayerItemObserverContext)
}
Hope this helps :-)

Detect when AvPlayer is stopped

I'm using the AVPlayer class to read streams.
I have to monitor playback.
Here is my question : Is it possible to detect when the player is stopped by the user ?
I looked at MPMoviePlayerController. If the user stopped the video, this controller sends a notification : MPMovieFinishReasonUserExited. Is there an equivalent ?
You can monitor rate property by adding observer on the player for key rate.
A value of 0.0 means pauses the video, while a value of 1.0 play at the natural rate of the current item.
Apple documentation and this topic.
Hope this helps.
here's the swift 3 code for #Thlbaut's answer
self.avPlayer?.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
then
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "rate" {
if let playRate = self.avPlayer?.rate {
if playRate == 0.0 {
print("playback paused")
} else {
print("playback started")
}
}
}
}

Resources