获取多个远程视频并使用AVQueuePlayer的应用程序设计

4
我编写了一个iPhone和iPad应用程序,通过HTTP流传输4个视频。每个视频必须在前一个视频开始时请求,因此我无法在一开始就请求所有视频。但是有一个例外,即我可以在开始时请求第二个视频(我们称其为内容)。让我们把其他视频称为广告。
对于每个广告或视频,我都有一个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提供建议?是什么导致它以无法重新启动的方式停止?
您对必须在主线程上执行的操作和不需要在主线程上执行的操作有什么想法?
3个回答

2
答案是所有的方法(不仅是属性)必须在主线程上调用AVQueuePlayer。此外,KVO观察必须在主线程上启动和停止。 如果您不这样做,即使是一个微小的情况,您的播放器最终也会进入拒绝播放的状态。更糟糕的是,支持AVQueuePlayer的mediaserverd进程也处于这种状态,因此即使开始播放新视频并重新创建AVQueuePlayer也无法正常工作。

1

已解决:

崩溃原因是在主线程上调用了此函数:

           currentDuration = CMTimeGetSeconds([[myAVQueuePlayer currentItem] duration]);

我的解决方案是:

    NSError* error = nil;
    if ( [[[myAVQueuePlayer currentItem] asset] statusOfValueForKey:@"duration" error:&error] == AVKeyValueStatusLoaded ) {
        currentDuration = CMTimeGetSeconds([[myAVQueuePlayer currentItem] duration]);            
    } else {
        [[[myAVQueuePlayer currentItem] asset] loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:@"duration"] completionHandler:^{
            currentDuration = CMTimeGetSeconds([[myAVQueuePlayer currentItem] duration]); 
        }];
    }

这是对你之前回答的回答吗? - oers

1

我遇到了同样的问题(由于mediaserverd崩溃,音频播放偶尔会在后台停止)... 我尝试在主线程中执行所有对AVQueuePlayer的调用/属性,但它并没有完全解决这个问题。

因此,我在developers.apple.com上开了一个TSI,并得到了答案:

1) 这个崩溃是什么原因引起的?是我们代码的问题还是已知的 bug,会在不久的将来修复?>> 媒体服务器崩溃确实会发生,当它们发生时,我们依靠开发者报告来跟踪问题并解决它们。AVFoundation 和队列播放器依赖于大量其他 AV 和核心音频代码来完成其工作,因此为了分析导致媒体服务器特定崩溃的原因,我们需要一个带有可重现测试用例的 bug 报告。我个人以前没有看到过这个崩溃日志,但其他用户已经报告了这个问题,似乎与 5.1 相关。我建议您尽快在 as 上提交一个 bug,记录您遇到的具体问题。您可以将 bug ID 发送给我,我可以将其升级到我们的 AVFoundation 和 Core Audio 工程团队。

2) 在这种情况下,除了现在正在做的事情之外,我还能做什么来防止 iOS 冻结应用程序?3) 如果 mediaserverd 崩溃并重新启动,我该如何收到通知?>>

我撰写了一篇关于处理媒体服务器崩溃的常规方法的问答。您可以监听 kAudioSessionProperty_ServerDied 属性。 https://developer.apple.com/library/ios/#qa/qa1749/_index.html 让我强调一下,这只是一个临时措施,任何崩溃都被视为我们方面的 bug,我们希望尽快修复它们。请尽快提交 bug。

谢谢。问候,Edward

Edward A. :: 音频和视频工程师开发者技术支持全球开发者关系 Apple Inc.*

希望这可以帮到你...或者至少让你不再为如何修复这个问题而烦恼 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接