Continuously playing music in the background on iOS
tl;dr: Have an active background task and audio session running continously whenever the user is semantically playing music, so code can execute while empty buffers are being refilled.
For the longest time since multitasking was introduced in iOS 4, Spotify occasionally had problems with music playback stopping against the user’s wishes. See, iOS applications can only execute in the background when at least one of six conditions are true:
- App has background mode “audio” in Info.plist and is playing audio, with a suitable audio category active
- Equivalent, but for getting location data
- Equivalent, but for talking to external accessories
- App is performing magic VoIP voodoo
- App is a Newsstand app and is downloading content
- App explicitly started a ten minute limited “background task”
If you’re a streaming music service, music to be played in the background won’t always be immediately available. Say your music buffer runs out, your device is on EDGE, and it takes a few seconds to get the music data: silence. Or even if you’re not streaming music, perhaps you need to tear down and set up your audio subsystem in between tracks, e g because they’re different kinds of tracks. What should a well-behaved iOS app do in this situation?
When I realized what the problem is, my first approach was to detect situations when music would not be playing, try to pre-empt them, and start a background task just before music stops playing and then end it when music is back again. If this sounds fragile and stupid to you, you’re absolutely right. I’ve heard other developers come to the same solution, but don’t do it: you’ll miss edge cases, it’ll be buggy, and it’s so overly complex.
The epiphany came when I realized what the audio background mode actually means for background tasks: background tasks become no-ops, with infinite lease time, as long as audio is playing. Only when there is no audio playing does the timer start counting, from ten minutes. Start playing again and the lease is back at infinite time.
The solution is then obvious: whether your app is playing audio is uninteresting. The only interesting information is whether the user currently wants music to play. If she does, start an audio session and a background task, and don’t stop them until the user wants music to stop. In between, it’s up to other subsystems to make sure that audio is playing. With the continous background task, we allow these subsystems to continue to work, even if they can’t play music this very moment. As long as they don’t take playback pauses that are longer than ten minutes, we’re all good.
This is a very simple approach, and should work no matter how your underlying audio playback code works. Below, you can find some sample code implementing my approach.
(If you’re thinking you haven’t seen this problem in Spotify in a long while, you’re right: I wrote this code two years ago and have been thinking I should blog it since then. My apologies for not doing so earlier.)