Skip to content

Latest commit

 

History

268 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

Compose Multiplatform versions

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.

Version Catalogs

[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-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.5.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())
            }
        }
    }
}

Configuring the Platform Player

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.

License

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.

Code artifacts

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

Native runtime artifacts

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-x64 is 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-platform mediamp-mpv-runtime-<os>-<arch> artifacts instead of the aggregator and are unaffected. Making the Linux runtime LGPL (via mpv's vaapi-drm path) 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.

Releases

Contributors

Languages