
Audio player library offers a unified API for seamless music playback, media caching, playlist management, shuffle/repeat modes, and background playback with state monitoring features.
Audio player library built with Kotlin Multiplatform (KMP). It provides a consistent API for music playback functionality across both Android and iOS.
For Kotlin Multiplatform, add the dependency below to your module's build.gradle.kts file:
sourceSets {
commonMain.dependencies {
implementation("io.github.moonggae:kmedia:$kmedia_version")
}
}Add the following permissions and service to your AndroidManifest.xml:
<!-- Permissions -->
<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" />
<!-- Service registration -->
<service android:name="io.github.moonggae.kmedia.session.PlaybackService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>Add the following to your Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>io.github.moonggae.kmedia.assetDownloadSession</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
</array>Initialize KMedia once when your app starts. On Android, call it from Application.onCreate() so
PlaybackService can be created safely before any UI screen.
class App : Application() {
override fun onCreate() {
super.onCreate()
KMedia.initialize(
context = this,
config = KMediaConfig(
cacheEnabled = true,
cacheSizeMb = 1024
)
)
}
}On Android, you can customize the activity Intent used when the media notification is tapped.
The Android-specific overload keeps Android types out of the common KMediaConfig.
import android.net.Uri
import io.github.moonggae.kmedia.KMediaAndroidConfig
import io.github.moonggae.kmedia.initialize
KMedia.initialize(
context = this,
config = KMediaConfig(
cacheEnabled = true,
cacheSizeMb = 1024
),
androidConfig = KMediaAndroidConfig(
sessionActivityIntentProvider = { _, defaultIntent ->
defaultIntent?.apply {
data = Uri.parse("kmedia://session/open?source=notification")
putExtra("source", "media_notification")
}
}
)
)Handle the intent from both onCreate() and onNewIntent() if your app needs the data or extras.
On iOS, call the same initialization API from your app entry point before creating the player UI.
KMedia.initialize(
context = Unit,
config = KMediaConfig(
cacheEnabled = true,
cacheSizeMb = 1024
)
)KMedia can be used as follows:
// Create a KMedia instance
val media = KMedia.create()
// Get music list
val musics = SampleMusicRepository().getSampleMusicList()
// Start playback
media.player.playMusics(musics, startIndex = 0)
// Play/Pause
media.player.play()
media.player.pause()
// Previous/Next track
media.player.previous()
media.player.next()
// Seek to position
media.player.seekTo(positionMs = 30000)For protected audio streams, attach request headers to each Music item. Headers are
kept in memory for playback and are not used as cache keys.
val protectedMusic = Music(
id = "protected-track-1",
title = "Protected Track",
uri = "https://media.example.com/tracks/protected-track-1/stream",
cacheKey = "protected-track-1-audio",
mimeType = "audio/mpeg",
requestHeaders = mapOf(
"Authorization" to "Bearer <token>",
"X-Playback-Token" to "<playback-token>"
)
)
media.player.playMusics(listOf(protectedMusic), startIndex = 0)Call KMedia.initialize(...) once per app process before creating any KMedia instance.
On Android, call it from Application.onCreate() so PlaybackService can resolve its
dependencies even when it is created before UI.
KMedia.create() returns the process-wide KMedia facade.
KMedia.release() releases resources owned by that facade, such as sleep timer jobs and
the playback controller connection. It does not shut down KMedia's internal dependency graph.
Reinitializing KMedia with a different config in the same process is not supported.
You can monitor the playback state through Flow:
val playbackState by media.playbackState.collectAsState()
// Check current playback status
when (playbackState.playingStatus) {
PlayingStatus.PLAYING -> // Playing
PlayingStatus.PAUSED -> // Paused
PlayingStatus.BUFFERING -> // Buffering
PlayingStatus.IDLE -> // Not ready
PlayingStatus.ENDED -> // Playback completed
}
// Current position and duration
val position = playbackState.position
val duration = playbackState.duration
// Current music information
val currentMusic = playbackState.music
val currentIndex = playbackState.currentIndex// Add music to current playlist
media.player.addMusic(newMusic)
// Add multiple musics to playlist
media.player.addMusics(musicList)
// Remove music at specific index
media.player.removeMusic(index = 2)
// Clear entire playlist
media.player.clearPlaylist()You can replace music in the current playlist without interrupting playback:
// Replace music at specific index
val updatedMusic = currentMusic.copy(
uri = "https://example.com/new-track.mp3",
title = "Updated Title"
)
media.player.replaceMusic(index = 0, music = updatedMusic)
// Replace current playing music
val playbackState by media.playbackState.collectAsState()
playbackState.music?.let { currentMusic ->
if (needsUpdate(currentMusic)) {
val newMusic = currentMusic.copy(
uri = getUpdatedUri(currentMusic.id)
)
media.player.replaceMusic(playbackState.currentIndex, newMusic)
}
}Use Cases for Music Replacement:
// Set repeat mode
media.player.setRepeatMode(RepeatMode.REPEAT_MODE_ONE) // Repeat one
media.player.setRepeatMode(RepeatMode.REPEAT_MODE_ALL) // Repeat all
media.player.setRepeatMode(RepeatMode.REPEAT_MODE_OFF) // No repeat
// Set shuffle mode
media.player.setShuffleMode(true) // Enable shuffle
media.player.setShuffleMode(false) // Disable shuffleKMedia includes a built-in sleep timer with smooth fade-out before pause.
// Start timer with fixed duration (milliseconds)
media.sleepTimer.start(durationMs = 15 * 60 * 1000L) // 15 minutes
// Stop playback at the end of current track
media.sleepTimer.startUntilCurrentTrackEnd()
// Cancel timer
media.sleepTimer.cancel()You can observe sleep timer state via Flow:
val sleepTimerState by media.sleepTimer.state.collectAsState()
if (sleepTimerState.isActive) {
println("Mode: ${sleepTimerState.mode}")
println("Remaining: ${sleepTimerState.remainingMs} ms")
}Notes:
ENDED state.// Check cache usage
val cacheUsage by media.cache.usedSizeBytes.collectAsState(initial = 0L)
// Observe cache statuses
val cacheStatuses by media.cache.statuses.collectAsState()
// Cache specific music
media.cache.preCacheMusic(url = "https://example.com/music.mp3", key = "music1")
// Cache protected music without putting tokens in the URL
media.cache.preCacheMusic(
url = "https://example.com/audio/asmr-1/stream",
key = "audio-file-1",
requestHeaders = protectedMusic.requestHeaders
)
// Check cache status
val isCached = media.cache.checkMusicCached("music1")
// Remove cache
media.cache.removeCachedMusic("music1")
// Clear all cache
media.cache.clearCache()KMedia exposes cache status changes as state, so UI code can subscribe without registering
callbacks from Application.
Usage example:
val cacheStatuses by media.cache.statuses.collectAsState()
when (cacheStatuses["music1"]) {
CacheStatus.NONE -> println("music1: No cache")
CacheStatus.PARTIALLY_CACHED -> println("music1: Caching in progress")
CacheStatus.FULLY_CACHED -> println("music1: Caching completed")
null -> println("music1: No cache status yet")
}Playback analytics are exposed as an in-process event stream.
Events are delivered to active collectors only. KMedia does not persist analytics events, does not replay historical events to new collectors, and does not guarantee delivery after process death. If analytics delivery must be guaranteed, collect this stream from an application-level coroutine and persist or upload events in your app layer.
Usage example:
LaunchedEffect(media) {
media.analyticsEvents.collect { event ->
val playPercentage = (event.totalPlayTimeMs.toFloat() / event.durationMs.toFloat()) * 100
println("${event.musicId} playback completed: $playPercentage% played")
// You can send analytics data to server here.
}
}Audio player library built with Kotlin Multiplatform (KMP). It provides a consistent API for music playback functionality across both Android and iOS.
For Kotlin Multiplatform, add the dependency below to your module's build.gradle.kts file:
sourceSets {
commonMain.dependencies {
implementation("io.github.moonggae:kmedia:$kmedia_version")
}
}Add the following permissions and service to your AndroidManifest.xml:
<!-- Permissions -->
<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" />
<!-- Service registration -->
<service android:name="io.github.moonggae.kmedia.session.PlaybackService"
android:exported="false"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>Add the following to your Info.plist:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>io.github.moonggae.kmedia.assetDownloadSession</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
</array>Initialize KMedia once when your app starts. On Android, call it from Application.onCreate() so
PlaybackService can be created safely before any UI screen.
class App : Application() {
override fun onCreate() {
super.onCreate()
KMedia.initialize(
context = this,
config = KMediaConfig(
cacheEnabled = true,
cacheSizeMb = 1024
)
)
}
}On Android, you can customize the activity Intent used when the media notification is tapped.
The Android-specific overload keeps Android types out of the common KMediaConfig.
import android.net.Uri
import io.github.moonggae.kmedia.KMediaAndroidConfig
import io.github.moonggae.kmedia.initialize
KMedia.initialize(
context = this,
config = KMediaConfig(
cacheEnabled = true,
cacheSizeMb = 1024
),
androidConfig = KMediaAndroidConfig(
sessionActivityIntentProvider = { _, defaultIntent ->
defaultIntent?.apply {
data = Uri.parse("kmedia://session/open?source=notification")
putExtra("source", "media_notification")
}
}
)
)Handle the intent from both onCreate() and onNewIntent() if your app needs the data or extras.
On iOS, call the same initialization API from your app entry point before creating the player UI.
KMedia.initialize(
context = Unit,
config = KMediaConfig(
cacheEnabled = true,
cacheSizeMb = 1024
)
)KMedia can be used as follows:
// Create a KMedia instance
val media = KMedia.create()
// Get music list
val musics = SampleMusicRepository().getSampleMusicList()
// Start playback
media.player.playMusics(musics, startIndex = 0)
// Play/Pause
media.player.play()
media.player.pause()
// Previous/Next track
media.player.previous()
media.player.next()
// Seek to position
media.player.seekTo(positionMs = 30000)For protected audio streams, attach request headers to each Music item. Headers are
kept in memory for playback and are not used as cache keys.
val protectedMusic = Music(
id = "protected-track-1",
title = "Protected Track",
uri = "https://media.example.com/tracks/protected-track-1/stream",
cacheKey = "protected-track-1-audio",
mimeType = "audio/mpeg",
requestHeaders = mapOf(
"Authorization" to "Bearer <token>",
"X-Playback-Token" to "<playback-token>"
)
)
media.player.playMusics(listOf(protectedMusic), startIndex = 0)Call KMedia.initialize(...) once per app process before creating any KMedia instance.
On Android, call it from Application.onCreate() so PlaybackService can resolve its
dependencies even when it is created before UI.
KMedia.create() returns the process-wide KMedia facade.
KMedia.release() releases resources owned by that facade, such as sleep timer jobs and
the playback controller connection. It does not shut down KMedia's internal dependency graph.
Reinitializing KMedia with a different config in the same process is not supported.
You can monitor the playback state through Flow:
val playbackState by media.playbackState.collectAsState()
// Check current playback status
when (playbackState.playingStatus) {
PlayingStatus.PLAYING -> // Playing
PlayingStatus.PAUSED -> // Paused
PlayingStatus.BUFFERING -> // Buffering
PlayingStatus.IDLE -> // Not ready
PlayingStatus.ENDED -> // Playback completed
}
// Current position and duration
val position = playbackState.position
val duration = playbackState.duration
// Current music information
val currentMusic = playbackState.music
val currentIndex = playbackState.currentIndex// Add music to current playlist
media.player.addMusic(newMusic)
// Add multiple musics to playlist
media.player.addMusics(musicList)
// Remove music at specific index
media.player.removeMusic(index = 2)
// Clear entire playlist
media.player.clearPlaylist()You can replace music in the current playlist without interrupting playback:
// Replace music at specific index
val updatedMusic = currentMusic.copy(
uri = "https://example.com/new-track.mp3",
title = "Updated Title"
)
media.player.replaceMusic(index = 0, music = updatedMusic)
// Replace current playing music
val playbackState by media.playbackState.collectAsState()
playbackState.music?.let { currentMusic ->
if (needsUpdate(currentMusic)) {
val newMusic = currentMusic.copy(
uri = getUpdatedUri(currentMusic.id)
)
media.player.replaceMusic(playbackState.currentIndex, newMusic)
}
}Use Cases for Music Replacement:
// Set repeat mode
media.player.setRepeatMode(RepeatMode.REPEAT_MODE_ONE) // Repeat one
media.player.setRepeatMode(RepeatMode.REPEAT_MODE_ALL) // Repeat all
media.player.setRepeatMode(RepeatMode.REPEAT_MODE_OFF) // No repeat
// Set shuffle mode
media.player.setShuffleMode(true) // Enable shuffle
media.player.setShuffleMode(false) // Disable shuffleKMedia includes a built-in sleep timer with smooth fade-out before pause.
// Start timer with fixed duration (milliseconds)
media.sleepTimer.start(durationMs = 15 * 60 * 1000L) // 15 minutes
// Stop playback at the end of current track
media.sleepTimer.startUntilCurrentTrackEnd()
// Cancel timer
media.sleepTimer.cancel()You can observe sleep timer state via Flow:
val sleepTimerState by media.sleepTimer.state.collectAsState()
if (sleepTimerState.isActive) {
println("Mode: ${sleepTimerState.mode}")
println("Remaining: ${sleepTimerState.remainingMs} ms")
}Notes:
ENDED state.// Check cache usage
val cacheUsage by media.cache.usedSizeBytes.collectAsState(initial = 0L)
// Observe cache statuses
val cacheStatuses by media.cache.statuses.collectAsState()
// Cache specific music
media.cache.preCacheMusic(url = "https://example.com/music.mp3", key = "music1")
// Cache protected music without putting tokens in the URL
media.cache.preCacheMusic(
url = "https://example.com/audio/asmr-1/stream",
key = "audio-file-1",
requestHeaders = protectedMusic.requestHeaders
)
// Check cache status
val isCached = media.cache.checkMusicCached("music1")
// Remove cache
media.cache.removeCachedMusic("music1")
// Clear all cache
media.cache.clearCache()KMedia exposes cache status changes as state, so UI code can subscribe without registering
callbacks from Application.
Usage example:
val cacheStatuses by media.cache.statuses.collectAsState()
when (cacheStatuses["music1"]) {
CacheStatus.NONE -> println("music1: No cache")
CacheStatus.PARTIALLY_CACHED -> println("music1: Caching in progress")
CacheStatus.FULLY_CACHED -> println("music1: Caching completed")
null -> println("music1: No cache status yet")
}Playback analytics are exposed as an in-process event stream.
Events are delivered to active collectors only. KMedia does not persist analytics events, does not replay historical events to new collectors, and does not guarantee delivery after process death. If analytics delivery must be guaranteed, collect this stream from an application-level coroutine and persist or upload events in your app layer.
Usage example:
LaunchedEffect(media) {
media.analyticsEvents.collect { event ->
val playPercentage = (event.totalPlayTimeMs.toFloat() / event.durationMs.toFloat()) * 100
println("${event.musicId} playback completed: $playPercentage% played")
// You can send analytics data to server here.
}
}