open-ani/mediamp

Video / audio player for Compose Multiplatform.

137

stars

255

commits

Kotlin

primary language

Sep 6, 2026

updated

android
audio
cmp
compose-multiplatform
jetpack-compose
kmp
kotlin
kotlin-multiplatform
kotlin-multiplatform-mobile
media
player
video

README

MediaMP

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:

PlatformArchitecture(s)Implementation
AndroidAnyExoPlayer
JVM on Windowsx86_64, AArch64MPV
JVM on macOSx86_64, AArch64MPV
JVM on Linuxx86_64MPV
iOSAArch64AVKit
Browser (wasm)AnyHTMLVideoElement

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.

Installation

The latest version is: Maven Central

Compose Multiplatform versions

Each MediaMP version is built against the following Compose Multiplatform (CMP) version:

MediaMP versionCMP version
0.4.01.12.0
0.1.3–0.3.21.10.1
0.0.1–0.1.21.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 uses the public LocalAwtWindow API and requires 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.

Version Catalogs

[versions]
# Replace with the latest version
mediamp = "0.4.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-hls for streaming .m3u8
  • 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.

One-liner

dependencies {
    // Replace with the latest version
    commonMainApi("org.openani.mediamp:mediamp-all:0.4.0")
}

[!TIP] For multi-module projects, consider detailed installation: Detailed Installation.

Supported Media Formats

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

Containers & Streaming

FormatDesktop (MPV)Android (ExoPlayer)iOS (AVKit)Browser (wasm)
MP4 / MOV
Matroska (MKV)
WebM
MPEG-TS
HLS (incl. AES-encrypted)🔶 Safari only

Video Codecs

CodecDesktop (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).

Audio Codecs

CodecDesktop (MPV)Android (ExoPlayer)iOS (AVKit)Browser (wasm)
AAC (incl. LATM/LOAS)
MP3
Opus
FLAC
AC-3 / E-AC-3🔶
DTS (incl. DTS-HD MA)🔶

Subtitles

FormatDesktop (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.

Usage

Streaming Video

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())
    }
}

Observing Playback State

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() }

Accessing Player Features in commonMain

Adjust Playback Speed

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())
}

Unit Testing

[!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
    }
}

Advanced Usages

Custom Media Data

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)
}

Obtaining the Platform Player

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.impl
class 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())
            }
        }
    }
}

License

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.

Contributors

Him188

156 commits

StageGuard

74 commits

NihilDigit

10 commits

openanibot

4 commits

open-ani/mediamp

Video / audio player for Compose Multiplatform.

137

stars

255

commits

Kotlin

primary language

Sep 6, 2026

updated

android
audio
cmp
compose-multiplatform
jetpack-compose
kmp
kotlin
kotlin-multiplatform
kotlin-multiplatform-mobile
media
player
video

README

MediaMP

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:

PlatformArchitecture(s)Implementation
AndroidAnyExoPlayer
JVM on Windowsx86_64, AArch64MPV
JVM on macOSx86_64, AArch64MPV
JVM on Linuxx86_64MPV
iOSAArch64AVKit
Browser (wasm)AnyHTMLVideoElement

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.

Installation

The latest version is: Maven Central

Compose Multiplatform versions

Each MediaMP version is built against the following Compose Multiplatform (CMP) version:

MediaMP versionCMP version
0.4.01.12.0
0.1.3–0.3.21.10.1
0.0.1–0.1.21.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 uses the public LocalAwtWindow API and requires 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.

Version Catalogs

[versions]
# Replace with the latest version
mediamp = "0.4.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-hls for streaming .m3u8
  • 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.

One-liner

dependencies {
    // Replace with the latest version
    commonMainApi("org.openani.mediamp:mediamp-all:0.4.0")
}

[!TIP] For multi-module projects, consider detailed installation: Detailed Installation.

Supported Media Formats

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

Containers & Streaming

FormatDesktop (MPV)Android (ExoPlayer)iOS (AVKit)Browser (wasm)
MP4 / MOV
Matroska (MKV)
WebM
MPEG-TS
HLS (incl. AES-encrypted)🔶 Safari only

Video Codecs

CodecDesktop (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).

Audio Codecs

CodecDesktop (MPV)Android (ExoPlayer)iOS (AVKit)Browser (wasm)
AAC (incl. LATM/LOAS)
MP3
Opus
FLAC
AC-3 / E-AC-3🔶
DTS (incl. DTS-HD MA)🔶

Subtitles

FormatDesktop (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.

Usage

Streaming Video

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())
    }
}

Observing Playback State

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() }

Accessing Player Features in commonMain

Adjust Playback Speed

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())
}

Unit Testing

[!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
    }
}

Advanced Usages

Custom Media Data

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)
}

Obtaining the Platform Player

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.impl
class 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())
            }
        }
    }
}

License

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.

Contributors

Him188

156 commits

StageGuard

74 commits

NihilDigit

10 commits

openanibot

4 commits

Languages

Kotlin

80.8%

C++

16.3%

Objective-C++

2.0%