我写了一个iPhone和iPad应用程序,应该流动,例如4个视频通过HTTP . 必须在前一个视频开始时请求每个视频,因此我不能在开始时请求所有视频 . 例外情况是我可以在开始时请求第二个视频(让我们称之为内容) . 我们打电话给其他视频广告 .
我有一个每个广告或视频的网址,它返回一些包含实际内容网址的XML说明 . (实际上它是主视频中的JSON,但没关系 . )
当XML返回时,我找到内容URL并为其创建一个AVPlayerItem . 当第一个广告返回时,我用它制作AVQueuePlayer . 如果那时主视频已经返回,我为它制作一个AVPlayerItem,并在第一个广告后将其插入队列播放器 .
广告和视频有两种格式,MP4和3GP . 我选择3GP如果设备不在wifi上,或者是低端设备(iPhone 3G和iPad第二代就是例子 . )
然后我在玩家和我创造的任何物品上观察各种事物 . 首先是当前项目的变化:
-(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根据广告(不允许使用FF)还是主视频来设置UI . 如果我们转换为广告,代码也会停止FFing .
我还观察了已创建的任何项目的状态 . 如果状态已变为可播放任何项目,则在主线程上类似地调用下面的方法:
-(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];
}
}
}
这基本上暂停了视频并显示了UI(显示了一个进度条,显示了已下载的视频数量),以便向用户提供有关视频停止原因的反馈 . 然后它会在一秒钟之后尝试重新启动视频 .
我在iPhone 3G上看到的最糟糕的错误与完全失速有关 . 有时我得到主视频的AVPlayerItemStatusFailed(错误消息无益 - 只是“未知错误” - AVFoundationErrorDomain错误-11800)所以用户退出停滞的视频并再次尝试,但似乎一旦玩家处于此状态,它不会播放任何视频,即使它之前播放的视频 . 然而,这是一个全新的AVQueuePlayer - 我在每组视频完成后退出我的VideoController,或者当用户厌倦了它们时 .
视频也可以进入“无广告”模式 . 主视频播放,但之后没有广告显示,并且对于所有后续视频,没有广告显示 .
我知道这对你来说是一个很难的问题,但是我已被要求提出问题,所以我要继续前进 .
你能看到我做错的事吗?
更一般地说,您是否可以在这样的情况下提供有关如何使用AVQueuePlayer的简要教程,其中创建AVQueuePlayer时不知道所有视频URL? ABQueuePlayer和AVPlayer的作用和注意事项是什么?
您能否就AVQueuePlayer处理低带宽情况提出建议?是什么导致它以不可重启的方式停止?
你有什么想法在主线程上必须做什么,什么不是?
3 回答
答案是必须在主线程上调用
AVQueuePlayer
的所有方法(不仅仅是属性) . 此外,必须在主线程上启动和停止KVO观察 .如果你不这样做,即使只是一个小案例,你的玩家最终会进入一个拒绝玩的状态 . 更糟糕的是,支持
AVQueuePlayer
的mediaserverd进程也处于这种状态,因此即使开始播放新视频并重新创建AVQueuePlayer
也无效 .解决:
崩溃的原因是在主线程上调用它:
我的解决方案是:
我遇到了同样的问题(由于mediaserverd崩溃,音频播放偶尔在后台停止)...我试图在主线程中对AVQueuePlayer执行所有调用/属性但是它没有完全解决这个问题 .
所以我在developers.apple.com开了一个TSI,答案是:
希望这会有所帮助...或者至少阻止你如何解决这个问题:-)