AVPlayer无法在后台加载媒体

当在后台运行时,我的AVPlayer实现无法播放下载的音频(例如播客),但能够播放本地存储的歌曲。 仅在手机与计算机断开连接时才能在后台播放。 如果我的手机直接连接到我的计算机/调试器,则任何本地或下载的媒体都可以正常播放。 在前台,播放任何媒体类型也没有问题。

这是我的实施:

AVPlayer *moviePlayer; AVPlayerItem *playerItem; NSURL *address = /* the url of either a local song or remote media */ if (moviePlayer != nil) { NSLog(@"removing rate, playbackBufferEmpty, playbackLikelyToKeepUp observers before creating new player"); [moviePlayer removeObserver:self forKeyPath:@"rate"]; [playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"]; [playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]; [playerItem removeObserver:self forKeyPath:@"status"]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemPlaybackStalledNotification object:playerItem]; [self setMoviePlayer:nil]; // release and nilify } // The following block of code was an experiment to see if starting a background task would resolve the problem. As implemented, this did not help. if([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { NSLog(@"Experiment. Starting background task to keep iOS awake"); task = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) { }]; } playerItem = [[AVPlayerItem alloc]initWithURL:address]; moviePlayer = [[AVPlayer alloc]initWithPlayerItem:playerItem]; // Add a notification to make sure that the player starts playing. This is handled by observeValueForKeyPath [moviePlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:nil]; [playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil]; [playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil]; [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; // The following 2 notifications handle the end of play [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayBackDidFinish:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayBackDidFinish:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayBackStalled:) name:AVPlayerItemPlaybackStalledNotification object:playerItem]; // Indicate the action the player should take when it finishes playing. moviePlayer.actionAtItemEnd = AVPlayerActionAtItemEndPause; moviePlayer.automaticallyWaitsToMinimizeStalling = NO; 

更新:在上面的实现中,我还展示了启动后台任务的实验性尝试,希望能够让AVPlayer在后台播放播客。 这也没有帮助,但我将其包括在内以供参考。 未显示,但我也在AVPlayerItem playbackLikelyToKeepUp状态更改为TRUE后结束后台任务。

然后我有以下代码来处理keyPath通知:

 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"rate"]) { NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: rate"]; DLog(@"%@", debugString); debugString = [self appendAvPlayerStatus: debugString]; [[FileHandler sharedInstance] logDebugString:debugString]; } else if (object == playerItem && [keyPath isEqualToString:@"playbackBufferEmpty"]) { NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: playbackBufferEmpty"]; DLog(@"%@", debugString); debugString = [self appendAvPlayerStatus: debugString]; [[FileHandler sharedInstance] logDebugString:debugString]; } else if (object == playerItem && [keyPath isEqualToString:@"playbackLikelyToKeepUp"]) { NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: playbackLikelyToKeepUp"]; DLog(@"%@", debugString); debugString = [self appendAvPlayerStatus: debugString]; [[FileHandler sharedInstance] logDebugString:debugString]; } else if (object == playerItem && [keyPath isEqualToString:@"status"]) { NSString *debugString = [NSString stringWithFormat: @"In observeValueForKeyPath: status"]; DLog(@"%@", debugString); debugString = [self appendAvPlayerStatus: debugString]; [[FileHandler sharedInstance] logDebugString:debugString]; } } 

我有以下处理notificationCenter通知:

 - (void) moviePlayBackDidFinish:(NSNotification*)notification { NSLog(@"moviePlaybackDidFinish. Time to stopMoviePlayerWithMusicPlayIndication"); [self stopMoviePlayer: YES]; // stop the movie player } - (void) moviePlayBackStalled:(NSNotification*)notification { NSString *debugString = [NSString stringWithFormat: @"In moviePlayBackStalled. Restarting player"]; DLog(@"%@", debugString); debugString = [self appendAvPlayerStatus: debugString]; [[FileHandler sharedInstance] logDebugString:debugString]; [moviePlayer play]; } 

通过实现一个日志记录工具来跟踪从计算机断开连接时的执行情况,这是我观察到的:

当在与计算机断开连接的后台运行时,playerItem将加载url地址,并使用playerItem初始化AVPlayer。 这会导致通知observeValueForKeyPath:rate发布,但这是收到的最后一个通知。 音频无法播放。 玩家只是挂起。 显示各种moviePlayer和playerItem标志的日志输出如下:

 ready to play movie/podcast/song dedication/Song from url: http://feedproxy.google.com/~r/1019TheMix-EricAndKathy/~5/ZZnF09tuxr0/20170309-1_1718764.mp3 In observeValueForKeyPath: rate - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 1, playbackBufferFull: 0, rate: 1.0 

但是,当直接连接到计算机或在前台运行时在后台运行时,您可以从下面的日志输出中看到,在加载URL地址并且使用playerItem初始化AVPlayer之后,一系列通知是发布了keyPath:rate,playbackBufferEmpty,playbackLikelyToKeepUp和status。 然后音频开始播放。 显示各种moviePlayer和playerItem标志的输出如下:

 ready to play movie/podcast/song dedication/Song from url: http://feedproxy.google.com/~r/1019TheMix-EricAndKathy/~5/d3w52TBzd88/20170306-1-16_1717980.mp3 In observeValueForKeyPath: rate - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 1, playbackBufferFull: 0, rate: 1.0 In observeValueForKeyPath: playbackBufferEmpty - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0 In observeValueForKeyPath: playbackLikelyToKeepUp - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusUnknown, playbackToKeepUp: 0, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0 In observeValueForKeyPath: status - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusReadyToPlay, playbackToKeepUp: 0, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0 In observeValueForKeyPath: playbackLikelyToKeepUp - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusReadyToPlay, playbackToKeepUp: 1, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0 In observeValueForKeyPath: rate - timeCntrlStatus: AVPlayerTimeControlStatusPlaying, itemStatus: AVPlayerItemStatusReadyToPlay, playbackToKeepUp: 1, playbackBufferEmpty: 0, playbackBufferFull: 0, rate: 1.0 

总而言之,您可以看到,如果直接连接到计算机/调试器,在前台或后台运行时,AVPlayer会成功加载播放缓冲区并播放音频。 但是当在后台运行而没有连接到计算机/调试器时,播放器似乎没有加载媒体而只是挂起。

在所有情况下,都不会收到AVPlayerItemPlaybackStalledNotification。

很抱歉很长的解释。 任何人都可以看到什么可能导致播放器在未连接到计算机时挂在后台?

在开始游戏之前尝试调用beginBackgroundTask 。 文档说它不应该用于在后台运行,但它也说它对未完成的业务很有用,我相信这可以描述你的情况。

我的理由是这样的:

  1. 为了让您的应用程序在audio背景模式下在后台运行,您必须在iOS通常取消预定时播放音频
  2. 在播放远程媒体时,当您告诉AVPlayer play和音频实际开始播放时,自然需要比本地媒体更多的时间,允许您的应用继续播放。

因此,后台任务是为您的应用程序提供更多时间( 有些人说多达3分钟 )来加载远程媒体并播放音频。

你应该做这个伎俩吗? 可能不是 – 这是一个AVPlayer / iOS调度程序实现细节 – 对于iOS调度程序在后台音频情况下“公平”,它真的应该通过让AVPlayer.play()标记来识别你的“尽力”尝试播放音频背景音频的开头。 如果由于某种原因播放失败,那么罚款,计划。 无论如何,如果您对此感到强烈,可以提交function请求。

ps不要忘记调用endBackgroundTask ! 在到期处理程序中,一旦音频开始播放,成为一个优秀的移动设备公民,确保您继续运行后台。 如果endBackgroundTask影响你在后台播放音频的能力,那么你太早称之为!