在后台运行时,我的 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 在后台播放播客。这也没有帮助,但我将其包含在内以供参考。未显示,但我也在 AVPlayerItemplaybackLikelyToKeepUp 状态更改为 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];
}
}
我有以下方法来处理通知中心通知:
- (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地址,并且AVPlayer会使用playerItem进行初始化。这会导致通知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。
抱歉这么长的解释。任何人都可以看到什么可能导致播放器在未连接到计算机时挂在后台?