2
votes

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?

1
Have you tried calling 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 Fistman
@Rhythmic Fistman Yes, I tried that. It didn't help. And besides, the AVPlayer has no problem playing local media in the background, so it doesn't seem like its an issue of iOS putting me to sleep.JeffB6688
Local files have different timing properties. To avoid being descheduled, a background audio app must be playing audio. The fact that your AVPlayer needs time to stream audio data before it can start playing could be the problem.Rhythmic Fistman
@RhythmicFistman Ok, that makes sense. Can you tell me if my method for beginning a background task is what you have in mind. Here is what I have tried: Right before I load the AVPlayerItem with the address to the podcast and before the playerItem is associated with the AVPlayer, I included the following code: if([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { task = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) { }]; }JeffB6688
Can you add the code to the question? It's hard to read in a comment.Rhythmic Fistman

1 Answers

1
votes

Try calling beginBackgroundTask before 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, which I believe describes your situation.

My reasoning is this:

  1. for your app to run in the background under the audio background mode you must be playing audio when iOS would normally deschedule you
  2. when playing remote media, it naturally requires more time than local media from between when you tell the AVPlayer to play and when audio actually starts playing, allowing your app to continue.

Hence the background task to give your app a bit more time (some say as much as 3 minutes) to load the remote media and play the audio.

Should you have to do this trick? Probably not - this is an AVPlayer/iOS scheduler implementation detail - for the iOS scheduler to be "fair" in the background audio case, it really ought to recognise your "best effort" attempt to play audio by letting AVPlayer.play() mark the beginning of background audio. If the playback fails for whatever reason, then fine, deschedule. Anyhow, if you feel strongly about it you can file a feature request.

p.s. don't forget to call endBackgroundTask! Both in the expiration handler and, to be a good mobile device citizen, as soon as audio has started playing, assuring your continuing background running. If endBackgroundTask is affecting your ability to play audio in the background, then you're calling it too early!