对于每个广告或视频,我都有一个URL,该URL返回一些XML描述,其中包含实际内容的URL。(实际上,在主视频的情况下,它是JSON,但不要紧。)
当XML返回时,我查找内容URL并为其创建AVPlayerItem。当第一个广告返回时,我使用它创建AVQueuePlayer。如果主视频已经返回,则为其创建AVPlayerItem,并在第一个广告后将其插入队列播放器中。
广告和视频都有两种格式可用,MP4和3GP。如果设备未连接WiFi或是低端设备(例如iPhone 3G和iPad第2代),则我选择3GP。
然后,我观察播放器和我创建的任何项的各种事物。首先是当前项的更改:
-(void)playerCurrentItemChanged:(NSString*)aPath ofPlayer:(AVQueuePlayer*)aQueuePlayer change:(NSDictionary*)aChange {
if ([aPath isEqualToString:kCurrentItemKey]) {
if (!_quitting) {
[self performSelectorOnMainThread:@selector(queuePlayerCurrentItemChanged) withObject:nil waitUntilDone:NO];
}
}
}
请注意,我在主线程上调用了另一个方法,因为文档中指出 AVPlayer 的非原子属性应该以这种方式使用。这个方法看起来像这样:
-(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];
// don't continue to FF if it's an ad
if (videoItem.isAd && (_queuePlayer.rate > 1.0)) {
_queuePlayer.rate = 1.0;
}
}
else {
NSLog(@"queuePlayerCurrentItemChanged to nil!!!");
}
}
一个VideoItem是我自己创建的类,它包装了AVPlayerItem,并添加了我需要跟踪的其他数据。
基本上,getReadyToPlay根据是否是广告(不允许快进)或主视频来设置UI。如果我们正在转换到广告,则代码还会停止快进。
我还观察了已创建的任何项目的状态。如果任何项目的状态变为可播放,则接下来的方法也会在主线程上调用:
-(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]; // the first item doesn't get a current item notification so do this here
}
[self playIfReady]; //pausenotadvance (do this every time now)
如果是第一个广告,则必须调用getReadyToPlay,因为对于播放器来说,此项从未通知当前项目。以前我只为这个项目调用playIfReady,但现在我为任何当前项目调用它,以尝试避免停顿。
同样地,如果项目状态为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) {
// this seems to be the only notification that we've run out of video when reachability is off
if (appDelegate._networkStatus == NotReachable) {
NSLog(@"AVPlayerItemStatusFailed when not reachable, show low bandwidth UI");
if (!_isPaused && !_lowBandwidthUIShowing) {
[_queuePlayer pause];;
[self showLowBandwidthUI];
}
return;
}
NSError* error = aPlayerItem.error;
if (aPlayerItem == _queuePlayer.currentItem) {
NSLog(@"AVPlayerStatusFailed playing currently playing ad with error: %@",error.localizedDescription);
if (videoItem.isLast) {
NSLog(@"Failed to play last ad, quitting");
[self playEnded];
}
else {
NSLog(@"Not the last ad, advance");
[_queuePlayer advanceToNextItem];
}
}
else {
NSLog(@"Error - AVPlayerStatusFailed on non-playing ad with error: %@",error.localizedDescription);
[_queuePlayer removeItem:aPlayerItem];
}
}
else {
// This is can be an invalid URL in the main video JSON or really bad network
// Assuming invalid URLS are pretty rare by the time we're in the app store, blame the network
// Whatever, give up because it's the main video
NSError* error = aPlayerItem.error;
if (appDelegate._networkStatus == ReachableViaWiFi) {
if (!_alertShowing) {
NSLog(@"Error - AVPlayerStatusFailed playing main video, bad JSON? : error %@",error.localizedDescription);
[self showServerAlertAndExit];
}
}
else {
if (!_alertShowing) {
NSLog(@"Error - AVPlayerStatusFailed playing main video, bandwidth? : error %@",error.localizedDescription);
[self showNetworkAlertAndExit];
}
}
}
return;
当有更多的广告到来时,我会将其添加到播放器的末尾或替换当前的广告。只有在当前广告为空(即播放器已完成当前视频片段并等待更多内容)时,我才进行后者:
-(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];
}
}
这段代码在模拟器、WiFi以及可能高端设备上似乎运行得相当不错。
但是,在一个较慢的3G网络下使用iPhone 3G则会出现各种不太可重复的缺陷。
我注意到播放能够跟上,并在主线程上调用此函数:
-(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]; // delay and try again
}
}
else {
// if we forced the showing of UI due to low bandwidth, fade it out, remove spinner
if (_lowBandwidthUIShowing) {
[self hideLowBandwidthUI];
[_queuePlayer play];
}
}
}
这基本上会暂停视频并显示用户界面(其中显示进度条,显示已下载多少视频,为用户提供反馈以解释视频为何会停止)。然后它会在一秒钟后尝试重新启动视频。
我在iPhone 3G上看到的最严重的错误是完全停顿。有时主视频会出现AVPlayerItemStatusFailed错误(错误消息不太有用-仅为“未知错误”- AVFoundationErrorDomain error -11800),因此用户退出了停顿的视频并再次尝试,但是一旦播放器处于此状态,甚至连之前已播放过的视频也无法播放。而且这是完全不同的一个AVQueuePlayer-每组视频完成后,或当用户厌倦它们时,我都会退出我的VideoController。
视频还可以进入“无广告”模式。主视频播放,但然后没有广告显示,对于所有后续视频,都没有广告显示。
我知道这对您来说可能是一个难以回答的问题,但我被要求提出这个问题,所以我正在进行。
您是否看到我做错了什么?
更一般地说,在像这样的情况下如何使用AVQueuePlayer的简短教程中,所有视频URL在创建AVQueuePlayer时都不知道? ABQueuePlayer和AVPlayer的注意事项是什么?
您能否就处理低带宽情况的AVQueuePlayer提供建议?是什么导致它以无法重新启动的方式停止?
您对必须在主线程上执行的操作和不需要在主线程上执行的操作有什么想法?