Background Music Queue with AVPlayer and iOS4

Update 2010-Nov-11: Apple has come out with AVQueuePlayer in iOS 4.1, and presumably most people are going to move to the latest version now that it performs well even on the 3G, but the AVPlayer can still be used to manually create a queuing of items, but if you want all the hard work to be done for you, with only a few limitations, then use AVQueuePlayer instead.

Update 2010-Oct-19: Apple has been updating their documentation slightly, and it has more information now with regard to setting up an asset. This is more specifically for video, but it might help fill in the blanks for just audio. AVPlayer Playback Guide

The iOS 4.0 framework introduced AVPlayer which allows playing a MP3s from the iTunes Music Library in the background without using the iPod music player. This has a couple benefits over the MPMusicPlayerController in that you can add new items, stop, or play music in the background. Also, a custom icon can represent your app in the multitasking menu where the ipod controls are located. If you stick with [MPMusicPlayerController iPodMusicPlayer] you can play music in the background through the ipod player, but once an app goes into the background, it no longer has control over the playback of the music.

Unfortunately this is only available for devices running the iOS 4.0 firmware (not iPad’s current version 3.2 or iOS 3.1.3 and lower). Over time this will not be an issue as people continue to upgrade their devices. Also I haven’t tested this yet, but I believe it won’t let you play DRM-enabled files, but I hope I’m wrong on this latter issue (will update this post after I test more capability thoroughly).

Also, you’ll have to write code to handle any version of iOS < 4.0 – for example by using MPMusicPlayerController instead – however, if you want to start music playing from the iPod collection after your app has entered into the background I believe you need to use AVPlayer – if you just want to play sounds or other non-ipod mp3 files (or any supported media file) from your bundle or that the user transfered to your app using iTunes then you can use the simpler AVAudioPlayer instead of AVPlayer.

To start playing audio, I first copy the MPMediaItemCollection from my MPMusicPlayerController and store it in a property of my controller class.

- (void)avPlayerStart {
	self.audioPlayerMusicCurrentItemIndex = 0;
	self.audioPlayerMusicItems = [[[AppPrefs sharedInstance].appMediaItemCollection items] mutableCopy];
	[self avPlayerNextSong];
}

After I have setup the collection or queue of items to play, I then set a “lock” BOOL value isLoadingAsset to YES to prevent this method executing multiple times – as the notification when a song finishes seems to get called more than once. Next we create an AVAsset for the next song, while keeping track of where we are in the queue. Finally we setup a completion handler that is called after the asset is actually loaded by the system, and either allocate a new AVPlayer or replace the current player item if we have already created the player object. Finally we call play on the player to start playing the now current song.

- (void)avPlayerNextSong {

       // used to prevent duplicate notifications
	self.isLoadingAsset = YES;

        // create the url to the file
	NSURL *anUrl = [[self.audioPlayerMusicItems objectAtIndex: self.audioPlayerMusicCurrentItemIndex] valueForProperty:MPMediaItemPropertyAssetURL];

	// Prepare Index for Next Song (wrap to continue playing: loop)
	self.audioPlayerMusicCurrentItemIndex++;
	if(self.audioPlayerMusicCurrentItemIndex &gt;= [self.audioPlayerMusicItems count]) {
		self.audioPlayerMusicCurrentItemIndex = 0;
	}

	AVAsset *asset = [AVURLAsset URLAssetWithURL:anUrl options:nil];
	NSArray *keys = [NSArray arrayWithObject:@"tracks"];
	[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^(void) {
		NSError *error = nil;
                // get the status to see if the asset was loaded
		AVKeyValueStatus trackStatus = [asset statusOfValueForKey:@"tracks" error:&amp;error];
		switch (trackStatus) {
			case AVKeyValueStatusLoaded:
				// AVAsset was loaded
				// If you want to change your UI you could do so here
				// [self updateUserInterfaceForTracks];
				if(self.audioPlayerMusic) {
                                        // AVPlayer has already been setup, so replace player with this AVAsset
					[self.audioPlayerMusic replaceCurrentItemWithPlayerItem:[AVPlayerItem playerItemWithAsset:asset]];
				} else {
                                        // AVPlayer has not been setup so initialize with this AVAsset
                                        self.audioPlayerMusic = [AVPlayer playerWithPlayerItem:[AVPlayerItem playerItemWithAsset:asset]];
				}
				[self.audioPlayerMusic play];
				break;
			case AVKeyValueStatusFailed:
				// error occured loading AVAsset
				break;
			case AVKeyValueStatusCancelled:
				// loading of the AVAsset was cancelled
				break;
			default:
				break;
		}
		self.isLoadingAsset = NO;
	}];
}</pre>
In order to advance the "queue" to the next song after the current song has finished playing avPlayerNextSong is called, but only after checking to make sure that another asset isn't currently loading. This check prevents songs from being skipped. Also making sure to run this check on the main thread so that we have access to the same controller (ie: self).
<pre lang="objc">- (void)handleAVPlayerItemDidPlayToEndTimeNotification:(NSNotification *)notification {
	dispatch_async(dispatch_get_main_queue(), ^{
		if(!self.isLoadingAsset) {
			[self avPlayerNextSong];
		}
	});
}

Please let me know if you find any issues with the code, or have problems getting iPod mp3s to play while your app is running in the background. This was meant as a quick look at AVPlayer , there are other steps you need to take in order to actually have audio play in the background, such as adding or setting the correct plist item.

</steve>

Resources

AVPlayer Reference
AVQueuePlayer Reference
AVFoundation Reference
MPMusicPlayerController

Comments are closed.