Skip to content

Commit

Permalink
Generalize backends
Browse files Browse the repository at this point in the history
  • Loading branch information
Him188 committed Dec 22, 2024
1 parent 0f8e1df commit 19ace54
Show file tree
Hide file tree
Showing 56 changed files with 770 additions and 364 deletions.
89 changes: 72 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MediaMP

MediaMP is a Kotlin-first media player for Jetpack Compose and Compose Multiplatform. It is an
MediaMP is a Kotlin-first media player for Compose Multiplatform. It is an
wrapper over popular media player libraries like ExoPlayer on each platform.

Supported targets and backends:
Expand All @@ -13,22 +13,77 @@ Supported targets and backends:

Platforms that are not listed above are not supported yet.

## Usage

Check the latest
version: [![Maven Central](https://img.shields.io/maven-central/v/org.openani.mediamp/mediamp-core)](https://img.shields.io/maven-central/v/org.openani.mediamp/mediamp-core)

### Kotlin Multiplatform

```kotlin
kotlin {
val mediampVersion = "0.1.0" // Replace with the latest version
sourceSets.commonMain.dependencies {
implementation("org.openani.mediamp:mediamp-core:$mediampVersion") // for data-layer, does not depend on Compose

implementation("org.openani.mediamp:mediamp-compose:$mediampVersion") // for Compose UI
}
sourceSets.androidMain.dependencies {
implementation("org.openani.mediamp:mediamp-backend-exoplayer:$mediampVersion")
}
sourceSets.jvmMain.dependencies { // Desktop JVM
implementation("org.openani.mediamp:mediamp-backend-vlc:$mediampVersion")
}
}
```

### Gradle Version Catalogs

```toml
[versions]
mediamp = "0.1.0" # Replace with the latest version

[libraries]
mediamp-core = { group = "org.openani.mediamp", module = "mediamp-core", version.ref = "mediamp" }
mediamp-compose = { group = "org.openani.mediamp", module = "mediamp-compose", version.ref = "mediamp" }
mediamp-backend-exoplayer = { group = "org.openani.mediamp", module = "mediamp-backend-exoplayer", version.ref = "mediamp" }
mediamp-backend-vlc = { group = "org.openani.mediamp", module = "mediamp-backend-vlc", version.ref = "mediamp" }
```

```kotlin
kotlin {
sourceSets.commonMain.dependencies {
implementation(libs.mediamp.core) // for data-layer, does not depend on Compose
implementation(libs.mediamp.compose) // for Compose UI
}
sourceSets.androidMain.dependencies {
implementation(libs.mediamp.backend.exoplayer)
}
sourceSets.jvmMain.dependencies { // Desktop JVM
implementation(libs.mediamp.backend.vlc)
}
}
```

# License

MediaMP is licensed under the GNU General Public License v3.0. You can find the full license text in
the `LICENSE` file.

```
MediaMP
Copyright (C) 2024 OpenAni and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
```
MediaMP
Copyright (C) 2024 OpenAni and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
```
5 changes: 0 additions & 5 deletions mediamp-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,8 @@ kotlin {
api(kotlin("test"))
api(libs.kotlinx.coroutines.test)
}
androidMain.dependencies {
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.ui.tooling)
}
getByName("jvmTest").dependencies {
api(libs.junit)
runtimeOnly(libs.junit)
}
desktopMain.dependencies {
}
Expand Down
9 changes: 0 additions & 9 deletions mediamp-api/src/commonMain/kotlin/MediampInternalApi.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@

@file:OptIn(MediampInternalApi::class)

package org.openani.mediamp.core.state
package org.openani.mediamp

import androidx.annotation.UiThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
Expand All @@ -26,9 +25,9 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import org.openani.mediamp.MediampInternalApi
import org.openani.mediamp.core.features.PlayerFeatures
import org.openani.mediamp.core.features.playerFeaturesOf
import org.openani.mediamp.features.PlayerFeatures
import org.openani.mediamp.features.playerFeaturesOf
import org.openani.mediamp.internal.MediampInternalApi
import org.openani.mediamp.metadata.AudioTrack
import org.openani.mediamp.metadata.Chapter
import org.openani.mediamp.metadata.SubtitleTrack
Expand All @@ -43,7 +42,7 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException

/**
* An extensible media player that plays [MediaSource]s.
* An extensible media player that plays [MediaSource]s. Instances can be obtained from a [MediampPlayerFactory].
*
* The [MediampPlayer] interface itself defines only the minimal API for controlling the player, including:
* - Playback State: [playbackState], [videoData], [videoProperties], [currentPositionMillis], [playbackProgress]
Expand All @@ -53,10 +52,10 @@ import kotlin.coroutines.cancellation.CancellationException
*
* ## Additional Features
*
* - [org.openani.mediamp.core.features.AudioLevelController]: Controls the audio volume and mute state.
* - [org.openani.mediamp.core.features.Buffering]: Monitors the buffering progress.
* - [org.openani.mediamp.core.features.PlaybackSpeed]: Controls the playback speed.
* - [org.openani.mediamp.core.features.Screenshots]: Captures screenshots of the video.
* - [org.openani.mediamp.features.AudioLevelController]: Controls the audio volume and mute state.
* - [org.openani.mediamp.features.Buffering]: Monitors the buffering progress.
* - [org.openani.mediamp.features.PlaybackSpeed]: Controls the playback speed.
* - [org.openani.mediamp.features.Screenshots]: Captures screenshots of the video.
*
* To obtain a feature, use the [PlayerFeatures.get] on [features].
*
Expand All @@ -71,18 +70,24 @@ import kotlin.coroutines.cancellation.CancellationException
* On other platforms, calls are not required to be on the main thread but should still be called from a single thread.
* The implementation is guaranteed to be non-blocking and fast so, it is a recommended approach of making all calls from the main thread in common code.
*/
interface MediampPlayer {
public interface MediampPlayer {
/**
* The underlying player implementation.
* It can be cast to the actual player implementation to access additional features that are not yet ported by Mediamp.
*/
public val impl: Any

/**
* A hot flow of the current playback state. Collect on this flow to receive state updates.
*
* States might be changed either by user interaction ([resume]) or by the player itself (e.g. decoder errors).
*/
val playbackState: StateFlow<PlaybackState>
public val playbackState: StateFlow<PlaybackState>

/**
* The video data of the currently playing video.
*/
val videoData: Flow<VideoData?>
public val videoData: Flow<VideoData?>

/**
* Sets the video source to play, by [opening][MediaSource.open] the [source],
Expand All @@ -98,65 +103,64 @@ interface MediampPlayer {
* @see stop
*/ // TODO: 2024/12/22 mention cancellation support, thread safety, errors
@Throws(VideoSourceOpenException::class, CancellationException::class)
suspend fun setVideoSource(source: MediaSource<*>)
public suspend fun setVideoSource(source: MediaSource<*>)

/**
* Properties of the video being played.
*
* Note that it may not be available immediately after [setVideoSource] returns,
* since the properties may be callback from the underlying player implementation.
*/
val videoProperties: StateFlow<VideoProperties?>
public val videoProperties: StateFlow<VideoProperties?>

/**
* Current playback position of the video being played in millis seconds, ranged from `0` to [VideoProperties.durationMillis].
*
* `0` if no video is being played ([videoData] is null).
*/
val currentPositionMillis: StateFlow<Long>
public val currentPositionMillis: StateFlow<Long>

/**
* Obtains the exact current playback position of the video in milliseconds.
*/
@UiThread
fun getExactCurrentPositionMillis(): Long
public fun getExactCurrentPositionMillis(): Long


/**
* A cold flow of the current playback progress, ranged from `0.0` to `1.0`.
*
* There is no guarantee on the frequency of updates, but it should normally be updated at once per second.
*/
val playbackProgress: Flow<Float>
public val playbackProgress: Flow<Float>

/**
* Resumes playback.
*
* If there is no video source set, this function will do nothing.
*/
fun resume()
public fun resume()

/**
* Pauses playback.
*
* If there is no video source set, this function will do nothing.
*/
fun pause()
public fun pause()

/**
* Stops playback, releasing all resources and setting [videoData] to `null`.
* Subsequent calls to [resume] will do nothing.
*
* To play again, call [setVideoSource].
*/
fun stop()
public fun stop()

/**
* Jumps playback to the specified position.
*
* // TODO argument errors?
*/
fun seekTo(positionMillis: Long)
public fun seekTo(positionMillis: Long)

/**
* Skips the current playback position by [deltaMillis].
Expand All @@ -167,22 +171,22 @@ interface MediampPlayer {
*
* // TODO argument errors?
*/
fun skip(deltaMillis: Long) {
public fun skip(deltaMillis: Long) {
seekTo(currentPositionMillis.value + deltaMillis)
}

// TODO: 2024/12/22 extract to feature
val subtitleTracks: TrackGroup<SubtitleTrack>
val audioTracks: TrackGroup<AudioTrack>
val chapters: StateFlow<List<Chapter>>
public val subtitleTracks: TrackGroup<SubtitleTrack>
public val audioTracks: TrackGroup<AudioTrack>
public val chapters: StateFlow<List<Chapter>>

/**
* Additional features that are supported by the underlying player implementation.
*/
val features: PlayerFeatures
public val features: PlayerFeatures
}

fun MediampPlayer.togglePause() {
public fun MediampPlayer.togglePause() {
if (playbackState.value.isPlaying) {
pause()
} else {
Expand All @@ -191,10 +195,10 @@ fun MediampPlayer.togglePause() {
}

@MediampInternalApi
abstract class AbstractMediampPlayer<D : AbstractMediampPlayer.Data>(
public abstract class AbstractMediampPlayer<D : AbstractMediampPlayer.Data>(
parentCoroutineContext: CoroutineContext,
) : MediampPlayer {
protected val backgroundScope = CoroutineScope(
protected val backgroundScope: CoroutineScope = CoroutineScope(
parentCoroutineContext + SupervisorJob(parentCoroutineContext[Job]),
).apply {
coroutineContext.job.invokeOnCompletion {
Expand All @@ -208,12 +212,12 @@ abstract class AbstractMediampPlayer<D : AbstractMediampPlayer.Data>(
* Currently playing resource that should be closed when the controller is closed.
* @see setVideoSource
*/
protected val openResource = MutableStateFlow<D?>(null)
protected val openResource: MutableStateFlow<D?> = MutableStateFlow<D?>(null)

open class Data(
open val mediaSource: MediaSource<*>,
open val videoData: VideoData,
open val releaseResource: () -> Unit,
public open class Data(
public open val mediaSource: MediaSource<*>,
public open val videoData: VideoData,
public open val releaseResource: () -> Unit,
)

final override val videoData: Flow<VideoData?> = openResource.map {
Expand Down Expand Up @@ -260,7 +264,7 @@ abstract class AbstractMediampPlayer<D : AbstractMediampPlayer.Data>(
}


fun closeVideoSource() {
public fun closeVideoSource() {
// TODO: 2024/12/16 proper synchronization?
val value = openResource.value
openResource.value = null
Expand Down Expand Up @@ -288,7 +292,7 @@ abstract class AbstractMediampPlayer<D : AbstractMediampPlayer.Data>(
protected abstract suspend fun openSource(source: MediaSource<*>): D

private val closed = MutableStateFlow(false)
fun close() {
public fun close() {
if (closed.getAndUpdate { true }) return // already closed
closeImpl()
closeVideoSource()
Expand All @@ -299,8 +303,8 @@ abstract class AbstractMediampPlayer<D : AbstractMediampPlayer.Data>(
}


enum class PlaybackState(
val isPlaying: Boolean,
public enum class PlaybackState(
public val isPlaying: Boolean,
) {
/**
* Player is loaded and will be playing as soon as metadata and first frame is available.
Expand Down Expand Up @@ -328,10 +332,11 @@ enum class PlaybackState(
/**
* For previewing
*/
class DummyMediampPlayer(
public class DummyMediampPlayer(
// TODO: 2024/12/22 move to preview package
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : AbstractMediampPlayer<AbstractMediampPlayer.Data>(parentCoroutineContext) {
override val impl: Any get() = this
override val playbackState: MutableStateFlow<PlaybackState> = MutableStateFlow(PlaybackState.PLAYING)
override fun stopImpl() {

Expand Down Expand Up @@ -367,7 +372,7 @@ class DummyMediampPlayer(
durationMillis = 100_000,
),
)
override val currentPositionMillis = MutableStateFlow(10_000L)
override val currentPositionMillis: MutableStateFlow<Long> = MutableStateFlow(10_000L)
override fun getExactCurrentPositionMillis(): Long {
return currentPositionMillis.value
}
Expand Down
Loading

0 comments on commit 19ace54

Please sign in to comment.