2

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?

JeffB6688
  • 3,782
  • 5
  • 38
  • 58
  • 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: https://developer.apple.com/reference/uikit/uiapplication/1623031-beginbackgroundtask – Rhythmic Fistman Mar 14 '17 at 00:49
  • @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 Mar 14 '17 at 02:04
  • 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 Mar 14 '17 at 03:14
  • @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 Mar 14 '17 at 16:32
  • Can you add the code to the question? It's hard to read in a comment. – Rhythmic Fistman Mar 14 '17 at 20:53
  • @RhythmicFistman Ok, I have updated the code in the problem statement to show my attempt to keep the AVPlayer running in the background by starting a background task just before the AVPlayItem is loaded with the address of the podcast. Also as shown in the problem statement, I indicate that I end that background task if the AVPlayerItem is estimated to have enough buffer to keep up. But as implemented, it did not help. If this is not the proper way to accomplish this, please let me know. – JeffB6688 Mar 15 '17 at 14:42
  • Apart from the fact that you should call `endBackgroundTask` in the expiration handler, it looks fine. Can you create a small project that reproduces the problem and link it to this question? – Rhythmic Fistman Mar 15 '17 at 20:00
  • @RhythmicFistman I did a clean build. Then I was getting erratic behavior from the AVPlayer. Sometimes it would play in the background and other times it would fail. So I put the code back in to start a background task when I loaded the AVPlayerItem with a podcast. However, this time I changed the point where I end the background task. Now I end the background task when the AVPlayerItem playbackToKeepUp becomes true. This seems to be working now with this approach. Hopefully this approach is ok. If you put up an answer on the use of a background task, I will credit you with the bounty points. – JeffB6688 Mar 17 '17 at 20:32
  • Done, I expanded on what I wrote in these comments. Thanks! – Rhythmic Fistman Mar 17 '17 at 21:10

1 Answers1

1

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!

Community
  • 1
  • 1
Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159