1
votes

I am playing Apple Music using [MPMusicPlayerController applicationMusicPlayer]. (AVPlayer and AVAudioPlayer don't work for Apple Music.)

While in the background, I need to know when the song has ended so I can play the next song. Unfortunately, MPMusicPlayerControllerPlaybackStateDidChangeNotification isn't fired while the app is in the background.

So I created a background task, using beginBackgroundTaskWithExpirationHandler, which fires an NSTimer once a second to check for the end of song. I have the audio background mode set, but my task still only gets 3 minutes. It was my understanding that if I use the audio background mode, my task would get infinite time. Is that not true? If not, how do I handle this? Surely others have come across this situation.

2

2 Answers

3
votes

For future reference:

I found that MPMusicPlayerController never engages audio UIBackgroundMode, using either applicationMusicPlayer or systemMusicPlayer. So you can never get more than 3 minutes background time. However, AVAudioPlayer DOES enagage audio background mode. So my (ugly) solution is to loop a silent MP3 using AVAudioPlayer while in the background. I'm going to assume Apple won't protest, this seems like a legitimate use of silent audio. Note that for both AVAudioPlayer and MPMusicPlayerController to play at the same time, you must have AVAudioSessionCategoryOptionMixWithOthers set for the AVAudioSession.

0
votes

I have released almost same app in AppStore. It plays songs using [MPMusicPlayerController applicationMusicPlayer].

Yes, MPMusicPlayerControllerPlaybackStateDidChangeNotification did not came to background app. So, I used the NSTimer to determine when the song maybe ended. You set NSTimer to 1sec. I set NSTimer to the period of the song's play-time. i.e. when the song has 3 minutes 15 secs time, I set the NSTimer to 3 minutes 15 secs.

When user pause the music, I pause the NSTimer. And when user resume the music, I set the NSTimer to the rest time of the song.

It works very well. I hope this may help you. Sorry for my cheep English.


Step you may need;

  1. Set audio background mode in Capabilities Setting. (You done)
  2. Use beginBackgroundTaskWithExpirationHandler in App Delegate.
    - (void)applicationWillResignActive:(UIApplication *)application
    {
        _bgid = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void)
        {
            [[UIApplication sharedApplication] endBackgroundTask:_bgid];

            _bgid = UIBackgroundTaskInvalid;
        }];
    }

    - (void)applicationDidBecomeActive:(UIApplication *)application
    {
        if ( _bgid != UIBackgroundTaskInvalid )
        {
            [[UIApplication sharedApplication] endBackgroundTask:_bgid];
        }
    }


3.Use NSTimer in non-repeated mode.


    #import "NMCountdownTimer.h"

    @interface NMCountdownTimer ()
    {
    @private
        NSTimer *_timer;
        NSTimeInterval _rest;
        NMMethodCompletion _completion;
    }
    @end

    @implementation NMCountdownTimer

    - (void)start:(NSTimeInterval)secs completion:(NMMethodCompletion _Nullable)completion
    {
        _completion = completion;

        if ( secs > 0.0f )
        {
            NSLog(@"[  ] start  (secs:%f)", secs);
            _timer = [NSTimer scheduledTimerWithTimeInterval:secs target:self selector:@selector(fired) userInfo:nil repeats:FALSE];
            _rest = 0.0f;
        }
    }

    - (void)pause
    {
        if ( _timer && ( _rest == 0.0f ) )
        {
            _rest = [[_timer fireDate] timeIntervalSinceReferenceDate] - [[NSDate date] timeIntervalSinceReferenceDate];

            NSLog(@"[  ] pause  (rest:%f)", _rest);

            [_timer invalidate];
            _timer = nil;
        }
    }

    - (void)resume
    {
        if ( ( ! _timer ) && ( _rest > 0.0f ) )
        {
            NSLog(@"[  ] resume (rest:%f)", _rest);
            _timer = [NSTimer scheduledTimerWithTimeInterval:_rest target:self selector:@selector(fired) userInfo:nil repeats:FALSE];
            _rest = 0.0f;
        }
    }

    - (void)kill
    {
        [_timer invalidate];
        _timer = nil;
        _rest = 0.0f;
        _completion = nil;

        NSLog(@"[  ] kill");
    }

    - (void)fired
    {
        [_timer invalidate];
        _timer = nil;
        _rest = 0.0f;

        if ( _completion )
        {
            _completion ( nil );
        }

        NSLog(@"[  ] fired");
    }

    - (void)dealloc
    {
    #if ( Namera_SHOW_DEALLOC && FALSE )
        NSLog(@"[DEA] %@ NMCountdownTimer", self );
    #endif

        _timer = nil;
        _completion = nil;
    }

    @end
    
    somewhere in your code like this;

    NSTimeInterval playback_duration = [[song valueForProperty: MPMediaItemPropertyPlaybackDuration] doubleValue];

    ....

    MPMediaItemCollection *collection = [MPMediaItemCollection collectionWithItems:[NSArray arrayWithObject:song]];

    [_musicCtr setQueueWithItemCollection:collection];
    [_musicCtr play];
    [_countdown_timer start:playback_duration completion:^(id _Nullable object)
    {
        [weakSelf execute_next_song];
    }];