There are already a few tutorials about building building an app that plays music on iOS. Why am I writing a whole new post about it then? Most of the existing tutorials focus on the really basic concepts about a music player app. This is going to be different. We will set up a full fledged music player with it’s own music library, ability to create custom playlists, streaming songs online etc.
This is not a post about accessing the existing music on user’s device, so if you are looking for that, feel free to jump back to Google and resume your search. This also isn’t about the low level use of MediaPlayer framework. We will be using a library to handle this.
The first thing we are going to tackle in the post is creating the Music Player. We are using StreamingKit, so we have most of our work cut for us, but it still requires a bit of handling logic around it to implement all the features that a Music Player of this generation needs.
To play music with StreamingKit, we need a STKAudioPlayer
. Check out all the options for the audio player in the source.
var audioPlayer: STKAudioPlayer!
// MARK: Init
override init() {
super.init()
resetAudioPlayer()
}
private func resetAudioPlayer() {
var options = STKAudioPlayerOptions()
options.flushQueueOnSeek = true
options.enableVolumeMixer = true
audioPlayer = STKAudioPlayer(options: options)
// Set up audio player
audioPlayer.meteringEnabled = true
audioPlayer.volume = 1
audioPlayer.delegate = self
}
Next thing a music player needs is to handle a queue of music files. You could get away with handling just one file per player and simplify the logic, but since we are focusing on a full fledged music player, we will see how to handle the queue. We create a class Track
to handle the properties of each queue element. I will be skipping the implementation since it would be pretty specific depending on the app you are working on, but the main properties it should be supporting are identifier
and a url
(the URL for the music file, local/streaming).
private(set) var queue: [AlbumFile]
// MARK: Public API
internal func playWithQueue(queue: [AlbumFile], index: Int = 0) {
guard index >= 0 && index < queue.count else {
return
}
self.queue = queue
audioPlayer.clearQueue()
if let url = queue[index].url {
audioPlayer.playURL(url)
}
for i in 1 ..< queue.count {
audioPlayer.queueURL(queue[Int((index + i) % queue.count)].url ?? NSURL())
}
currentIndex = index
loop = false
}
While we are adding a public API to the Music Player, let’s also add simple helper functions to control the playback.
internal func stop() {
audioPlayer.stop()
queue = []
currentIndex = -1
}
internal func play(file: AlbumFile) {
audioPlayer.playURL(file.url ?? NSURL())
}
internal func next() {
guard queue.count > 0 else {
return
}
currentIndex = (currentIndex + 1) % queue.count
playWithQueue(queue, index: currentIndex)
}
internal func prev() {
currentIndex = max(0, currentIndex - 1)
playWithQueue(queue, index: currentIndex)
}
STKAudioPlayer
communicates several events during playback to its delegate like when an item starts playing, finishes buffering, finishes playing or encounters an error. Let’s see how we can handle these events to perform meaningful operations in the music player.
// MARK: Audio Player Delegate
func audioPlayer(audioPlayer: STKAudioPlayer, didStartPlayingQueueItemId queueItemId: NSObject) {
if let index = (queue.indexOf { $0.url == queueItemId }) {
currentIndex = index
}
}
func audioPlayer(audioPlayer: STKAudioPlayer, didFinishBufferingSourceWithQueueItemId queueItemId: NSObject) {
updateNowPlayingInfoCenter()
}
func audioPlayer(audioPlayer: STKAudioPlayer, stateChanged state: STKAudioPlayerState, previousState: STKAudioPlayerState) {
self.state.value = state
if state != .Stopped && state != .Error && state != .Disposed {
}
updateNowPlayingInfoCenter()
}
func audioPlayer(audioPlayer: STKAudioPlayer, didFinishPlayingQueueItemId queueItemId: NSObject, withReason stopReason: STKAudioPlayerStopReason, andProgress progress: Double, andDuration duration: Double) {
if let index = (queue.indexOf { $0.url == audioPlayer.currentlyPlayingQueueItemId() }) {
currentIndex = index
}
if stopReason == .Eof {
next()
} else if stopReason == .Error {
stop()
resetAudioPlayer()
}
}
func audioPlayer(audioPlayer: STKAudioPlayer, unexpectedError errorCode: STKAudioPlayerErrorCode) {
print("Error when playing music \(errorCode)")
resetAudioPlayer()
playWithQueue(queue, index: currentIndex)
}
In order to play music while your app is in the background, you must do some additional set up.
Info.plist
file to include the audio in the UIBackgroundModes
array.AppDelegate.application(application:, didFinishLaunchingWithOptions:)
.func setupMusicPlayer() {
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
} catch {
print("Failed to setup audio")
}
}
After we wrap everything up in a class (named MusicPlayer
for the tutorial), we are good to go. Since you would normally be supporting just one instance of the music player in your app, it’s a good idea to create a singleton and use it all around your app. Here’s how you can set it up.
class MusicPlayer: NSObject, STKAudioPlayerDelegate {
private static var instance = MusicPlayer()
// …
// MARK: Static
internal static func sharedInstance() -> MusicPlayer {
return instance
}
}
Here’s how you can use it from other parts of the UI.
MusicPlayer.sharedInstance().playWithQueue(tracks, index: indexPath.row)