Does anyone know if AVQueuePlayer
starts buffering the next AVPlayerItem
when the current item is about to finish playing?
I know there's nothing in the docs to suggest this, I'm asking mostly if anyone has observed this kind of behavior or not.
Ok, I've looked over this problem again and written some code to check out AVQueuePlayer
.
jollyCocoa's answer pointed me in the right direction by suggesting to observe the status property on AVPlayerItem
. However the documentation doesn't seem to point out that this property (and it's AVPlayerItemStatusReadyToPlay
value in particular) might be related to buffering.
However the AVPlayerItem's
loadedTimeRanges
property seems more related to buffering.
Doing KVO on that array was a bit trickier - the array object itself doesn't change, only it's items do - so I resorted to printing out it's content every second.
What I found out is that a few seconds in the queue's first item, the loadedTimeRanges
for the second item shows up a new CMTimeRange
with start time 0 and some small duration. The duration can increase up to 60 or so seconds while the previous item keeps playing.
Short answer: AVQueuePlayer
will buffer the next AVPlayerItem
while playing the current one.
Found a work around!!! After many hours of searching, and failed tests, I found a solution that works on iOS 5 and above.
This will play files without any delay in between. Not as convenient if you want to switch between files, but will certainly put everything together for you. It's still possible to keep track of where each file starts, when adding AVURLAsset, keep the CMTime variable, something like:
NSValue *toJumpTo = [NSValue valueWithCMTime:composition.duration];
[array addObject:toJumpTo];
and then just go through the array, checking the current time value and comparing it using CMTimeCompare.
Edit: Was found on this page visible from the web archive (the link currently points to spam).
I've been experimenting with the AVQueuePlayer using movies provided by a server. In this particular test I set up a queuePlayer with AVPlayerItems initialized with URL's to two different video assets of different types. One is an .mp4-file (progressive download), the other one an .m3u8 http-streaming file. It does act a bit funky, I must say.
Depending on the order in which I queue the files I get quite different behavior. If I start with the .mp4-file, the AVPlayerLayer goes blank and silent when the nextItem starts player (the .m3u8-file). If I change the order and start with the m3u8-file, the AVPlayerLayer goes blank but the audio from the second file plays fine.
My impression, from what I've seen so far, is that the AVQueuePlayer does NOT start downloading the next AVPlayerItem before the current AVPlayerItem has finished playing. But I might be wrong.
To be continued I guess...
Edit: Ok, so I've done some more testing and it seems the next items starts downloading before the last item finishes playing. See post below. Stroke the "NOT"...
Edit2: Adding my explanation here instead, since the comment left me with no formatting options. (Best practice for this is welcome, if this is a bad way to handle answer updates)
Anyhow, I iterated through my itemsArray adding observation of the status property using KVO...
[playerItem addObserver: self forKeyPath:@"status" options: 0 context: NULL];
I also set my controller as the observer of the AVPlayerItem Notification AVPlayerItemDidPlayToEndTimeNotification:
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(playerItemDidReachEnd:)
name: AVPlayerItemDidPlayToEndTimeNotification
object: nil];
Now I could log the status change of the AVPlayerItem, and it turns out the next AVPlayerItem in the queue is logged with a status change AVPlayerItemStatusReadyToPlay before the current item ends playing back.
My conclusion is therefor that the next item in line starts downloading prior to the current item ends.
However! I create an array with AVPlayerItems which I use to create my player. Reading the AVFoundation docs on AVAssets, I get the impression that these download their assets asynchronously, but I'm still uncertain when these downloads are being initiated by the IAPlayerItem. I still have some funky behavior when I add the .m3u8-file to the queue, which makes me wonder if these assets begin downloading at the time of creation (seems a bit strange to me though).
I made simple demo for you and others that want to reduce buffering time of each video from URL.
NOTE: Don't know this is right way or not but It's working perfect for me without any issue. If anyone know more or want to improve code for better result then welcome to change my answer.
In the below code, First video take normal buffering time. Next video will be start automatically when current video is finish and you can swipe left and right to move next and previous video.
Follow the below steps.
1) In the videoPlayerVC.h file.
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
@interface videoPlayerVC : UIViewController
{
AVPlayerViewController *avPlyrViewController;
AVPlayerItem *currentAVPlyrItem;
int currentVideoNO; // For current video track.
NSMutableArray *arrOfAVPItems;
NSMutableArray *arrOfPlyer;
}
2) In the videoPlayerVC.m file
Here you can start to play video by click on button. Below is button's action method.
-(void)clickOnPlayVideosImage:(UIButton *)sender
{
// In my App. all video URLs is up to 15 second.
currentVideoNO = 0;
NSMutableArray *arrForVideoURL = [[NSMutableArray alloc]initWithObjects:
[NSURL URLWithString:@"http://videos/url1.mov"],
[NSURL URLWithString:@"http://videos/url2.mov"],
[NSURL URLWithString:@"http://videos/url3.mov"],
[NSURL URLWithString:@"http://videos/url3.mov"],
[NSURL URLWithString:@"http://videos/url4.mov"],
[NSURL URLWithString:@"http://videos/url5.mov"], nil];
AVPlayerItem *thePlayerItemA = [[AVPlayerItem alloc] initWithURL:[arrForVideoURL objectAtIndex:0]];
AVPlayerItem *thePlayerItemB = [[AVPlayerItem alloc] initWithURL:[arrForVideoURL objectAtIndex:1]];
AVPlayerItem *thePlayerItemC = [[AVPlayerItem alloc] initWithURL:[arrForVideoURL objectAtIndex:2]];
AVPlayerItem *thePlayerItemD = [[AVPlayerItem alloc] initWithURL:[arrForVideoURL objectAtIndex:3]];
AVPlayerItem *thePlayerItemE = [[AVPlayerItem alloc] initWithURL:[arrForVideoURL objectAtIndex:4]];
AVPlayerItem *thePlayerItemF = [[AVPlayerItem alloc] initWithURL:[arrForVideoURL objectAtIndex:5]];
if(arrOfAVPItems.count > 0)
[arrOfAVPItems removeAllObjects];
arrOfAVPItems = nil;
if(arrOfPlyer.count > 0)
[arrOfPlyer removeAllObjects];
arrOfPlyer = nil;
arrOfPlyer = [[NSMutableArray alloc] init];
arrOfAVPItems = [NSMutableArray arrayWithObjects:thePlayerItemA, thePlayerItemB, thePlayerItemC, thePlayerItemD, thePlayerItemE, thePlayerItemF, nil]; // Add All items in the Array
for(AVPlayerItem *myPlyrItem in arrOfAVPItems)
{
AVPlayer *videoPlayerNext = [AVPlayer playerWithPlayerItem:myPlyrItem]; /// Add item in the player
[videoPlayerNext play];
[videoPlayerNext pause];
[arrOfPlyer addObject:videoPlayerNext]; /// Make Array of "AVPlayer" just reduce buffering of each video.
}
avPlyrViewController = [AVPlayerViewController new];
avPlyrViewController.delegate = self;
avPlyrViewController.player = (AVPlayer *)arrOfPlyer[0]; // Add first player from the Array.
avPlyrViewController.showsPlaybackControls = YES;
avPlyrViewController.allowsPictureInPicturePlayback = YES;
avPlyrViewController.videoGravity = AVLayerVideoGravityResizeAspect;
[self presentViewController:avPlyrViewController animated:YES completion:^{
[self performSelector:@selector(playVideos) withObject:nil afterDelay:1];/// call method after one second for play video.
UISwipeGestureRecognizer * swipeleft=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeleftToNextVideo:)];
swipeleft.direction=UISwipeGestureRecognizerDirectionLeft;
[avPlyrViewController.view addGestureRecognizer:swipeleft]; // Add left swipe for move on next video
UISwipeGestureRecognizer * swiperight=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeleftToPerviousVideo:)];
swiperight.direction=UISwipeGestureRecognizerDirectionRight;
[avPlyrViewController.view addGestureRecognizer:swiperight]; // Add right swipe for move on previous video
}];
}
Now write playVideos
method code.
#pragma mark - AVPlayer Methods -
-(void)playVideos
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:currentAVPlyrItem]; // remove current "AVPlayerItem" from the notification observe.
if(arrOfAVPItems.count > 0)
{
currentAVPlyrItem = (AVPlayerItem *)arrOfAVPItems[currentVideoNO]; // Add "AVPlayerItem" from the array.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:currentAVPlyrItem]; // Add notification observer to indication current "AVPlayerItem" is finish.
// pause and nil previous player if available.
[avPlyrViewController.player pause];
avPlyrViewController.player = nil;
avPlyrViewController.player = (AVPlayer *)arrOfPlyer[currentVideoNO]; // add new player from the array
[avPlyrViewController.player.currentItem seekToTime:kCMTimeZero]; // set for start video on initial position.
[avPlyrViewController.player play]; // Play video
}
}
Notification observer to indicate video is finish.
- (void)playerItemDidReachEnd:(NSNotification *)notification
{
NSLog(@"IT REACHED THE END");
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:currentAVPlyrItem]; // remove current "AVPlayerItem" from the notification observe.
[self swipeleftToNextVideo:nil]; // Call method for next video.
}
Method for play next video
-(void)swipeleftToNextVideo:(UISwipeGestureRecognizer*)gestureRecognizer
{
currentVideoNO++;
if(currentVideoNO > (arrOfAVPItems.count -1))
currentVideoNO = 0;
NSLog(@"current - %d and last - %d", currentVideoNO, (int)(arrOfAVPItems.count -1));
[self playVideos];
}
Method for play previous video.
-(void)swipeleftToPerviousVideo:(UISwipeGestureRecognizer*)gestureRecognizer
{
currentVideoNO--;
if(currentVideoNO < 0)
currentVideoNO = (int)(arrOfAVPItems.count -1);
NSLog(@"current - %d and last - %d", currentVideoNO, (int)(arrOfAVPItems.count -1));
[self playVideos];
}
Follow these steps. If any doubt then comment please.
You can use three AVPlayers. First is previous, second is current and last is next. When current is playing, play last player as preloading and observe its status. When the preloading player's status becomes AVPlayerStatusReadyToPlay, stop it and call next lines: [player.playerItem cancelPendingSeeks]; [player.playerItem.asset cancelLoading];
These codes will prevent pre-loading all data of the video.