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.
[versions]
# Replace with the latest version
mediamp = "0.3.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.3.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())
}
}
}
}MediaMP is mainly licensed under the Apache License version 2. However, depending on the license of transitive dependencies, the backend-specific implementations may have different licenses.
A breakdown of the licenses:
- mediamp-exoplayer: Apache License 2.0 (Apache-v2)
- mediamp-mpv: Apache License 2.0
- All other published modules: Apache License 2.0
The deprecated, no-longer-published mediamp-vlc sources remain GPLv3 (mediamp-vlc/LICENSE).
You can find the full license text of Apache-v2 in the LICENSE file from the root of the
repository.