
Enables seamless YouTube video and audio playback across multiple platforms, integrating with JetBrains Compose Multiplatform. Features include authentication tokens, event handling, and reels view support.
MediaPlayer-KMP is a Kotlin Multiplatform (KMP) library that allows you to display and play YouTube videos across Android, iOS, Web, and Desktop platforms using JetBrains Compose Multiplatform. It provides a unified API for video playback that seamlessly integrates into Kotlin's multiplatform ecosystem.
VideoPlayer and MediaPlayer work on Android, iOS, Desktop (JVM), JS and Wasm.watch?v=, youtu.be, shorts, embed) and the YouTube player is used automatically..m3u8), DASH, MP3, AAC, WAV, OGG, FLAC and more (format support depends on the platform's native player).Ready, Playing, Paused, Buffering, Ended and Error callbacks.Authorization or any custom header (e.g. a JWT) with media requests.| Feature | Android | iOS | Desktop | Web (JS / Wasm) |
|---|---|---|---|---|
| Video files & streams | ExoPlayer (HLS, DASH, MP4…) | AVPlayer | JavaFX WebView | <video> |
| YouTube | ✅ | ✅ | ✅ | |
| Audio player | ✅ | ✅ | ✅ | ✅ |
| Local files | ✅ | ✅ | ✅ | — |
| Playback events | ✅ | ✅ | ✅ | ✅ |
| Request headers | ✅ streamed | ✅ streamed | ||
| Fullscreen button | ✅ | native controls | — | native controls |
Version Catalog
[versions]
mediaPlayerKMP = "2.2.0"
[libraries]
mediaplayer-kmp = { module = "io.github.khubaibkhan4:mediaplayer-kmp", version.ref = "mediaPlayerKMP" }// composeApp/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.mediaplayer.kmp)
// or: implementation("io.github.khubaibkhan4:mediaplayer-kmp:2.2.0")
}
}
}All public APIs live in the io.github.khubaibkhan4.mediaplayer package:
import io.github.khubaibkhan4.mediaplayer.VideoPlayer
import io.github.khubaibkhan4.mediaplayer.MediaPlayer
import io.github.khubaibkhan4.mediaplayer.PlayerEvent
import io.github.khubaibkhan4.mediaplayer.ReelsView
import io.github.khubaibkhan4.mediaplayer.PagerTypeAndroid — nothing extra. The library only adds the INTERNET permission; it does not declare any
foreground service, so the Play Console will not ask you about media-playback services.
iOS — nothing extra.
Web (JS / Wasm) — nothing extra. Players are positioned over the Compose canvas inside
document.body by default.
Desktop (JVM) — the desktop player is built on JavaFX, which ships separate native jars per OS. The library only compiles against JavaFX, so your desktop app must add the JavaFX jars for the OS it runs on:
// composeApp/build.gradle.kts
kotlin {
sourceSets {
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
listOf("base", "graphics", "controls", "swing", "web", "media").forEach {
implementation("org.openjfx:javafx-$it:19:${javaFxClassifier()}")
}
}
}
}
fun javaFxClassifier(): String {
val os = System.getProperty("os.name").lowercase()
val arm = System.getProperty("os.arch").let { it == "aarch64" || it == "arm64" }
return when {
os.contains("win") -> "win"
os.contains("mac") -> if (arm) "mac-aarch64" else "mac"
else -> if (arm) "linux-aarch64" else "linux"
}
}The library targets Java 17, so desktop apps need JDK 17 or newer.
VideoPlayer detects the URL type automatically: YouTube links open the YouTube player, everything
else (MP4, HLS, DASH, URLs without an extension, local paths) uses the platform's native player.
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://www.youtube.com/watch?v=AD2nEllUMJw",
autoPlay = true,
showControls = true,
)
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://freetestdata.com/wp-content/uploads/2022/02/Free_Test_Data_1MB_MP4.mp4",
autoPlay = false,
showControls = true,
)| Parameter | Description |
|---|---|
modifier |
Size and layout of the player. Give it a height (or aspect ratio). |
url |
YouTube link, media URL or local file path. A YouTube t= parameter sets the start time. |
autoPlay |
Start playing as soon as the media is ready. |
showControls |
Show the native playback controls. |
headers |
Optional HTTP headers sent with media requests (ignored for YouTube). |
onPlayerEvent |
Optional callback receiving PlayerEvents. |
Pass the file path, for example /storage/emulated/0/Movies/a.mp4 on Android,
/Users/me/Movies/a.mp4 on macOS or D:\Videos\a.mp4 on Windows. file:// URIs work too.
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = filePath,
autoPlay = false,
showControls = true,
)MediaPlayer shows an audio player with play/pause, a seek bar and volume control for audio URLs
(mp3, wav, aac, ogg, m4a, flac, radio streams…). On Android, iOS and Web, video URLs passed
to it are shown with the video player; on Desktop MediaPlayer always plays audio only, so use
VideoPlayer for video there.
MediaPlayer(
modifier = Modifier.fillMaxWidth(),
url = "https://commondatastorage.googleapis.com/codeskulptor-demos/DDR_assets/Kangaroo_MusiQue_-_The_Neverwritten_Role_Playing_Game.mp3",
headers = emptyMap(),
startTime = Color.Black,
endTime = Color.Black,
autoPlay = true,
volumeIconColor = Color.Black,
playIconColor = Color.Blue,
sliderTrackColor = Color.LightGray,
sliderIndicatorColor = Color.Blue,
showControls = true,
)Both VideoPlayer and MediaPlayer accept headers, e.g. for private videos protected by a JWT:
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://example.com/protected-video.m3u8",
autoPlay = true,
showControls = true,
headers = mapOf(
"Authorization" to "Bearer $jwt",
"Custom-Header" to "YourValue",
),
)On Android and iOS the headers are sent with every streaming request (including HLS segments). On Web and Desktop the file is downloaded first and then played, so headers are not suitable for live streams or very large files there.
VideoPlayer, MediaPlayer and ReelsView report playback state through onPlayerEvent:
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://example.com/video.mp4",
autoPlay = true,
showControls = true,
onPlayerEvent = { event ->
when (event) {
PlayerEvent.Ready -> println("Loaded")
PlayerEvent.Playing -> println("Started")
PlayerEvent.Paused -> println("Paused")
PlayerEvent.Buffering -> println("Buffering")
PlayerEvent.Ended -> println("Finished")
is PlayerEvent.Error -> println("Failed: ${event.message}")
}
}
)Events are delivered on the UI thread, so you can update Compose state directly.
The Android video player shows a fullscreen button in its controls. Tapping it opens the video in an immersive fullscreen dialog; tapping it again or pressing Back returns to the inline player. The library does not force an orientation; lock or rotate the activity yourself if your app needs landscape fullscreen. YouTube videos use the YouTube player's own fullscreen button.
@Composable
fun MainScreen() {
val videoUrls = listOf(
"https://www.example.com/video1.mp4",
"https://www.example.com/video2.mp4",
"https://www.example.com/video3.mp4"
)
ReelsView(
videoUrls = videoUrls,
pagerType = PagerType.Vertical, // or PagerType.Horizontal
modifier = Modifier.fillMaxSize(),
autoPlay = true,
showControls = true,
onInteraction = { page, url -> println("Page $page: $url") },
onPlayerEvent = { page, event -> println("Page $page: $event") }
)
}val viewer = HtmlContentViewerFactory().createHtmlContentViewer()
val htmlEmbedFeature = HtmlEmbedFeature(viewer)
htmlEmbedFeature.embedHtml(
url = "https://github.com/KhubaibKhan4/MediaPlayer-KMP/",
options = EmbedOptions(
customCss = "body { background-color: #f0f0f0; }",
onPageLoaded = { println("Page loaded successfully!") },
onError = { error -> println("Error loading page: $error") }
)
)
HtmlContentViewerView(
viewer = viewer,
modifier = Modifier.fillMaxSize()
)val viewer = HtmlContentViewerFactory().createHtmlContentViewer()
viewer.loadUrl("https://example.com")
viewer.setPageLoadListener {
viewer.evaluateJavaScript("document.title") { title ->
println("Page title: $title")
}
}import VideoPlayer). Switch to
import io.github.khubaibkhan4.mediaplayer.VideoPlayer (same for MediaPlayer, ReelsView,
PagerType, VideoPlayerScreen). The old imports still compile but are deprecated; the IDE
quick-fix rewrites them for you. They will be removed in a future release.PlaybackService, a media-button receiver or the
FOREGROUND_SERVICE* permissions. If your app relied on them, declare them in your own manifest.headers and onPlayerEvent on VideoPlayer; onPlayerEvent on
MediaPlayer, ReelsView and VideoPlayerScreen. Existing calls keep working.ExoPlayerVideoPlayer,
YoutubeVideoPlayer, DesktopWebView, HtmlView, isVideoFile, formatTime, …) are now internal.
Use VideoPlayer / MediaPlayer instead.Desktop: Error initializing QuantumRenderer: no suitable pipeline found / No toolkit found
Make sure the JavaFX jars match your OS (Platform setup). In VMs or on Linux without
GPU drivers, force software rendering with the JVM argument -Dprism.order=sw:
compose.desktop {
application {
jvmArgs += listOf("-Dprism.order=sw")
}
}Desktop: has been compiled by a more recent version of the Java Runtime
Use JDK 17 or newer (2.2.0+ is compiled for Java 17).
Desktop: a format does not play (e.g. FLV, MKV, live FLV streams) The desktop player relies on JavaFX's WebKit/GStreamer, which supports MP4 (H.264/AAC), HLS, MP3, AAC, WAV and AIFF. Other formats are not supported on desktop yet.
Desktop: YouTube does not start YouTube's player does not fully support JavaFX's embedded browser. Playback of plain video files is unaffected.
Web: the player draws above dialogs / dropdown menus Web players are real HTML elements placed above the Compose canvas, so Compose popups cannot cover them. This is a Compose for Web limitation (CMP-6858); hide the player while a dialog is open.
Web: autoplay starts muted
Browsers block autoplay with sound, so YouTube videos with autoPlay = true start muted.
./gradlew :sample:composeApp:run./gradlew :sample:composeApp:installDebug./gradlew :sample:composeApp:wasmJsBrowserDevelopmentRunFor iOS, open the sample in Xcode (or use the Kotlin Multiplatform plugin in Android Studio) and run it on a simulator.
Publishing to Maven Central is automated by .github/workflows/publish.yml.
MAVEN_CENTRAL_USERNAME and MAVEN_CENTRAL_PASSWORD (a Central Portal user token from
central.sonatype.com → Account → Generate User Token), SIGNING_KEY (output of
gpg --armor --export-secret-keys <KEY_ID>), SIGNING_KEY_ID (last 8 characters of the key id)
and SIGNING_PASSWORD.v2.3.0. Publishing the release builds, tests, signs and
releases version 2.3.0 to Maven Central. A failed run can be re-run from the Actions tab, or
started manually with Run workflow and an explicit version.Let's chat about potential projects, job opportunities, or any other collaboration! Feel free to connect with me through the following channels:
![]() |
![]() |
![]() |
|---|---|---|
![]() |
![]() |
|---|
![]() |
| Mobile | Desktop |
|---|---|
![]() |
![]() |
https://github.com/KhubaibKhan4/MediaPlayer-KMP/assets/98816544/657ad29d-5129-4f78-af56-ad354ba0935d
https://github.com/KhubaibKhan4/MediaPlayer-KMP/assets/98816544/efd68685-2f41-4445-ad76-c183869ab93a
Stargazers
Forkers
We welcome contributions to the MediaPlayer-KMP Library Project! To contribute, please follow these guidelines:
We expect all contributors and users of the MediaPlayer-KMP Library Project to adhere to our code of conduct. Please review the Code of Conduct for details on expected behavior and reporting procedures.
MediaPlayer-KMP is a Kotlin Multiplatform (KMP) library that allows you to display and play YouTube videos across Android, iOS, Web, and Desktop platforms using JetBrains Compose Multiplatform. It provides a unified API for video playback that seamlessly integrates into Kotlin's multiplatform ecosystem.
VideoPlayer and MediaPlayer work on Android, iOS, Desktop (JVM), JS and Wasm.watch?v=, youtu.be, shorts, embed) and the YouTube player is used automatically..m3u8), DASH, MP3, AAC, WAV, OGG, FLAC and more (format support depends on the platform's native player).Ready, Playing, Paused, Buffering, Ended and Error callbacks.Authorization or any custom header (e.g. a JWT) with media requests.| Feature | Android | iOS | Desktop | Web (JS / Wasm) |
|---|---|---|---|---|
| Video files & streams | ExoPlayer (HLS, DASH, MP4…) | AVPlayer | JavaFX WebView | <video> |
| YouTube | ✅ | ✅ | ✅ | |
| Audio player | ✅ | ✅ | ✅ | ✅ |
| Local files | ✅ | ✅ | ✅ | — |
| Playback events | ✅ | ✅ | ✅ | ✅ |
| Request headers | ✅ streamed | ✅ streamed | ||
| Fullscreen button | ✅ | native controls | — | native controls |
Version Catalog
[versions]
mediaPlayerKMP = "2.2.0"
[libraries]
mediaplayer-kmp = { module = "io.github.khubaibkhan4:mediaplayer-kmp", version.ref = "mediaPlayerKMP" }// composeApp/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.mediaplayer.kmp)
// or: implementation("io.github.khubaibkhan4:mediaplayer-kmp:2.2.0")
}
}
}All public APIs live in the io.github.khubaibkhan4.mediaplayer package:
import io.github.khubaibkhan4.mediaplayer.VideoPlayer
import io.github.khubaibkhan4.mediaplayer.MediaPlayer
import io.github.khubaibkhan4.mediaplayer.PlayerEvent
import io.github.khubaibkhan4.mediaplayer.ReelsView
import io.github.khubaibkhan4.mediaplayer.PagerTypeAndroid — nothing extra. The library only adds the INTERNET permission; it does not declare any
foreground service, so the Play Console will not ask you about media-playback services.
iOS — nothing extra.
Web (JS / Wasm) — nothing extra. Players are positioned over the Compose canvas inside
document.body by default.
Desktop (JVM) — the desktop player is built on JavaFX, which ships separate native jars per OS. The library only compiles against JavaFX, so your desktop app must add the JavaFX jars for the OS it runs on:
// composeApp/build.gradle.kts
kotlin {
sourceSets {
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
listOf("base", "graphics", "controls", "swing", "web", "media").forEach {
implementation("org.openjfx:javafx-$it:19:${javaFxClassifier()}")
}
}
}
}
fun javaFxClassifier(): String {
val os = System.getProperty("os.name").lowercase()
val arm = System.getProperty("os.arch").let { it == "aarch64" || it == "arm64" }
return when {
os.contains("win") -> "win"
os.contains("mac") -> if (arm) "mac-aarch64" else "mac"
else -> if (arm) "linux-aarch64" else "linux"
}
}The library targets Java 17, so desktop apps need JDK 17 or newer.
VideoPlayer detects the URL type automatically: YouTube links open the YouTube player, everything
else (MP4, HLS, DASH, URLs without an extension, local paths) uses the platform's native player.
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://www.youtube.com/watch?v=AD2nEllUMJw",
autoPlay = true,
showControls = true,
)
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://freetestdata.com/wp-content/uploads/2022/02/Free_Test_Data_1MB_MP4.mp4",
autoPlay = false,
showControls = true,
)| Parameter | Description |
|---|---|
modifier |
Size and layout of the player. Give it a height (or aspect ratio). |
url |
YouTube link, media URL or local file path. A YouTube t= parameter sets the start time. |
autoPlay |
Start playing as soon as the media is ready. |
showControls |
Show the native playback controls. |
headers |
Optional HTTP headers sent with media requests (ignored for YouTube). |
onPlayerEvent |
Optional callback receiving PlayerEvents. |
Pass the file path, for example /storage/emulated/0/Movies/a.mp4 on Android,
/Users/me/Movies/a.mp4 on macOS or D:\Videos\a.mp4 on Windows. file:// URIs work too.
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = filePath,
autoPlay = false,
showControls = true,
)MediaPlayer shows an audio player with play/pause, a seek bar and volume control for audio URLs
(mp3, wav, aac, ogg, m4a, flac, radio streams…). On Android, iOS and Web, video URLs passed
to it are shown with the video player; on Desktop MediaPlayer always plays audio only, so use
VideoPlayer for video there.
MediaPlayer(
modifier = Modifier.fillMaxWidth(),
url = "https://commondatastorage.googleapis.com/codeskulptor-demos/DDR_assets/Kangaroo_MusiQue_-_The_Neverwritten_Role_Playing_Game.mp3",
headers = emptyMap(),
startTime = Color.Black,
endTime = Color.Black,
autoPlay = true,
volumeIconColor = Color.Black,
playIconColor = Color.Blue,
sliderTrackColor = Color.LightGray,
sliderIndicatorColor = Color.Blue,
showControls = true,
)Both VideoPlayer and MediaPlayer accept headers, e.g. for private videos protected by a JWT:
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://example.com/protected-video.m3u8",
autoPlay = true,
showControls = true,
headers = mapOf(
"Authorization" to "Bearer $jwt",
"Custom-Header" to "YourValue",
),
)On Android and iOS the headers are sent with every streaming request (including HLS segments). On Web and Desktop the file is downloaded first and then played, so headers are not suitable for live streams or very large files there.
VideoPlayer, MediaPlayer and ReelsView report playback state through onPlayerEvent:
VideoPlayer(
modifier = Modifier.fillMaxWidth().height(340.dp),
url = "https://example.com/video.mp4",
autoPlay = true,
showControls = true,
onPlayerEvent = { event ->
when (event) {
PlayerEvent.Ready -> println("Loaded")
PlayerEvent.Playing -> println("Started")
PlayerEvent.Paused -> println("Paused")
PlayerEvent.Buffering -> println("Buffering")
PlayerEvent.Ended -> println("Finished")
is PlayerEvent.Error -> println("Failed: ${event.message}")
}
}
)Events are delivered on the UI thread, so you can update Compose state directly.
The Android video player shows a fullscreen button in its controls. Tapping it opens the video in an immersive fullscreen dialog; tapping it again or pressing Back returns to the inline player. The library does not force an orientation; lock or rotate the activity yourself if your app needs landscape fullscreen. YouTube videos use the YouTube player's own fullscreen button.
@Composable
fun MainScreen() {
val videoUrls = listOf(
"https://www.example.com/video1.mp4",
"https://www.example.com/video2.mp4",
"https://www.example.com/video3.mp4"
)
ReelsView(
videoUrls = videoUrls,
pagerType = PagerType.Vertical, // or PagerType.Horizontal
modifier = Modifier.fillMaxSize(),
autoPlay = true,
showControls = true,
onInteraction = { page, url -> println("Page $page: $url") },
onPlayerEvent = { page, event -> println("Page $page: $event") }
)
}val viewer = HtmlContentViewerFactory().createHtmlContentViewer()
val htmlEmbedFeature = HtmlEmbedFeature(viewer)
htmlEmbedFeature.embedHtml(
url = "https://github.com/KhubaibKhan4/MediaPlayer-KMP/",
options = EmbedOptions(
customCss = "body { background-color: #f0f0f0; }",
onPageLoaded = { println("Page loaded successfully!") },
onError = { error -> println("Error loading page: $error") }
)
)
HtmlContentViewerView(
viewer = viewer,
modifier = Modifier.fillMaxSize()
)val viewer = HtmlContentViewerFactory().createHtmlContentViewer()
viewer.loadUrl("https://example.com")
viewer.setPageLoadListener {
viewer.evaluateJavaScript("document.title") { title ->
println("Page title: $title")
}
}import VideoPlayer). Switch to
import io.github.khubaibkhan4.mediaplayer.VideoPlayer (same for MediaPlayer, ReelsView,
PagerType, VideoPlayerScreen). The old imports still compile but are deprecated; the IDE
quick-fix rewrites them for you. They will be removed in a future release.PlaybackService, a media-button receiver or the
FOREGROUND_SERVICE* permissions. If your app relied on them, declare them in your own manifest.headers and onPlayerEvent on VideoPlayer; onPlayerEvent on
MediaPlayer, ReelsView and VideoPlayerScreen. Existing calls keep working.ExoPlayerVideoPlayer,
YoutubeVideoPlayer, DesktopWebView, HtmlView, isVideoFile, formatTime, …) are now internal.
Use VideoPlayer / MediaPlayer instead.Desktop: Error initializing QuantumRenderer: no suitable pipeline found / No toolkit found
Make sure the JavaFX jars match your OS (Platform setup). In VMs or on Linux without
GPU drivers, force software rendering with the JVM argument -Dprism.order=sw:
compose.desktop {
application {
jvmArgs += listOf("-Dprism.order=sw")
}
}Desktop: has been compiled by a more recent version of the Java Runtime
Use JDK 17 or newer (2.2.0+ is compiled for Java 17).
Desktop: a format does not play (e.g. FLV, MKV, live FLV streams) The desktop player relies on JavaFX's WebKit/GStreamer, which supports MP4 (H.264/AAC), HLS, MP3, AAC, WAV and AIFF. Other formats are not supported on desktop yet.
Desktop: YouTube does not start YouTube's player does not fully support JavaFX's embedded browser. Playback of plain video files is unaffected.
Web: the player draws above dialogs / dropdown menus Web players are real HTML elements placed above the Compose canvas, so Compose popups cannot cover them. This is a Compose for Web limitation (CMP-6858); hide the player while a dialog is open.
Web: autoplay starts muted
Browsers block autoplay with sound, so YouTube videos with autoPlay = true start muted.
./gradlew :sample:composeApp:run./gradlew :sample:composeApp:installDebug./gradlew :sample:composeApp:wasmJsBrowserDevelopmentRunFor iOS, open the sample in Xcode (or use the Kotlin Multiplatform plugin in Android Studio) and run it on a simulator.
Publishing to Maven Central is automated by .github/workflows/publish.yml.
MAVEN_CENTRAL_USERNAME and MAVEN_CENTRAL_PASSWORD (a Central Portal user token from
central.sonatype.com → Account → Generate User Token), SIGNING_KEY (output of
gpg --armor --export-secret-keys <KEY_ID>), SIGNING_KEY_ID (last 8 characters of the key id)
and SIGNING_PASSWORD.v2.3.0. Publishing the release builds, tests, signs and
releases version 2.3.0 to Maven Central. A failed run can be re-run from the Actions tab, or
started manually with Run workflow and an explicit version.Let's chat about potential projects, job opportunities, or any other collaboration! Feel free to connect with me through the following channels:
![]() |
![]() |
![]() |
|---|---|---|
![]() |
![]() |
|---|
![]() |
| Mobile | Desktop |
|---|---|
![]() |
![]() |
https://github.com/KhubaibKhan4/MediaPlayer-KMP/assets/98816544/657ad29d-5129-4f78-af56-ad354ba0935d
https://github.com/KhubaibKhan4/MediaPlayer-KMP/assets/98816544/efd68685-2f41-4445-ad76-c183869ab93a
Stargazers
Forkers
We welcome contributions to the MediaPlayer-KMP Library Project! To contribute, please follow these guidelines:
We expect all contributors and users of the MediaPlayer-KMP Library Project to adhere to our code of conduct. Please review the Code of Conduct for details on expected behavior and reporting procedures.