Building a Music Player App with Kotlin and ExoPlayer

In this tutorial, we will explore the process of building a music player app using Kotlin and ExoPlayer. Kotlin is a modern programming language for Android development that offers concise syntax, null safety, and many other features. ExoPlayer is an open-source media player library developed by Google that provides powerful playback capabilities for audio and video files.

building music player app kotlin exoplayer

Introduction

Kotlin is a statically-typed programming language developed by JetBrains. It is fully interoperable with Java and offers many features that help developers write more concise and expressive code. ExoPlayer, on the other hand, is a flexible and extensible media player library that provides support for various media formats and streaming protocols.

By combining Kotlin and ExoPlayer, we can create a music player app that offers seamless playback, customizable user interface, and advanced features like audio effects and playlist management. In this tutorial, we will cover the entire process of building such an app, starting from setting up the project to implementing advanced functionality.

Setting Up the Project

To begin, we need to create a new Kotlin project in Android Studio. Open Android Studio and select "Start a new Android Studio project". Choose an appropriate project name and set the minimum SDK version to Android 5.0 (API level 21) or higher.

Next, we need to add the ExoPlayer dependency to our project. Open the build.gradle file for the app module and add the following line to the dependencies block:

implementation 'com.google.android.exoplayer:exoplayer:2.15.1'

This will include the ExoPlayer library in our project. Sync the project to download the library and make it available for use.

Now, let's set up the layout for our music player app. Open the activity_main.xml layout file and add the necessary views for displaying the media controls and other UI elements. You can customize the layout according to your app's design requirements.

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- Add your views here -->

</RelativeLayout>

With the project set up and the layout defined, we can now move on to implementing the basic functionality of our music player app.

Implementing Basic Functionality

The first step is to load and play audio files in our app. We can use the ExoPlayer library to handle the playback of audio files. To load an audio file, we need to create a SimpleExoPlayer instance and set the media source to the desired audio file.

// Create a SimpleExoPlayer instance
val player = SimpleExoPlayer.Builder(context).build()

// Set the media source to the audio file
val mediaItem = MediaItem.fromUri(audioUri)
player.setMediaItem(mediaItem)

// Prepare the player for playback
player.prepare()

Next, we need to display media controls to allow the user to play, pause, and seek the audio file. We can use the PlayerView provided by ExoPlayer to achieve this.

// Create a PlayerView instance
val playerView = findViewById<PlayerView>(R.id.player_view)

// Attach the player to the PlayerView
playerView.player = player

To handle playback events, we can add a Player.EventListener to the player instance. This listener will be notified when playback state changes occur, such as when the player starts, pauses, or stops playback.

// Create a Player.EventListener instance
val eventListener = object : Player.EventListener {
    override fun onPlaybackStateChanged(state: Int) {
        // Handle playback state changes
    }

    override fun onPlayerError(error: ExoPlaybackException) {
        // Handle player errors
    }
}

// Add the event listener to the player
player.addListener(eventListener)

With the basic functionality implemented, our music player app is ready to play audio files. However, we can enhance the user experience by adding additional features.

Enhancing the User Experience

One way to enhance the user experience is to add a seek bar that allows the user to seek to a specific position in the audio file. We can add a SeekBar view to our layout and update its progress based on the current playback position.

<SeekBar
    android:id="@+id/seek_bar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/player_view" />
// Get a reference to the SeekBar view
val seekBar = findViewById<SeekBar>(R.id.seek_bar)

// Update the SeekBar progress based on the current playback position
seekBar.max = player.duration.toInt()
seekBar.progress = player.currentPosition.toInt()

// Add a listener to the SeekBar to handle user seek events
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        // Seek to the desired position in the audio file
        player.seekTo(progress.toLong())
    }

    override fun onStartTrackingTouch(seekBar: SeekBar?) {
        // Handle the start of user seek events
    }

    override fun onStopTrackingTouch(seekBar: SeekBar?) {
        // Handle the end of user seek events
    }
})

Another way to enhance the user experience is to implement playlist functionality. We can create a list of audio files and allow the user to switch between them. To implement playlist functionality, we need to keep track of the current position in the playlist and update the media source accordingly.

// Create a list of audio files
val playlist = listOf(audioUri1, audioUri2, audioUri3)

// Keep track of the current position in the playlist
var currentPlaylistPosition = 0

// Update the media source to the current audio file
val mediaItem = MediaItem.fromUri(playlist[currentPlaylistPosition])
player.setMediaItem(mediaItem)
player.prepare()

To customize the player UI, we can modify the layout file and add additional views, such as buttons for shuffle and repeat functionality. We can also customize the appearance of the media controls provided by the PlayerView.

With these enhancements, our music player app now offers a more interactive and user-friendly experience. However, we also need to handle audio focus and interruptions to ensure smooth playback.

Handling Audio Focus and Interruptions

Managing audio focus is crucial for a music player app to coexist with other audio-playing apps on the user's device. We can use the AudioManager system service to request and abandon audio focus when needed.

// Get a reference to the AudioManager system service
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager

// Request audio focus
val result = audioManager.requestAudioFocus(
    audioFocusChangeListener,
    AudioManager.STREAM_MUSIC,
    AudioManager.AUDIOFOCUS_GAIN
)

// Handle the audio focus request result
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    // Start playback
} else {
    // Handle audio focus request failure
}

To handle incoming calls and notifications, we can listen for the ACTION_PHONE_STATE_CHANGED and ACTION_NOTIFICATION_POLICY_CHANGED broadcast intents. When these intents are received, we can pause the playback and save the current playback position.

// Create a BroadcastReceiver instance
val broadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        when (intent?.action) {
            TelephonyManager.ACTION_PHONE_STATE_CHANGED -> {
                // Handle incoming calls
            }
            AudioManager.ACTION_NOTIFICATION_POLICY_CHANGED -> {
                // Handle notification policy changes
            }
        }
    }
}

// Register the BroadcastReceiver to receive the intents
val intentFilter = IntentFilter().apply {
    addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED)
    addAction(AudioManager.ACTION_NOTIFICATION_POLICY_CHANGED)
}
registerReceiver(broadcastReceiver, intentFilter)

To deal with audio interruptions, such as when a phone call is received, we can listen for the AudioManager.ACTION_AUDIO_BECOMING_NOISY broadcast intent. When this intent is received, we can pause the playback.

// Create a BroadcastReceiver instance
val broadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
            // Pause playback
        }
    }
}

// Register the BroadcastReceiver to receive the intent
val intentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
registerReceiver(broadcastReceiver, intentFilter)

By handling audio focus and interruptions, our music player app can provide a seamless playback experience even in the presence of external audio events.

Implementing Advanced Features

To implement audio effects, such as equalizer and reverb, we can use the AudioEffect class provided by the Android framework. We can create an instance of AudioEffect and apply the desired audio effect to the player.

// Create an instance of AudioEffect
val audioEffect = AudioEffect(EFFECT_TYPE_EQUALIZER, EFFECT_UUID_EQUALIZER, 0, 0)

// Apply the audio effect to the player
player.audioComponent.addAudioEffect(audioEffect)

To support audio streaming, we can use the HlsMediaSource class provided by ExoPlayer. This class allows us to load and play audio files from HLS (HTTP Live Streaming) sources.

// Create a Uri for the HLS audio source
val hlsUri = Uri.parse("http://example.com/audio.m3u8")

// Create a MediaSource for the HLS audio source
val mediaSource = HlsMediaSource.Factory(dataSourceFactory)
    .createMediaSource(MediaItem.fromUri(hlsUri))

// Set the media source to the player
player.setMediaSource(mediaSource)

// Prepare the player for playback
player.prepare()

Working with playlists can be achieved by creating a custom data structure to hold the list of audio files and implementing logic to navigate through the playlist.

// Create a custom data structure for the playlist
data class AudioTrack(val title: String, val uri: Uri)

// Create a list of AudioTrack objects
val playlist = listOf(
    AudioTrack("Track 1", audioUri1),
    AudioTrack("Track 2", audioUri2),
    AudioTrack("Track 3", audioUri3)
)

// Keep track of the current position in the playlist
var currentPlaylistPosition = 0

// Update the media source to the current audio file
val mediaItem = MediaItem.fromUri(playlist[currentPlaylistPosition].uri)
player.setMediaItem(mediaItem)
player.prepare()

With these advanced features implemented, our music player app now provides a wide range of functionality to enhance the user's listening experience.

Conclusion

In this tutorial, we have explored the process of building a music player app using Kotlin and ExoPlayer. We started by setting up the project and adding the ExoPlayer dependency. Then, we implemented basic functionality for loading and playing audio files, displaying media controls, and handling playback events.

Next, we enhanced the user experience by adding a seek bar, implementing playlist functionality, and customizing the player UI. We also discussed how to handle audio focus and interruptions to ensure smooth playback.

Finally, we implemented advanced features like audio effects, audio streaming, and playlist management. By following this tutorial, you should now have a solid understanding of how to build a music player app using Kotlin and ExoPlayer. Happy coding!