Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

244 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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:

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.

Installation

The latest version is: Maven Central

Version Catalogs

[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-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.3.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

Format Desktop (MPV) Android (ExoPlayer) iOS (AVKit) Browser (wasm)
MP4 / MOV βœ… βœ… βœ… βœ…
Matroska (MKV) βœ… βœ… ❌ ❌
WebM βœ… βœ… ❌ βœ…
MPEG-TS βœ… βœ… ❌ ❌
HLS (incl. AES-encrypted) βœ… βœ… βœ… πŸ”Ά Safari only

Video Codecs

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

Audio Codecs

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) βœ… πŸ”Ά ❌ ❌

Subtitles

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.

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.

Releases

Contributors

Languages