When running in the background, my implementation of the AVPlayer is unable to play audio (e.g. podcast) that is downloaded, but is able to play songs that are stored locally. Failure to play in the background is only when the phone is disconnected from my computer. If my phone is direct connected to my computer/debugger, any media that is local or is downloaded plays without a problem. In the foreground, there is also no problem playing either media type.
Here is my implementation:
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;
UPDATE: In the above implementation, I am also showing an experimental attempt to start a background task in hopes of enabling the AVPlayer to play a podcast in the background. This did not help either, but I include it for reference. Not shown, but I also end the background task after the AVPlayerItem playbackLikelyToKeepUp status changes to TRUE.
Then I have the following code to handle the keyPath notifications:
-(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];
}
}
And I have the following to handle notificationCenter notifications:
- (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];
}
By implementing a logging tool to trace execution when disconnected from the computer, here is what I am observing:
When running in the background disconnected from computer, the playerItem is loaded with the url address and the AVPlayer is initialized with the playerItem. This causes the notification observeValueForKeyPath: rate to be posted, but that is the last notification received. Audio does not play. The player just hangs. The output from my log showing various moviePlayer and playerItem flags is as follows:
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
However, when running in the background when directly connected to the computer or when running in the foreground, you can see from the log output below that after the url address is loaded and and the AVPlayer is initialized with the playerItem, a series of notifications are posted for keyPath: rate, playbackBufferEmpty, playbackLikelyToKeepUp, and status. Audio then starts playing. The output showing various moviePlayer and playerItem flags is as follows:
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
So in summary, you see above that when running in foreground or in background if directly connected to the computer/debugger, the AVPlayer successfully loads the playback buffer and plays the audio. But when running in the background and NOT connected to the computer/debugger, the player does not appear to load the media and just hangs.
In all of the cases, the AVPlayerItemPlaybackStalledNotification is never received.
Sorry for the long explanation. Can anyone see what might be causing the player to hang in the background when not connected to the computer?
beginBackgroundTask
as you start playing? The documentation says it shouldn't be used to run in the background, but it also says it's useful for unfinished business: developer.apple.com/reference/uikit/uiapplication/… – Rhythmic FistmanAVPlayer
needs time to stream audio data before it can start playing could be the problem. – Rhythmic Fistman