Simple Music Player App By Using Media 3
Media 3 looks complex, but finally, I created a simple version.
if u want to create simple music player without worrying about to run it in background or show media controls in notification then you can simple use exoplayer .
But if u want ur player should run in background and show media controls than u need these things.
MediaLibrarySession
MediaLibrarySession callback
MediaLibrarySession helps your app to run even in foreground and background .
and MediaLibrary Session help you to control your playback.
Now coding Part
Dependency I useddependencies {
//media3 implementation (
libs.androidx.media
3.exoplayer) implementation (
libs.androidx.media
3.ui) implementation (
libs.androidx.media
3.common) implementation (
libs.androidx.media
3.session) implementation(
libs.androidx.media
3.exoplayer.hls) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation (libs.glide) annotationProcessor (libs.glide.compiler) }
My PlaybackService
package com.example.myapplication
import android.content.Intent
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@UnstableApi
class PlayerService : MediaLibraryService() {
private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var player: Player
override fun onCreate() {
super.onCreate()
player = ExoPlayer.Builder(this).build()
mediaLibrarySession = MediaLibrarySession.Builder(
this,
player,
MediaLibrarySessionCallback()
).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onDestroy() {
mediaLibrarySession.release()
player.release()
super.onDestroy()
}
override fun onTaskRemoved(rootIntent: Intent?) {
mediaLibrarySession.release()
player.release()
stopSelf()
super.onTaskRemoved(rootIntent)
}
private inner class MediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }
return Futures.immediateFuture(updatedMediaItems)
}
override fun onPlaybackResumption(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo
): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
// Implement your logic for resuming playback here
// For example, you might want to return the last played item
return Futures.immediateFuture(
MediaSession.MediaItemsWithStartPosition(emptyList(), 0, 0)
)
}
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
return connectionResult;
}
}
}
MainActivity
package com.example.myapplication
import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.widget.SeekBar
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.bumptech.glide.Glide
import com.example.myapplication.databinding.ActivityMainBinding
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import kotlin.time.Duration
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var controller: MediaController
private var mediaControllerFuture: ListenableFuture<MediaController>? = null
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//media player code
// Media player code
Log.e("MEDIACONTROLLER", "SESSION CREATED BEFORE")
val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java))
Log.e("MEDIACONTROLLER", "SESSION CREATED")
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
mediaControllerFuture?.apply {
addListener(Runnable {
Log.e("MEDIACONTROLLER", "Media Controller Future")
controller = get()
setupSeekBar()
}, MoreExecutors.directExecutor())
Log.e("MEDIACONTROLLER", "Media Controller Future")
}
// Initialize the binding
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
Glide.with(this)
.load("https://picsum.photos/seed/picsum/600/270")
.into(binding.songImage)
binding.play.setOnClickListener {
playMedia()
}
binding.pause.setOnClickListener {
controller.pause()
}
binding.stop.setOnClickListener {
controller.stop()
}
}
fun playMedia() {
val mediaItem = MediaItem.Builder()
.setMediaId("YOUR SONG URL COME HERE .m3u8")
.setMediaMetadata(
MediaMetadata.Builder()
.setFolderType(MediaMetadata.FOLDER_TYPE_ALBUMS)
.setArtworkUri(Uri.parse("https://picsum.photos/200"))
.setAlbumTitle("YOUR ALBUM TITLE")
.setDisplayTitle("YOUR SONG NAME")
.build()
)
.setMimeType(MimeTypes.APPLICATION_M3U8)
.build()
controller.setMediaItem(mediaItem)
controller.prepare()
controller.play()
Log.e("MEDIACONTROLLER", "Media Controller PLAY")
}
override fun onDestroy() {
super.onDestroy()
mediaControllerFuture?.let {
MediaController.releaseFuture(it)
}
handler.removeCallbacks(updateSeekBarRunnable)
}
@OptIn(UnstableApi::class)
private fun setupSeekBar() {
// Update seekbar progress as playback progresses
controller.addListener(object : Player.Listener {
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
updateSeekBar()
}
override fun onPlaybackStateChanged(state: Int) {
updateSeekBar()
}
})
// Set up seekbar change listener
binding.seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
controller.seekTo(progress.toLong())
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// You can pause playback here if desired
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
// You can resume playback here if you paused in onStartTrackingTouch
}
})
// Initial update of seekbar
updateSeekBar()
}
@OptIn(UnstableApi::class)
private fun updateSeekBar() {
val duration = controller.duration
if (duration > 0) {
binding.seekBar.max = duration.toInt()
}
binding.seekBar.progress = controller.currentPosition.toInt()
if(controller.isPlaying){
updateDuration(
formatDuration(controller.currentPosition),
formatDuration(controller.contentDuration)
)
}
// Schedule the next update
handler.removeCallbacks(updateSeekBarRunnable)
handler.postDelayed(updateSeekBarRunnable, 1000)
}
private val handler = Handler(Looper.getMainLooper())
private val updateSeekBarRunnable = object : Runnable {
override fun run() {
updateSeekBar()
}
}
private fun updateDuration(currentDuration:String,totalDuration:String){
binding.currentDuration.text= currentDuration
binding.totalDuration.text= totalDuration
}
fun formatDuration(milliseconds: Long): String {
val minutes = (milliseconds / 1000) / 60
val seconds = (milliseconds / 1000) % 60
// Format the time as MM:SS, ensuring two digits for both minutes and seconds
return String.format("%02d:%02d", minutes, seconds)
}
}
XML for layout
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:tools="
http://schemas.android.com/tools
" xmlns:android="
http://schemas.android.com/apk/res/android
" xmlns:app="
http://schemas.android.com/apk/res-auto
"> <data> <!-- <variable--> <!-- name="textName"--> <!-- type="String" />--> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/gradientbackground" android:orientation="vertical"> <ImageView android:id="@+id/songImage" android:layout_width="match_parent" android:layout_height="270dp" android:scaleType="centerCrop" tools:srcCompat="@tools:sample/backgrounds/scenic" /> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="20.dp" android:paddingTop="30.dp" android:paddingBottom="20.dp" android:text="Evolve" android:textColor="#000000" android:textSize="24sp" android:textStyle="bold" /> <SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="match_parent" android:layout_height="23dp" android:orientation="horizontal" android:paddingLeft="20dp" android:paddingRight="20dp"> <TextView android:id="@+id/currentDuration" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" /> <TextView android:id="@+id/totalDuration" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAlignment="textEnd" /> </LinearLayout> <Space android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="20dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="54dp" android:orientation="horizontal" android:paddingLeft="16dp" android:paddingRight="16dp" > <Button android:id="@+id/pause" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="Pause" /> <Button android:id="@+id/play" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="Play" /> <Button android:id="@+id/stop" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="Stop" /> </LinearLayout> </LinearLayout> </layout>
Manifest File<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="
http://schemas.android.com/apk/res/android
" xmlns:tools="
http://schemas.android.com/tools
"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AppCompat.Light.NoActionBar"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".PlayerService" android:exported="true" android:foregroundServiceType="mediaPlayback"> <intent-filter> <action android:name="
androidx.media
3.session.MediaSessionService" /> <action android:name="
android.media
.browse.MediaBrowserService" /> </intent-filter> </service> </application> </manifest>