I wrote an application for the iPhone and iPad, which should be broadcast, for example. 4 videos over HTTP. Each video should be requested as the previous one starts, so I cannot ask them all at the beginning. The exception is that I can request a second video (let it name its contents) at the beginning. Letโs name other video ads.
I have a URL for each ad or video that returns some XML description containing the URL of the actual content. (This is actually JSON in the case of the main video, but it doesn't matter.)
When the XML returns, I find the content URL and create an AVPlayerItem for it. When the first ad returns, I do an AVQueuePlayer with it. If the main video has already returned by this time, I create an AVPlayerItem for it and insert it into the queue player after the first announcement.
Two formats are available for advertising and video, MP4 and 3GP. I choose 3GP if the device is not connected to Wi-Fi, or it is a low-end device (examples of iPhone 3G and iPad of the 2nd generation.)
Then I observe various things on the player and any items that I created. First, the current item changes:
-(void)playerCurrentItemChanged:(NSString*)aPath ofPlayer:(AVQueuePlayer*)aQueuePlayer change:(NSDictionary*)aChange { if ([aPath isEqualToString:kCurrentItemKey]) { if (!_quitting) { [self performSelectorOnMainThread:@selector(queuePlayerCurrentItemChanged) withObject:nil waitUntilDone:NO]; } } }
Note. I am calling another method in the main thread because the docs say that the non-atomic properties of AVPlayer should be used this way.
This method is as follows:
-(void)queuePlayerCurrentItemChanged { NSAssert([NSThread isMainThread], @"FIX ME! VideoController method called using a thread other than main!"); AVPlayerItem* playerItem = _queuePlayer.currentItem; if (playerItem) { VideoItem* videoItem = [self findVideoItemFromPlayerItem:playerItem]; [self getReadyToPlay:videoItem];
VideoItem is my own class that wraps AVPlayerItem and adds extra data that I need to track.
In principle, getReadyToPlay customizes the user interface, depending on whether it is an ad (not allowed by FF) or the main video. The code also stops FFing if we go to the declaration.
I also monitor the status of any object that was created. The following method is also called in the main thread if the status has become reproducible for any element:
-(void)queuePlayerItemStatusPlayable:(AVPlayerItem*)aPlayerItem { NSAssert([NSThread isMainThread], @"FIX ME! VideoController method called using a thread other than main!"); VideoItem* videoItem = [self findVideoItemFromPlayerItem:aPlayerItem]; NSLog(@"queuePlayerItemStatusPlayable for item %@",videoItem); if (_queuePlayer.currentItem != aPlayerItem) { NSLog(@" but playable status is for non current item %@, ignoring",videoItem); return; } if (_videoItemIndex == 0) { NSLog(@" and playable status is for item 0 - call getReadyToPlay"); [self getReadyToPlay:videoItem];
}
If this is the first ad, I have to call getReadyToPlay, since there is not a single notification about the current item for the player. I used to call playIfReady only for this element, but now I call it for any current element, trying to avoid stopping.
Likewise, this code is called if the item receives the status AVPlayerItemStatusFailed:
-(void)queuePlayerItemStatusFailed:(AVPlayerItem*)aPlayerItem { NSAssert([NSThread isMainThread], @"FIX ME! VideoController method called using a thread other than main!"); VideoItem* videoItem = [self findVideoItemFromPlayerItem:aPlayerItem]; if (videoItem.isAd) {
}
When another ad appears, I will either add it to the end of the player or replace the current item. I only do the last if the current element is zero, which means that the player has finished the current fragment of the video and stopped, expecting more:
-(void) addNewItemToVideoPlayer:(AVPlayerItem*)aPlayerItem { NSAssert([NSThread isMainThread], @"FIX ME! VideoController method called using a thread other than main!"); if (_queuePlayer.currentItem == nil) { NSLog(@" CCV replaced nil current item, player %@",_queuePlayer); [_queuePlayer replaceCurrentItemWithPlayerItem:aPlayerItem]; if (!_isPaused) [_queuePlayer play]; } else if ([_queuePlayer canInsertItem:aPlayerItem afterItem:((VideoItem*)[_videoItems objectAtIndex:_indexLastCued]).avPlayerItem]) { NSLog(@" CCV inserted item after valid current item, player %@",_queuePlayer); [_queuePlayer insertItem:aPlayerItem afterItem:((VideoItem*)[_videoItems objectAtIndex:_indexLastCued]).avPlayerItem]; } }
This code seems to work very well on simulators / Wi-Fi and possibly on high-performance devices.
iPhone 3G on a slow 3G network shows a lot of not very repeatable defects.
I am watching playback that is probably not far behind, and call it in the main thread:
-(void)queuePlayerLikelyToKeepUp:(AVPlayerItem*)aPlayerItem { NSAssert([NSThread isMainThread], @"FIX ME! VideoController method called using a thread other than main!"); if (aPlayerItem.playbackLikelyToKeepUp == NO) { if (!_isPaused) { [_queuePlayer pause]; [self showLowBandwidthUI]; [self performSelector:@selector(restartVideo) withObject:nil afterDelay:1.0];
This basically pauses the video and displays a user interface (which shows a progress bar showing how many videos have been downloaded to give the user feedback on why the video stopped). Then he tries to restart the video after a second.
The worst mistakes that I see on the iPhone 3G are due to a complete breakdown. Sometimes I get AVPlayerItemStatusFailed for the main video (the error message is useless - just โunknown errorโ - AVFoundationErrorDomain error -11800), so the user exits the locked video and tries again, but then it seems that as soon as the player is in this state, he doesnโt will play videos, even those that he played before. And yet this is a brand new AVQueuePlayer - I exit my VideoController after each set of videos is done or when the user gets tired of them.
Videos can also fall into ad-free mode. The main video is played, but then the ads are not showing, and there are no impressions for all subsequent videos.
I know that this is a difficult, if not impossible, question that you answer, but I was asked to ask, so I go ahead.
Do you see something that I am doing wrong?
More generally, can you give a short tutorial on how to use AVQueuePlayer in a situation where all video URLs are unknown when creating AVQueuePlayer? What to do and not do ABQueuePlayer and AVPlayer?
Can you give advice on handling low bandwidth situations with AVQueuePlayer? What makes it linger on a restart?
Do you have any thoughts on what should be done in the main thread and what should not?