MediaMP is a media player for Compose Multiplatform. It is a wrapper over popular media player libraries like ExoPlayer on each platform.
The goal is to provide a unified media player abstraction for
commonMain, as
well as supporting backend-specific features and direct access with the underlying media player
library for advanced use cases.
Supported targets and backends:
| Platform | Architecture(s) | Implementation |
|---|---|---|
| Android | Any | ExoPlayer |
| JVM on Windows | x86_64, AArch64 | MPV |
| JVM on macOS | x86_64, AArch64 | MPV |
| JVM on Linux | x86_64 | MPV |
| iOS | AArch64 | AVKit |
| Browser (wasm) | Any | HTMLVideoElement |
Platforms that are not listed above are not supported yet. Feel free to file an issue if you need them.
The VLC backend is deprecated and no longer maintained; MPV replaced it as the desktop backend in state spec v2.
Warning
Pre-1.0: minor releases may contain breaking API changes; they are called out in the release notes. Please open an issue if you have any suggestions or find any bugs.
Each MediaMP version is built against the following Compose Multiplatform (CMP) version:
| MediaMP version | CMP version |
|---|---|
| 0.4.0–0.5.0 | 1.12.0 |
| 0.1.3–0.3.2 | 1.10.1 |
| 0.0.1–0.1.2 | 1.7.1 |
The desktop MPV backend in MediaMP 0.1.14–0.3.2 is incompatible with CMP 1.12.0 because
CMP removed the internal LocalWindow API. MediaMP 0.4.0 and newer use the public LocalAwtWindow
API and require CMP 1.12.0 or newer. See #67.
With CMP 1.12.0, use Kotlin 2.3.20 or newer for Kotlin/Wasm and compileSdk 37 or newer
for Android.
[versions]
# Replace with the latest version
mediamp = "0.5.0"
[libraries]
mediamp-all = { module = "org.openani.mediamp:mediamp-all", version.ref = "mediamp" }dependencies {
commonMainApi(libs.mediamp.all)
}The -all bundle includes:
- Mediamp common APIs and Compose UI APIs
- ExoPlayer backend for Android
- With
media3-exoplayer-hlsfor streaming.m3u8
- With
- MPV backend for JVM (desktop)
- AVKit backend for iOS
- Browser player for Compose Web /
wasmJs
Warning
Compatibility Warning
-all bundle exposes transitive dependencies on recommend backends.
If, in the future, we develop a new backend and believe it is a better choice, the -all may be
updated to the new backend. This should generally be fine unless your app accesses
low-level APIs. Be mindful of this when updating -all bundles to newer versions.
dependencies {
// Replace with the latest version
commonMainApi("org.openani.mediamp:mediamp-all:0.5.0")
}Tip
For multi-module projects, consider detailed installation: Detailed Installation.
The desktop backend bundles its own mpv and FFmpeg build, so its format list is fixed and identical on Windows, macOS and Linux. The other backends delegate to the OS.
Legend: ✅ supported · 🔶 device/browser-dependent · ❌ not supported
| Format | Desktop (MPV) | Android (ExoPlayer) | iOS (AVKit) | Browser (wasm) |
|---|---|---|---|---|
| MP4 / MOV | ✅ | ✅ | ✅ | ✅ |
| Matroska (MKV) | ✅ | ✅ | ❌ | ❌ |
| WebM | ✅ | ✅ | ❌ | ✅ |
| MPEG-TS | ✅ | ✅ | ❌ | ❌ |
| HLS (incl. AES-encrypted) | ✅ | ✅ | ✅ | 🔶 Safari only |
| Codec | Desktop (MPV) | Android (ExoPlayer) | iOS (AVKit) | Browser (wasm) |
|---|---|---|---|---|
| H.264 / AVC | ✅ | ✅ | ✅ | ✅ |
| H.265 / HEVC | ✅ | 🔶 | ✅ | 🔶 |
| AV1 | ✅ | 🔶 | 🔶 | 🔶 |
| VP9 | ✅ | 🔶 | ❌ | ✅ |
Hardware decoding on desktop: D3D11VA (Windows), VideoToolbox (macOS), VAAPI (Linux);
AV1 additionally bundles dav1d for software fallback. Android/iOS/Browser use the
platform decoders (MediaCodec / VideoToolbox / browser-managed).
| Codec | Desktop (MPV) | Android (ExoPlayer) | iOS (AVKit) | Browser (wasm) |
|---|---|---|---|---|
| AAC (incl. LATM/LOAS) | ✅ | ✅ | ✅ | ✅ |
| MP3 | ✅ | ✅ | ✅ | ✅ |
| Opus | ✅ | ✅ | ❌ | ✅ |
| FLAC | ✅ | ✅ | ✅ | ✅ |
| AC-3 / E-AC-3 | ✅ | 🔶 | ✅ | ❌ |
| DTS (incl. DTS-HD MA) | ✅ | 🔶 | ❌ | ❌ |
| Format | Desktop (MPV) | Android (ExoPlayer) | iOS (AVKit) | Browser (wasm) |
|---|---|---|---|---|
| ASS / SSA | ✅ full rendering | 🔶 basic styling | ❌ | ❌ |
| SRT / SubRip | ✅ | ✅ | ❌ | ❌ |
| WebVTT | ✅ | ✅ | ✅ | ✅ |
| PGS | ✅ | ✅ | ❌ | ❌ |
The tables above list common formats only. The desktop backend additionally plays many legacy formats (AVI/WMV/RMVB, MPEG-2/VC-1/RealVideo, WMA/TrueHD, VobSub/SAMI, ...) — see docs/supported-formats.md for the full per-platform breakdown.
fun main() = singleWindowApplication {
val player = rememberMediampPlayer()
val scope = rememberCoroutineScope()
Column {
Button(onClick = {
scope.launch {
player.playUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4")
}
}) {
Text("Play")
}
MediampPlayerSurface(player, Modifier.fillMaxSize())
}
}The player state is an atomic snapshot PlayerState
of three orthogonal axes, observed via player.state (spec: docs/playback-state-v2.md):
val state: PlayerState = player.state.value
state.mediaStatus // lifecycle: Idle / Opening / Ready / Ended / Error / Released
state.playWhenReady // play/pause intent — drive the play/pause button icon with this
state.isBuffering // data availability — show a spinner when state.isLoadingOrBuffering// Play/pause button: never dead, no flicker during buffering.
Button(onClick = { player.togglePlayWhenReady() }) {
Icon(if (state.playWhenReady) PauseIcon else PlayIcon)
}
// Session-advancing reactions (e.g. auto-play-next) use events, not state:
player.events.filterIsInstance<PlaybackEvent.MediaEnded>().collect { playNextEpisode() }val player = rememberMediampPlayer()
LaunchedEffect(player) {
player.playUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4")
}
Column {
Button(onClick = {
player.features[PlaybackSpeed]?.set(2.0f) // `null` means the platform does not support this feature
}) {
Text("Speed up to 2x")
}
MediampPlayerSurface(player, Modifier.fillMaxSize())
}Note
The unit testing API is experimental and will be changed in the future. Use at your own risk.
Add dependency:
[libraries]
mediamp-test = { module = "org.openani.mediamp:mediamp-test", version.ref = "mediamp" }dependencies {
commonTestApi(libs.mediamp.test)
}A scriptable player TestMediampPlayer is provided for unit testing.
It runs the same state machine (and follows the same specification, docs/playback-state-v2.md)
as the real players, backed by a fake native transport that you drive from the test: control how
opens complete (openBehavior), and inject native facts (injectStall, injectEnded,
injectError, injectExternalPlayWhenReady, injectPosition, injectProperties).
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
class MyTest {
@Test
fun test() = runTest {
val player = TestMediampPlayer(StandardTestDispatcher(testScheduler))
// Will not actually make network requests. playUri defaults to playWhenReady = true.
player.playUri("https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4")
assertEquals(MediaStatus.Ready, player.state.value.mediaStatus)
assertTrue(player.state.value.isPlaying)
player.injectPosition(1000L) // The fake playback clock is driven by the test
advanceUntilIdle() // Let the state machine process the injected fact
assertEquals(1000L, player.currentPositionMillis.value)
player.injectStall(true) // Simulate a mid-playback buffering stall
advanceUntilIdle()
assertTrue(player.state.value.isBuffering)
assertTrue(player.state.value.playWhenReady) // Buffering does not change the play intent
}
}fun main() = singleWindowApplication {
val player = rememberMediampPlayer()
val scope = rememberCoroutineScope()
Column {
Button(onClick = {
scope.launch {
player.setMediaData(createMediaData(), playWhenReady = true)
}
}) {
Text("Play")
}
MediampPlayerSurface(player, Modifier.fillMaxSize())
}
}
fun createMediaData(): SeekableInputMediaData {
// Implement SeekableInputMediaData.
// It's like implementing a kotlinx-io Input with random-access seeking.
}If you use kotlinx-io, you might consider the BufferedSeekableInput provided by
mediamp-source-ktxio in helping the
custom implementation of I/O operations:
[libraries]
mediamp-source-ktxio = { module = "org.openani.mediamp:mediamp-source-ktxio", version.ref = "mediamp" }dependencies {
commonMainApi(libs.mediamp.source.ktxio)
}Access the underlying Android ExoPlayer, desktop MPVHandle and iOS AVPlayer for
advanced use cases.
// On Android
val player = ExoPlayerMediampPlayer()
val platform: ExoPlayer = player.impl// On iOS
val player = AVKitMediampPlayer()
val platform: AVPlayer = player.impl// On Desktop
val player = MpvMediampPlayer(...)
val platform: MPVHandle = player.implclass MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val player: MediampPlayer = rememberMediampPlayer()
Column {
Button(onClick = {
Toast.makeText(
this@MainActivity,
"The backend is ${player.impl as ExoPlayer}!",
Toast.LENGTH_SHORT
).show()
}) {
Text("Play")
}
MediampPlayerSurface(player, Modifier.fillMaxSize())
}
}
}
}Some native settings (e.g. buffering) can only be set while the platform player is being built. Each backend accepts an optional hook that runs after MediaMP's defaults are applied, so your settings win.
// On Android: customize the ExoPlayer.Builder before build()
val player = ExoPlayerMediampPlayerFactory().create(
context, coroutineContext,
configurePlayerBuilder = { builder ->
builder.setLoadControl(
DefaultLoadControl.Builder()
.setBufferDurationsMs(30_000, 120_000, 2_500, 5_000)
.build(),
)
},
)// On Desktop / Android / iOS with mpv: set mpv options before mpv_initialize
val player = MpvMediampPlayer(context, coroutineContext) { handle ->
handle.option("demuxer-max-bytes", "${256 * 1024 * 1024}")
handle.option("cache-secs", "120")
}// On iOS: AVFoundation keeps buffering settings on each AVPlayerItem
val player = AVKitMediampPlayer(
configurePlayer = { it.automaticallyWaitsToMinimizeStalling = true },
configurePlayerItem = { item, _ -> item.preferredForwardBufferDuration = 60.0 },
)On Web, pass your own pre-configured HTMLVideoElement to WebMediampPlayer.
All MediaMP source code is licensed under the Apache License version 2 (see LICENSE in the
repository root), except for the VLC modules noted below. The published Maven artifacts that
bundle native libraries additionally carry the license of what they bundle, and each artifact's
POM lists exactly the licenses that apply to it.
These contain MediaMP code only (Kotlin/JVM/Android/iOS). None of them pulls in a native
runtime: mediamp-all and mediamp-mpv only pin the runtime versions, so the desktop
runtimes below are only on your classpath if you add them yourself.
| Artifact | License |
|---|---|
mediamp-api, mediamp-compose, mediamp-all, mediamp-exoplayer, mediamp-avkit, mediamp-ffmpeg, mediamp-mpv (JVM / iOS), mediamp-native-loader, mediamp-source-ktxio |
Apache-2.0 |
mediamp-mpv (Android AAR, bundles libmpv .so built with -Dgpl=false) |
Apache-2.0 + LGPL-2.1-or-later |
mediamp-vlc-loader (depends on vlcj); the deprecated, unpublished mediamp-vlc |
GPL-3.0 |
Desktop natives are separate artifacts that you add with runtimeOnly(...). Each one contains
the Apache-2.0 JNI wrapper plus the bundled libraries listed here.
| Artifact | Bundled libraries | License |
|---|---|---|
mediamp-mpv-runtime-windows-x64, -windows-arm64, -macos-x64, -macos-arm64 |
libmpv built with -Dgpl=false |
Apache-2.0 + LGPL-2.1-or-later |
mediamp-mpv-runtime-linux-x64 |
libmpv built with GPL-only X11 code | GPL-3.0 |
mediamp-mpv-runtime (aggregator, depends on every mpv runtime above including Linux) |
no files of its own | GPL-3.0 |
mediamp-ffmpeg-runtime-<os>-<arch>, mediamp-ffmpeg-runtime (aggregator) |
FFmpeg built without --enable-gpl |
Apache-2.0 + LGPL-2.1-or-later |
mediamp-ffmpeg-runtime-ios-xcframework |
FFmpeg built without --enable-gpl |
Apache-2.0 + LGPL-2.1-or-later |
What this means for a closed-source application:
- Windows, macOS, Android: libmpv and FFmpeg are LGPL, so you may ship them with a proprietary application as long as the LGPL terms are met. The libraries are dynamically loaded from the runtime jar / AAR, so users can replace them.
- Linux: mpv's X11 video output, which the Linux runtime relies on for GLX and VAAPI, has
no LGPL relicensing, so
mediamp-mpv-runtime-linux-x64is built as GPLv2+ libmpv. Combined with MediaMP's Apache-2.0 code the artifact is distributed under GPLv3. Applications that ship it must comply with the GPL; applications that do not target Linux can simply depend on the per-platformmediamp-mpv-runtime-<os>-<arch>artifacts instead of the aggregator and are unaffected. Making the Linux runtime LGPL (via mpv'svaapi-drmpath) is planned.
The corresponding sources for all bundled libraries are the git submodules under
mediamp-mpv/mpv and mediamp-ffmpeg/, built by the Gradle scripts in buildSrc.