iOS – Playing audio in the background

I recently completed an iOS music app called Fonzi.  Fonzi is a music jukebox player app allowing users to select songs for playback from a single device.  Apple provides extensive documentationon Audio session programming.  This post is an overview of whats required to get your iOS app to play audio in the background.

UPDATE: I’ve added some sample code to demonstrate how to play background audio in iOS. Check out the GitHub project here.

Playing audio content

I chose to skin my cat using AVQueuePlayer because it provided basic queuing mechanisms for audio queue management. If you correctly configure your app to play music in the background AND the app is currently playing audio content, it will continue to run in the background. By queueing up songs in AVQueuePlayer, there are always songs to play, and the app is never suspended. Never allow the queue to become empty.

AVQueuePlayer accepts instances of AVPlayerItems. Configure AVQueuePlayer to advance to the next item in the queue. Do this by setting its actionAtItemEnd property to AVPlayerActionAtItemEndAdvance. Register for the AVPlayerItemDidPlayToEndTimeNotification. You’ll need this to update any changes to your UI to indicate when a song has ended.

avQueuePlayer.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification object:[avQueuePlayer_ currentItem]];

- (void)playerItemDidReachEnd:(NSNotification *)notification {
//allow for state updates, UI changes
}

Playing background audio

At a very basic level there are three prerequisites your app must satisfy to play audio in the background:

  • Music must be playing (see above)
  • Set UIBackgroundModes correctly in app-info plist
  • For basic playback, set your audio session category to AVAudioSessionCategoryPlayback. If you don’t, you’ll get the default behavior.
Optionally, you can choose to configure your app to respond to changes in audio, audio session interruptions and remote control events.

Set UIBackgroundModes key in app-info.plist file for audio

AudioBackgroundModes

Initialize your audio session

A sample audio session initalization might look something like this.

-(void) initAudioSession {
// Registers this class as the delegate of the audio session to listen for audio interruptions
[[AVAudioSession sharedInstance] setDelegate: self];
//Set the audio category of this app to playback.
NSError *setCategoryError = nil; [[AVAudioSession sharedInstance]
  setCategory: AVAudioSessionCategoryPlayback error: &setCategoryError];
if (setCategoryError) {
//RESPOND APPROPRIATELY
}

// Registers the audio route change listener callback function
// An instance of the audio player/manager is passed to the listener
AudioSessionAddPropertyListener ( kAudioSessionProperty_AudioRouteChange,
audioRouteChangeListenerCallback, self );

//Activate the audio session
NSError *activationError = nil;
[[AVAudioSession sharedInstance] setActive: YES error: &activationError];
if (activationError) {
//RESPOND APPROPRIATELY
}
}

Set Audio Session Category 

By setting the Audio Session category to AVAudioSessionCategorPlayback, audio will continue to play when the silent switch is enabled, or when the screen is locked. There are other categories, and depending on the needs of your app, you might change them appropriately. Fonzi required playback only.

Listen for changes in audio output

When a user plugs in/plugs out headphones. docks/undocks the device, your app might need to respond appropriately. The interface and implementation samples are shown below.

//interface
void audioRouteChangeListenerCallback ( void     *inUserData,
AudioSessionPropertyID    inPropertyID,
UInt32                    inPropertyValueSize,
const void                *inPropertyValue );
//implementation
void audioRouteChangeListenerCallback (
void                      *inUserData,
AudioSessionPropertyID    inPropertyID,
UInt32                    inPropertyValueSize,
const void                *inPropertyValue ) {

// ensure that this callback was invoked for a route change
if (inPropertyID != kAudioSessionProperty_AudioRouteChange) return;

// Determines the reason for the route change,
// to ensure that it is not because of a category change.
CFDictionaryRef routeChangeDictionary = inPropertyValue;
CFNumberRef routeChangeReasonRef = CFDictionaryGetValue ( routeChangeDictionary,
CFSTR (kAudioSession_AudioRouteChangeKey_Reason) );

SInt32 routeChangeReason;
CFNumberGetValue (routeChangeReasonRef, kCFNumberSInt32Type, &routeChangeReason);

// "Old device unavailable"
// headset was unplugged, or device was removed from a dock connector
// that supports audio output. A test for when audio is paused

YOURPLAYERINSTANCE *playerInstance = (YOURPLAYERINSTANCE*) inUserData;

if (routeChangeReason == kAudioSessionRouteChangeReason_OldDeviceUnavailable) {
// player might respond appropriately - pause
} else if (routeChangeReason == kAudioSessionRouteChangeReason_NewDeviceAvailable){
//audio plugged back in, player might respond appropriately - play }
}

Handling Audio interruptions – implement AVAudioSessionDelegate

The most common cause of an audio interruption is a phone call, but could be a clock or calender alarm too. The app’s audio session is deactivated. By implementing the AVAudioSessionDelegate, we’re given the opportunity to adjust the app state. In Fonzi’s case – pause the AVQueuePlayer, and if the UI is visible, adjust as necessary. There’s nothing worse than music playing over a phone call – unless the music is more entertaining than the person you’re talking to.

Once the audio interruption is completed, another call to the delegate  is made, allowing the app to resume the audio session, and adjust the UI if necessary. Implement the beginInterruption and endInterruption methods on AVAudioSessionDelegate to do so.

Remote Control events

Remote control events are any event received for the purpose of controlling media. eg iTunes pause/play/next/previous buttons available from double-tapping the home screen button, or remote-control events from play/pause buttons on headphones. If you choose to do so, your app can respond to these events. Configure your UI to respond to remote control events by: registering for the events; making the controller become first responder. If the app is backgrounded, the view controller will respond appropriately. NOTE: Remember to de-register for the remote control events when the view is hidden through normal view lifecycle events.

By overriding the remoteControlReceivedWithEvent method you can choose which event to respond to. In the sample below, we respond to play/pause events only, but there are others too . Check out UIEventSubtype

-(void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
}

-(void)viewWillDisappear:(BOOL)animated {
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder]; [super viewWillDisappear:animated];
}

- (BOOL) canBecomeFirstResponder {
return YES;
}

- (void) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
if (receivedEvent.type == UIEventTypeRemoteControl) {
switch (receivedEvent.subtype) {
case UIEventSubtypeRemoteControlTogglePlayPause: {
//RESPOND TO PLAY/PAUSE EVENT HERE
break;
}
default:
break;
}
}
}

Online resources

- Apple documentation. Audio session programming. Also, Long-Running background tasks
- Matt Gallagher’s audio blog posts.

Tags: , ,

2 Responses to “iOS – Playing audio in the background”

  1. jeanApril 6, 2012 at 4:06 am #

    hi !
    on my Xcode, this line fails : YOURPLAYERINSTANCE *playerInstance = (YOURPLAYERINSTANCE*) inUserData;

    I think it’s because I must replace YOURPLAYERINSTANCE by …something, but what ?
    I’m a begginer so everything is a problem for me ;-)

  2. JonathanMay 1, 2012 at 5:45 pm #

    Hi Jean,

    YOURPLAYERINSTANCE is the instance of the class you used to register the route changes with:

    AudioSessionAddPropertyListener ( kAudioSessionProperty_AudioRouteChange,
    audioRouteChangeListenerCallback, self );

    YOURPLAYERINSTANCE is the ‘self’ class instance. Basically you’re casting the generic inUserData to your type-specific implementation eg MyAudioPlayer. MyAudioPlayer might be the class with all your Audio Playback logic. The reason I do this is to allow my audio player to respond in an appropriate fashion to route changes eg pause/play etc.

    Hope this helps!
    Jonathan

Leave a Reply