In my app at some point the user can invoke video playing with tapping on an UI element, and then the following code segment will be executed:
self.loadingView.frame = _frameWhereItShouldBeLocated;
[self.loadSpinner startAnimating]; // self.loadSpinner is an UIActivityIndicatorView
self.loadingView.hidden = FALSE;
AVPlayer *player = [AVPlayer playerWithURL:fileUrl]; // fileUrl is where is the video file is hosted, which is not a local path
if ([player.currentItem.asset isPlayable])
{
if (!self.playerController) {
self.playerController = [[AVPlayerViewController alloc] init];
self.playerController.transitioningDelegate = self;
self.playerController.modalPresentationStyle = UIModalPresentationCustom;
}
self.playerController.player = player;
self.playerController.showsPlaybackControls = TRUE;
[self.navigationController presentViewController:self.playerController animated:YES completion:nil];
[self.playerController.player play];
}
I expect the loading view will be visible immediately after user's tap, and then after some time, the player controller is then presented and play the video. However, it happens that there is a significant delay before the loading view become visible.
This is what I expected:
User tap -> loading view shown -> (some time for loading the video, etc) -> play video
Instead this is what I've got:
User tap -> (significant time delay) -> loading view shown -> play video
After some debugging I found that the delay is caused by the [player.currentItem.asset isPlayable] call, i.e. the loading view only become visible after the call is returned. I tried to put the segment below the display of loading view in a dispatch_async call but it makes no different.
Is there anyway to handle this to make it behaves as expected?
Thanks a lot!
Insted of if ([player.currentItem.asset isPlayable])
check this
if ((player.rate != 0) && (player.error == nil)) {
// player is playing
}
Or
You can add notification for rate property like below
[player addObserver:self
forKeyPath:#"rate"
options:NSKeyValueObservingOptionNew
context:NULL];
Then check if the new value for observed rate is zero, which means that playback has stopped for some reason, like reaching the end or stalling because of empty buffer.
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:#"rate"]) {
float rate = [change[NSKeyValueChangeNewKey] floatValue];
if (rate == 0.0) {
// Playback stopped
} else if (rate == 1.0) {
// Normal playback
} else if (rate == -1.0) {
// Reverse playback
}
}
}
For rate == 0.0 case, to know what exactly caused the playback to stop, you can do the following checks:
if (self.player.error != nil) {
// Playback failed
}
if (CMTimeGetSeconds(self.player.currentTime) >=
CMTimeGetSeconds(self.player.currentItem.duration)) {
// Playback reached end
} else if (!self.player.currentItem.playbackLikelyToKeepUp) {
// Not ready to play, wait until enough data is loaded
}
Based on above condition hide your indicator view.
And load your indicator view on main thread.
dispatch_async(dispatch_get_main_queue(), ^{
//load spinner
});
Related
Im trying to fully load a video URL into an AVPlayer before the user is able to play the video, for seamless playback.
I add two observers to the AVPlayerItem:
[item addObserver:self forKeyPath:#"loadedTimeRanges"
options:NSKeyValueObservingOptionNew context:&timeRanges];
[item addObserver:self forKeyPath:#"playbackBufferFull"
options:NSKeyValueObservingOptionNew context:&playbackBufferFull];
I wait until value is equal to the duration of the video - fully loaded.
if (context == &timeRanges) {
NSLog(#"BUFFERING");
NSArray *timeRanges = (NSArray *)[change objectForKey:NSKeyValueChangeNewKey];
CMTimeRange timerange = [timeRanges[0] CMTimeRangeValue];
CGFloat value = CMTimeGetSeconds(CMTimeAdd(timerange.start, timerange.duration));
CGFloat duration = CMTimeGetSeconds(self.player.currentItem.duration);
NSLog(#"VALUE:DURATION == %f:%f", value, duration);
if(value == duration) {
NSLog(#"FINISHED");
[self prerollVideoPlayer];
}
return;
}
But this rarely happens. The player usually stops buffering at around 3 for a five second video. Then the "playbackBufferFull" is called
if (context == &playbackBufferFull){
if (self.player.currentItem.playbackBufferFull) {
NSLog(#"IS FULL");
//[self prerollVideoPlayer];
}
return;
}
How can I increase the buffer size? Or how to buffer the rest of the video once playbackBufferFull is called? The videos being played, are only five seconds long.
Take a look at the preferredForwardBufferDuration property on AVPlayerItem.
Something like this should work:
AVPlayerItem* theItem = self.player.currentItem;
theItem.preferredForwardBufferDuration = CMTimeGetSeconds( theItem.duration );
Then you can observe playback buffer full, like you show in your code, to begin playback.
Every time, when I try to playing a megabyte video using AVPlayer, it initially shows a white screen for a second and then starts the video.
Why is this happening if the video is already cached? Is there a way to stop this from happening, so that it goes straight to the video without displaying a white screen?
I tried using AVPlayer's isReady to check the status of AVPlayer and play video only when it's ready, but it still displays the white screen.
Also every time when I try to get the video duration of the video that's about to play through AVPlayer I keep getting 0.0 seconds initially, so I am not able to add a timer to the video either because I can't get the video duration because it keeps displaying a white screen for a second.
Firstly, AVPlayer doesn't show any white screen, its your background which is white. So, basically your AVPlayer is starting late. I guess you press a UIButton and then it loads the file in AVPlayer and immediately start playing it. Thats where the problem is. It may take some time for the AVPlayer to buffer enough data and be ready to play the file. Using KVO, it is possible to be notified for changes of the player status.
So first you need to disable the play button, load the AVPlayer and add an observer:
play.enabled = NO;
player = [AVPlayer playerWithURL:URL];
[player addObserver:self forKeyPath:#"status" options:0 context:nil];
Then enable it after checking AVPlayerStatusReadyToPlay:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (object == player && [keyPath isEqualToString:#"status"]) {
if (player.status == AVPlayerStatusReadyToPlay) {
play.enabled = YES;
}
}
}
I know this is an old question, but I get the same issue even when properly detecting when the AVPlayer is ready to play.
I wanted it to play over an image so that there was a smooth transition between an initial static image, and then moving video.
The trick for me was to set a clear background with:
AVPlayerViewController *controller = [[AVPlayerViewController alloc] init];
[controller.view setBackgroundColor:[UIColor clearColor]];
This way, if I toggle the visibility of the player when it's ready to play, I never see a black or white screen, because the player has a clear background, making for a smooth transition!
Blancos is right. AVPlayer is taking time to achieve state AVPlayerStatusReadyToPlay.
So, initialize the player with url and play it only when it is AVPlayerStatusReadyToPlay.
player = [AVPlayer playerWithURL:URL];
[player addObserver:self forKeyPath:#"status" options:0 context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == player && [keyPath isEqualToString:#"status"]) {
if (player.status == AVPlayerStatusReadyToPlay) {
player.play()
}
}
}
I had the same problem. To avoid adding a KVO, I just set the AVPlayer up when the url is set like so...
var urlToUse: NSURL?
{
didSet
{
guard let urlToUse = urlToUse else { return }
replayPlayer = AVPlayer(URL: urlToUse)
}
}
That way the AVPlayer status will be ready when needed.
I am using AVQueuePlayer to play a few videos, everything works fine in iOS 6 & 7. However, in iOS 8, when the current AVPlayerItem finishes playing the next video does not play. Placing an observer on the queue's currentItem property shows the the queue has the next video set as its current item as expected, it just does not play (even with explicit play calls).
Does anyone have any insight into what might be happening and how to fix it? Has anyone else come across this issue?
I had exactly the same issue and I wasted ten hours on this...
But it fixed now by adding the "insertItem" method into the main thread.
So I have my player as a property :
#property AVQueuePlayer * player;
I just init it like this :
_player = [[AVQueuePlayer alloc] init];
And here is my method to add items from a path :
- (void)addInQueuePlayerFile:(NSString *)path {
AVAsset * asset = [AVAsset assetWithURL:[[NSURL alloc] initFileURLWithPath:path]];
AVPlayerItem * playerItem = [AVPlayerItem playerItemWithAsset:asset];
dispatch_async(dispatch_get_main_queue(), ^{
[_player insertItem:playerItem afterItem:nil];
});
}
So its working but I don't really understand why... So if someone has an answer...
But also if this doesn't work for you, try to add an observer to your player (and/or your playerItem). You can add an observer like that :
[_player addObserver:self forKeyPath:#"status" options:0 context:nil]
And catch it with this method :
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == _player && [keyPath isEqualToString:#"status"]) {
if (_player.status == AVPlayerStatusFailed) {
NSLog(#"AVPlayer Failed");
} else if (_player.status == AVPlayerStatusReadyToPlay) {
NSLog(#"AVPlayer item Ready to Play");
} else if (_player.status == AVPlayerStatusUnknown) {
NSLog(#"AVPlayer item Unknown");
}
}
}
Let me know if this work for you.
The avplayer should always be accessed on the main thread. This is nothing new to iOS8 but perhaps Apple changed how threads are used so you're seeing the issue more often than in iOS7.
To ensure safe access to a player’s nonatomic properties while dynamic changes in playback state may be reported, you must serialize access with the receiver’s notification queue. In the common case, such serialization is naturally achieved by invoking AVPlayer’s various methods on the main thread or queue.
https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVPlayer_Class/index.html
I had the same problem. After several hours of trial, I found that if some of the items in queue have different encryption methods, the player would stall.
For example, if the player just finished playing an encrypted stream, and the next item in queue was unencrypted, the player is not going to move on to the next item.
I'm having some trouble with registering WHEN the player is starting to play external videos (over internet) using AVPlayer. Please read the question before suggesting solutions.
I initialize the player like this:
player = [[AVPlayer alloc] initWithURL:[[NSURL alloc] initWithString:#"http://example.com/video.mp4"]];
playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[playerLayer setFrame:[videoView bounds]];
[videoView.layer addSublayer:playerLayer];
This adds the player to the view correctly. I have added the following two lines of code to keep track of when the player is ready, and what the status/rate is;
[player addObserver:self forKeyPath:#"rate" options:0 context:nil];
[player addObserver:self forKeyPath:#"status" options:0 context:nil];
These two line will call the method - (void)observeValueForKeyPath:.... when something changes with the status or the rate of the AVPlayer.
So far it looks like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
//To print out if it is 'rate' or 'status' that has changed:
NSLog(#"Changed: %#", keyPath);
if ([keyPath isEqualToString:#"rate"]) //If rate has changed:
{
if ([player rate] != 0) //If it started playing
{
NSLog(#"Total time: %f", CMTimeGetSeconds([[player currentItem] duration]));
// This NSLog is supposed to print out the duration of the video.
[self setControls];
// This method (setControls) is supposed to set play/pause-buttons
// as well as labels for the current and total time of the current video.
}
}
else if ([keyPath isEqualToString:#"status"]) // If the status changed
{
if(player.status == AVPlayerStatusReadyToPlay) //If "ReadyToPlay"
{
NSLog(#"ReadyToPlay");
[player play]; //Start the video
}
}
}
The state of the AVPlayer changes to readyToPlay almost immediately after initializing it, and I then call [player play]. When this happens, the rate changes to 1.00000, meaning it's actually playing at that rate, but the video is now just starting to buffer, not playing. The screen is black, and it takes a couple of seconds, and then it starts playing. The rate, however, indicates it starts playing before it does. The rate stays at 1.00000, not going down to 0 when start-buffering, which makes it very difficult for me to know when the player has enough information to start setting the controls (I.E time stamps etc).
The NSLog() printing out the duration of the video above prints out nan (Not A Number), which leads me to think that the item isn't ready to be played, however, the rate stays at 1.0000 until it has buffered a while, then it will actually play, still with rate at 1.0000.
It does, however, get called twice. The rate "changes" to 1.0000 twice without being anything else in between. In neither calls, the duration of the video is an available variable.
My goal is to fetch the current and total timestamp of the video as fast as possible (I.E 0:00/3:52). This will also be used to register the scrubbing of a slider (for fast-forward etc.).
These values are not ready when the player notifies me it's playing at a rate of 1.0000, twice. If I manually click "play" after a second or so (and call [player play]), then it's working. How can I register to know when the video is ready, not just 'ready to get ready'?
See addBoundaryTimeObserverForTimes:queue:usingBlock: on AVPlayer and this example from Apple.
AVPlayer *player = [AVPlayer playerWithURL:[NSURL URLWithString:#"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"]];
[player play];
// Assumes a property: #property (strong) id playerObserver;
// Cannot use kCMTimeZero so instead use a very small period of time
self.playerObserver = [player addBoundaryTimeObserverForTimes:#[[NSValue valueWithCMTime:CMTimeMake(1, 1000)]] queue:NULL usingBlock:^{
//Playback started
[player removeTimeObserver:self.playerObserver];
}];
I think the nearest you'll get to is to observe player.currentItem.playbackLikelyToKeepUp
In my application I have to play audio files stored on a web server. I'm using AVPlayer for it. I have all the play/pause controls and all delegates and observers there which work perfectly fine. On playing small audio files everything works great.
When a long audio file is played it also starts playing fine but after some seconds the AVPlayer pauses the playing (most probably to buffer it). The issue is it doesn't resume on its own again. It keeps in a pause state and if I manually press the play button again it plays smoothly again.
I want to know why AVPlayer doesn't resume automatically and how can I manage to resume the audio again without user pressing the play button again? Thanks.
Yes, it stops because the buffer is empty so it has to wait to load more video. After that you have to manually ask for start again. To solve the problem I followed these steps:
1) Detection: To detect when the player has stopped I use the KVO with the rate property of the value:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"rate"] )
{
if (self.player.rate == 0 && CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) && self.videoPlaying)
{
[self continuePlaying];
}
}
}
This condition: CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) is to detect the difference between arriving at the end of the video or stopping in the middle
2) Wait for the video to load - If you continue playing directly you will not have enough buffer to continue playing without interruption. To know when to start you have to observe the value playbackLikelytoKeepUp from the playerItem (here I use a library to observe with blocks but I think it makes the point):
-(void)continuePlaying
{
if (!self.playerItem.playbackLikelyToKeepUp)
{
self.loadingView.hidden = NO;
__weak typeof(self) wSelf = self;
self.playbackLikelyToKeepUpKVOToken = [self.playerItem addObserverForKeyPath:#keypath(_playerItem.playbackLikelyToKeepUp) block:^(id obj, NSDictionary *change) {
__strong typeof(self) sSelf = wSelf;
if(sSelf)
{
if (sSelf.playerItem.playbackLikelyToKeepUp)
{
[sSelf.playerItem removeObserverForKeyPath:#keypath(_playerItem.playbackLikelyToKeepUp) token:self.playbackLikelyToKeepUpKVOToken];
sSelf.playbackLikelyToKeepUpKVOToken = nil;
[sSelf continuePlaying];
}
}
}];
}
And that's it! problem solved
Edit: By the way the library used is libextobjc
I am working with video files, so there's more to my code than you need, but the following solution should pause the player when it hangs, then check every 0.5 second to see whether we've buffered enough to keep up. If so, it restarts the player. If the player hangs for more than 10 seconds without restarting, we stop the player and apologize to the user. This means you need the right observers in place. The code below is working pretty well for me.
properties defined / init'd in a .h file or elsewhere:
AVPlayer *player;
int playerTryCount = -1; // this should get set to 0 when the AVPlayer starts playing
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
partial .m:
- (AVPlayer *)initializePlayerFromURL:(NSURL *)movieURL {
// create AVPlayer
AVPlayerItem *videoItem = [AVPlayerItem playerItemWithURL:movieURL];
AVPlayer *videoPlayer = [AVPlayer playerWithPlayerItem:videoItem];
// add Observers
[videoItem addObserver:self forKeyPath:#"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
[self startNotificationObservers]; // see method below
// I observe a bunch of other stuff, but this is all you need for this to work
return videoPlayer;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:#"playbackLikelyToKeepUp"]) {
if (self.player.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.player.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.player.currentTime, !=, self.player.currentItem.duration)) {
// if so, post the playerHanging notification
[self.notificationCenter postNotificationName:PlayerHangingNotification object:self.videoPlayer];
}
}
}
- (void)startNotificationObservers {
[self.notificationCenter addObserver:self
selector:#selector(playerContinue)
name:PlayerContinueNotification
object:nil];
[self.notificationCenter addObserver:self
selector:#selector(playerHanging)
name:PlayerHangingNotification
object:nil];
}
// playerHanging simply decides whether to wait 0.5 seconds or not
// if so, it pauses the player and sends a playerContinue notification
// if not, it puts us out of our misery
- (void)playerHanging {
if (playerTryCount <= 10) {
playerTryCount += 1;
[self.player pause];
// start an activity indicator / busy view
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.player];
} else { // this code shouldn't actually execute, but I include it as dummyproofing
[self stopPlaying]; // a method where I clean up the AVPlayer,
// which is already paused
// Here's where I'd put up an alertController or alertView
// to say we're sorry but we just can't go on like this anymore
}
}
// playerContinue does the actual waiting and restarting
- (void)playerContinue {
if (CMTIME_COMPARE_INLINE(self.player.currentTime, ==, self.player.currentItem.duration)) { // we've reached the end
[self stopPlaying];
} else if (playerTryCount > 10) // stop trying
[self stopPlaying];
// put up "sorry" alert
} else if (playerTryCount == 0) {
return; // protects against a race condition
} else if (self.player.currentItem.playbackLikelyToKeepUp == YES) {
// Here I stop/remove the activity indicator I put up in playerHanging
playerTryCount = 0;
[self.player play]; // continue from where we left off
} else { // still hanging, not at end
// create a 0.5-second delay to see if buffering catches up
// then post another playerContinue notification to call this method again
// in a manner that attempts to avoid any recursion or threading nightmares
playerTryCount += 1;
double delayInSeconds = 0.5;
dispatch_time_t executeTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(executeTime, dispatch_get_main_queue(), ^{
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if (playerTryCount > 0) {
if (playerTryCount <= 10) {
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.videoPlayer];
} else {
[self stopPlaying];
// put up "sorry" alert
}
}
});
}
Hope it helps!
Accepted answer gives a possible solution to the problem, but it lacks flexibility, also it's hard to read. Here's more flexible solution.
Add observers:
//_player is instance of AVPlayer
[_player.currentItem addObserver:self forKeyPath:#"status" options:0 context:nil];
[_player addObserver:self forKeyPath:#"rate" options:0 context:nil];
Handler:
-(void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([keyPath isEqualToString:#"status"]) {
if (_player.status == AVPlayerStatusFailed) {
//Possibly show error message or attempt replay from tart
//Description from the docs:
// Indicates that the player can no longer play AVPlayerItem instances because of an error. The error is described by
// the value of the player's error property.
}
}else if ([keyPath isEqualToString:#"rate"]) {
if (_player.rate == 0 && //if player rate dropped to 0
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //if video was started
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //but not yet finished
_isPlaying) { //instance variable to handle overall state (changed to YES when user triggers playback)
[self handleStalled];
}
}
}
Magic:
-(void)handleStalled {
NSLog(#"Handle stalled. Available: %lf", [self availableDuration]);
if (_player.currentItem.playbackLikelyToKeepUp || //
[self availableDuration] - CMTimeGetSeconds(_player.currentItem.currentTime) > 10.0) {
[_player play];
} else {
[self performSelector:#selector(handleStalled) withObject:nil afterDelay:0.5]; //try again
}
}
The "[self availableDuration]" is optional, but you can manually launch playback based on amount of video available. You can change how often the code checks whether enough video is buffered. If you decide to use the optional part, here's the method implementation:
- (NSTimeInterval) availableDuration
{
NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
Don't forget the cleanup. Remove observers:
[_player.currentItem removeObserver:self forKeyPath:#"status"];
[_player removeObserver:self forKeyPath:#"rate"];
And possible pending calls to handle stalled video:
[UIView cancelPreviousPerformRequestsWithTarget:self selector:#selector(handleStalled) object:nil];
I had a similar issue.
I had some local files i wanted to play, configured the AVPlayer and called [player play], the player stops at frame 0 and wouldn't play anymore until i called play again manually.
The accepted answer was impossible for me to implement due to faulty explanation, then i just tried delaying the play and magically worked
[self performSelector:#selector(startVideo) withObject:nil afterDelay:0.2];
-(void)startVideo{
[self.videoPlayer play];
}
For web videos i also had the problem, i solve it using wallace's answer.
When creating the AVPlayer add an observer:
[self.videoItem addObserver:self forKeyPath:#"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:#"playbackLikelyToKeepUp"]) {
if (self.videoPlayer.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, !=, self.videoPlayer.currentItem.duration)) {
NSLog(#"hanged");
[self performSelector:#selector(startVideo) withObject:nil afterDelay:0.2];
}
}
}
Remember to remove observer before dismissing the view
[self.videoItem removeObserver:self forKeyPath:#"playbackLikelyToKeepUp"]
I think use AVPlayerItemPlaybackStalledNotification to detect the stalled is a better way.
First I observe for playback stalling
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerStalled),
name: AVPlayerItemPlaybackStalledNotification, object: videoPlayer.currentItem)
Then I force playback continuation
func playerStalled(note: NSNotification) {
let playerItem = note.object as! AVPlayerItem
if let player = playerItem.valueForKey("player") as? AVPlayer{
player.play()
}
}
This is probably not the best way of doing it, but I'm using it until I find something better :)
I also ran into this issue as described here
I tested this answer below multiple times and it worked every time so far.
Here is what I came up with for the Swift 5 version of #wallace's answer.
1- instead of observing the keyPath "playbackLikelyToKeepUp" I use the .AVPlayerItemPlaybackStalled Notification and inside there I check to see if the buffer is full or not via if !playerItem.isPlaybackLikelyToKeepUp {...}
2- instead of using his PlayerHangingNotification I use a function named playerIsHanging()
3- instead of using his PlayerContinueNotification I use a function named checkPlayerTryCount()
4- and inside checkPlayerTryCount() I do everything the same as his (void)playerContinue function except when I ran into } else if playerTryCount == 0 { nothing would happen. To avoid that I added 2 lines of code above the return statement
5- like #PranoyC suggested under #wallace's comments I set the playerTryCount to a max of 20 instead of 10. I also set it as a class property let playerTryCountMaxLimit = 20
You have to add/remove your activity indicator/spinner where the comment suggests to do so
code:
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled(_:)),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: playerItem)
#objc func playerItemPlaybackStalled(_ notification: Notification) {
// The system may post this notification on a thread other than the one used to registered the observer: https://developer.apple.com/documentation/foundation/nsnotification/name/1387661-avplayeritemplaybackstalled
guard let playerItem = notification.object as? AVPlayerItem else { return }
// playerItem.isPlaybackLikelyToKeepUp == false && if the player's current time is greater than zero && the player's current time is not equal to the player's duration
if (!playerItem.isPlaybackLikelyToKeepUp) && (CMTimeCompare(playerItem.currentTime(), .zero) == 1) && (CMTimeCompare(playerItem.currentTime(), playerItem.duration) != 0) {
DispatchQueue.main.async { [weak self] in
self?.playerIsHanging()
}
}
}
var playerTryCount = -1 // this should get set to 0 when the AVPlayer starts playing
let playerTryCountMaxLimit = 20
func playerIsHanging() {
if playerTryCount <= playerTryCountMaxLimit {
playerTryCount += 1
// show spinner
checkPlayerTryCount()
} else {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("1.-----> PROBLEM")
}
}
func checkPlayerTryCount() {
guard let player = player, let playerItem = player.currentItem else { return }
// if the player's current time is equal to the player's duration
if CMTimeCompare(playerItem.currentTime(), playerItem.duration) == 0 {
// show spinner or better yet remove spinner and show a replayButton or auto rewind to the beginning ***BE SURE TO RESET playerTryCount = 0 ***
} else if playerTryCount > playerTryCountMaxLimit {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("2.-----> PROBLEM")
} else if playerTryCount == 0 {
// *** in his answer he has nothing but a return statement here but when it would hit this condition nothing would happen. I had to add these 2 lines of code for it to continue ***
playerTryCount += 1
retryCheckPlayerTryCountAgain()
return // protects against a race condition
} else if playerItem.isPlaybackLikelyToKeepUp {
// remove spinner and reset playerTryCount to zero
playerTryCount = 0
player?.play()
} else { // still hanging, not at end
playerTryCount += 1
/*
create a 0.5-second delay using .asyncAfter to see if buffering catches up
then call retryCheckPlayerTryCountAgain() in a manner that attempts to avoid any recursion or threading nightmares
*/
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DispatchQueue.main.async { [weak self] in
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if self!.playerTryCount > 0 {
if self!.playerTryCount <= self!.playerTryCountMaxLimit {
self!.retryCheckPlayerTryCountAgain()
} else {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("3.-----> PROBLEM")
}
}
}
}
}
}
func retryCheckPlayerTryCountAgain() {
checkPlayerTryCount()
}
in very bad network playbackLikelyToKeepUp most probably to be false.
use kvo to observe playbackBufferEmpty is better, more sensitive to if existing buffer data that can be used for playback .if the value change to true you can call play method to continue playback.
In my case,
I was trying to record video with imagePickerController and playback recorded video with AVPlayerController. But it starts playing video and gets stops after 1 second. Somehow it gets time to save video and if you playback it immediately, it won't play.
So solution is ,
call play video after 0.5 seconds (delay). like below
-(void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self performSelector:#selector(playVideo) withObject:self
afterDelay:0.5];
}
-(void) playVideo {
self.avPlayerViewController = [[AVPlayerViewController alloc] init];
if(self.avPlayerViewController != nil)
{
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:Vpath];
AVPlayer* player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
self.avPlayerViewController.player = player;
self.avPlayerViewController.showsPlaybackControls = NO;
[self.avPlayerViewController setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.avPlayerViewController.view setFrame:[[UIScreen mainScreen] bounds]];
self.avPlayerViewController.view.clipsToBounds = YES;
self.avPlayerViewController.delegate = self;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(playerDidFinishPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
[self.viewVideoHolder addSubview:self.avPlayerViewController.view];
[self.avPlayerViewController.player play];
}
}
-(void) playerDidFinishPlaying
{
[avPlayer pause];
}
swift ios