Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect when AVPlayer is playing

Tags:

ios

avplayer

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.

like image 311
Josh O'Connor Avatar asked Jul 15 '16 04:07

Josh O'Connor


4 Answers

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))
}
like image 146
Onur Tuna Avatar answered Oct 19 '22 09:10

Onur Tuna


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

like image 20
Idan Avatar answered Oct 19 '22 10:10

Idan


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.

like image 13
HSG Avatar answered Oct 19 '22 09:10

HSG


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")
          }
      }
 }
like image 7
ZAFAR007 Avatar answered Oct 19 '22 10:10

ZAFAR007