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))
}
Related
I'm looking for a definitive answer how to get current AVPlayer/AVPlayerItem state on iOS 9 onwards. To put things simple, let them be Google's ExoPlayer style states:
idle (no media or error)
buffering (video is not actually playing/advancing and waiting for
more data)
playing (video is actually playing/advancing)
completed (video finished playing to the end)
Note, that at this point I'm not looking for a way to track state changes (via notifications, KVO-observing or other means), just a state at current point of time. Consider the following pseudo-code:
typedef enum : NSUInteger {
PlayerStateIdle,
PlayerStateBuffering,
PlayerStatePlaying,
PlayerStateCompleted
} PlayerState;
+ (PlayerState)resolvePlayerState:(AVPlayer*)player {
// Magic code here
}
Walls, I've been banging my head against so far:
timeControlStatus available from iOS 10 onwards
playbackBufferEmpty is always true
playbackBufferFull is always false
loadedTimeRanges may look promising at first glance, but there's neither an indication of how much time must be pre-buffered for playback nor guarantee that currentTime being on the edge of loaded time range is a stall
According to the documentation
You can use Key-value observing to observe these state changes as they
occur. One of the most important player item properties to observe is
its status. The status indicates if the item is ready for playback and
generally available for use.
To setup observing:
func prepareToPlay() {
let url = <#Asset URL#>
// Create asset to be played
asset = AVAsset(url: url)
let assetKeys = [
"playable",
"hasProtectedContent"
]
// Create a new AVPlayerItem with the asset and an
// array of asset keys to be automatically loaded
playerItem = AVPlayerItem(asset: asset,
automaticallyLoadedAssetKeys: assetKeys)
// Register as an observer of the player item's status property
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
// Associate the player item with the player
player = AVPlayer(playerItem: playerItem)
}
To handle:
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
// Get the status change from the change dictionary
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
// Switch over the status
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}
}
}
Not sure what your problem is.
You can just access player.status or player.timeControlStatus and return a result that matches your ENUM
Is that what you mean?
I am trying to play a video after I download from remote using following code.
let player = AVPlayer(url: URL(dataRepresentation: VideoService.instance.videoInfoArray[0].video, relativeTo: nil)!)
let playerController = AVPlayerViewController()
playerController.player = player
present(playerController, animated: true) {
player.play()
}
My VideoService class is a singleton class and video variable type is Data(). However when I try to play video with below code it is not working. How can I play video using data representative?
According to URL class documentation the initializer init?(dataRepresentation:relativeTo:isAbsolute:) takes Data argument, yes, but that data is supposed to be an ASCII representation of an URL string, not the actual data you want to play.
What you need to do is save your video data to a file and use the file's URL to initialize AVPlayer
guard let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)?.first else { return }
url.appendPathComponent("video.mpeg") // or whatever extension the video is
VideoService.instance.videoInfoArray[0].video.write(to: url) // assuming video is of Data type
let player = AVPlayer(url: url)
// other stuff with the player
To determine the reason why the player can't play your file, use key-value observing of player's status property. If you observe a change to AVPlayer.Status.failed, then you can check the player's error property to see the localized reason and error code.
var playerStatusContext = 0
player.addObserver(player, forKeyPath: "status", options: [.new, .initial], context: &playerStatusContext)
// (...)
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
guard context == &playerStatusContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayer.status) {
let status: AVPlayer.Status
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayer.Status(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
if status == .failed {
print(player.error)
}
}
}
I'm using an AVPlayer to play a video url. I followed Apple's code to play the video and handle errors:
Inside the section that says Respond to A State Change there is a switch statement that has a .failed case and the commented out code says: // Player item failed. See error. I've had this run a couple of times by putting a break point there.
The problem is I don't see anything like a variable with a NSError type that would give me an option to actually print out what the error is. How can I find out what the .failed the error is?
Observe the Player's State:
let url: URL = // Asset URL
var asset: AVAsset!
var player: AVPlayer!
var playerItem: AVPlayerItem!
// Key-value observing context
private var playerItemContext = 0
let requiredAssetKeys = [
"playable",
"hasProtectedContent"
]
func prepareToPlay() {
// Create the asset to play
asset = AVAsset(url: url)
// Create a new AVPlayerItem with the asset and an
// array of asset keys to be automatically loaded
playerItem = AVPlayerItem(asset: asset,
automaticallyLoadedAssetKeys: requiredAssetKeys)
// Register as an observer of the player item's status property
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.old, .new],
context: &playerItemContext)
// Associate the player item with the player
player = AVPlayer(playerItem: playerItem)
}
Respond to a State Change
override func observeValue(forKeyPath keyPath: String?,
of object: Any?,
change: [NSKeyValueChangeKey : Any]?,
context: UnsafeMutableRawPointer?) {
// Only handle observations for the playerItemContext
guard context == &playerItemContext else {
super.observeValue(forKeyPath: keyPath,
of: object,
change: change,
context: context)
return
}
if keyPath == #keyPath(AVPlayerItem.status) {
let status: AVPlayerItemStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
// Switch over status value
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
// Player item failed. See error.
case .unknown:
// Player item is not yet ready.
}
}
}
if status is .failed then you can access the error from AVPlayer itself like this.
switch status {
case .readyToPlay:
// Player item is ready to play.
case .failed:
handleErrorWithMessage(player.currentItem?.error?.localizedDescription, error:player.currentItem?.error)
case .unknown:
// Player item is not yet ready.
}
Find below function.
func handleErrorWithMessage(_ message: String?, error: Error? = nil) {
print("Error occured with message: \(message), error: \(error).")
}
Hope this helps.
I have to detect whether the video is in playing or buffering mode.I am loading the video from a URL. I have tried the below code and I am able to track after once the video has started playing, but not when it is in buffering state.
Also, I want to add an overlay view in my player. I have tried to add the overlay in AVPlayer but when in full screen mode the overlay disappears.
Need some suggestions. Thanks in advance. Below is my code:
let playerAV = AVPlayerViewController()
var player = AVPlayer()
player = AVPlayer(URL: url)
print(url)
playerAV.player = player
playerAV.view.frame = CGRectMake(0, 0, self.videoView.frame.width, self.videoView.frame.height)
self.addChildViewController(playerAV)
self.videoView.addSubview(playerAV.view)
playerAV.didMoveToParentViewController(self)
playerAV.player?.play()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChannelDetailViewController.notificationObserver(_:)), name:AVPlayerItemDidPlayToEndTimeNotification , object: player.currentItem)
_ = UIDevice.beginGeneratingDeviceOrientationNotifications
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChannelDetailViewController.deviceOrientationDidChange(_:)) , name:
UIDeviceOrientationDidChangeNotification, object: nil)
player.addObserver(self, forKeyPath: "rate", options: NSKeyValueObservingOptions.New, context: nil)
player.addPeriodicTimeObserverForInterval(CMTime(value: 1, timescale: 3), queue: dispatch_get_main_queue()) { [weak self] time in
self?.handlePlayerStatus(time)
}
func handlePlayerStatus(time: CMTime) {
if player.status == .ReadyToPlay {
// buffering is finished, the player is ready to play
print("playing")
}
if player.status == .Unknown{
print("Buffering")
}
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "rate" {
if let rate = change?[NSKeyValueChangeNewKey] as? Float {
if player.currentItem!.status == AVPlayerItemStatus.ReadyToPlay{
if rate != 0 && player.error == nil {
print("normal playback")
}
else{
print("playback stopped")
}
}else if player.currentItem?.status == AVPlayerItemStatus.Unknown{
print("test")
}
}
}
print("you are here")
}
check here for project
Check my answer: https://stackoverflow.com/a/38867386/5109911, this shows you how check if the player is loading the buffer, to check if it is ready to play you have to check both player.currentItem.status and player.status
To add an overlay to AVPlayer I suggest using an UIView above your AVPlayer layer.
I took a slightly different approach to a similar problem - updating the UI when the AVPlayer starts actual playback (in our case, it was audio playback, but the same principle would apply).
We used a boundary time to execute a block at the start of playback – specifically after 1/3 of a second of playback – to update the UI at the same time as audible playback:
let times = [NSValue(time:CMTimeMake(1,3))]
_ = self.player.addBoundaryTimeObserver(forTimes: times, queue: DispatchQueue.main, using: {
[weak self] time in
// Code to update the UI goes here, e.g.
self?.someUIView.isHidden = false
})
I have a little more detail on the approach (and a sample Xcode project) on our company blog: http://www.artermobilize.com/blog/2017/02/09/detect-when-ios-avplayer-finishes-buffering-using-swift/
As to question #2, I concur with Marco's suggestion of using an UIView overtop of the AVPlayer layer.
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")
}
}
}
}