From 19ace54d0d8c317d609218b4770ae9f8b4036db2 Mon Sep 17 00:00:00 2001 From: Him188 Date: Sun, 22 Dec 2024 22:36:52 +0000 Subject: [PATCH] Generalize backends --- README.md | 89 ++++++++++++++---- mediamp-api/build.gradle.kts | 5 - .../commonMain/kotlin/MediampInternalApi.kt | 9 -- .../src/commonMain/kotlin}/MediampPlayer.kt | 89 +++++++++--------- .../commonMain/kotlin/MediampPlayerFactory.kt | 51 ++++++++++ .../kotlin}/features/AudioLevelController.kt | 22 ++--- .../commonMain/kotlin}/features/Buffering.kt | 18 ++-- .../commonMain/kotlin}/features/Feature.kt | 10 +- .../kotlin}/features/PlaybackSpeed.kt | 12 +-- .../kotlin}/features/PlayerFeatures.kt | 30 +++--- .../kotlin}/features/Screenshots.kt | 10 +- .../kotlin/internal/MediampInternalApi.kt | 18 ++++ .../commonMain/kotlin}/internal/MonoTasker.kt | 2 +- .../kotlin}/internal/MutableTrackGroup.kt | 5 +- .../kotlin/metadata/VideoProperties.kt | 11 ++- .../kotlin/source/HttpStreamingMediaSource.kt | 2 +- .../src/commonMain/kotlin/source/VideoData.kt | 6 +- .../kotlin/MediampPlayerFactory.ios.kt | 17 ++++ .../kotlin/MediampPlayerFactory.jvm.kt | 34 +++++++ mediamp-backend-exoplayer/build.gradle.kts | 78 +++++++++++++++ .../src/androidTest/kotlin/package.kt | 11 +++ .../src/main/kotlin/ExoPlayerMediampPlayer.kt | 81 ++++++++++------ .../src/main/kotlin/ExoPlayerSurface.kt | 40 ++++++++ .../internal/SeekableInputDataSource.kt | 12 +-- .../src/main/kotlin/package.kt | 10 ++ .../org.openani.mediamp.MediampPlayerFactory | 10 ++ mediamp-backend-vlc/build.gradle.kts | 64 +++++++++++++ .../kotlin}/ComposeMediaPlayerComponent.kt | 16 +++- .../main/kotlin/MediaPlayerSurfaceWithVlc.kt | 30 +----- .../main/kotlin}/SkiaBitmapVideoSurface.kt | 8 +- .../src/main/kotlin}/SkiaVideoSurface.kt | 16 +++- .../src/main/kotlin/VlcVideoMediampPlayer.kt | 43 ++++----- .../io/SeekableInputCallbackMedia.kt | 7 +- .../build.gradle.kts | 10 +- .../androidInstrumentedTest/kotlin/package.kt | 0 .../src/androidMain}/kotlin/package.kt | 0 .../kotlin/compose}/MediaPlayerSurface.kt | 6 +- .../src/commonMain/kotlin/package.kt | 19 ++++ .../src/commonTest/kotlin/package.kt | 10 ++ .../src/desktopMain/kotlin/package.kt | 11 +++ .../kotlin/compose/MediaPlayerSurface.ios.kt | 14 +-- mediamp-compose/src/iosMain/kotlin/package.kt | 11 +++ .../kotlin/compose/MediaPlayerSurface.jvm.kt | 33 +++++++ mediamp-compose/src/jvmMain/kotlin/package.kt | 11 +++ mediamp-compose/src/jvmTest/kotlin/package.kt | 10 ++ ...ch___swipeToSeek_shows_detached_slider.png | Bin .../kotlin/core/VideoPlayer.android.kt | 80 ---------------- .../src/androidMain/kotlin/package.kt | 2 - .../kotlin/core/state/PlayerStateFactory.kt | 23 ----- mediamp-core/src/commonMain/kotlin/package.kt | 10 -- mediamp-core/src/commonTest/kotlin/package.kt | 1 - .../src/desktopMain/kotlin/package.kt | 2 - mediamp-core/src/iosMain/kotlin/package.kt | 2 - mediamp-core/src/jvmTest/kotlin/package.kt | 1 - .../kotlin/source/SystemFileMediaSource.kt | 2 +- settings.gradle.kts | 10 +- 56 files changed, 770 insertions(+), 364 deletions(-) delete mode 100644 mediamp-api/src/commonMain/kotlin/MediampInternalApi.kt rename {mediamp-core/src/commonMain/kotlin/core/state => mediamp-api/src/commonMain/kotlin}/MediampPlayer.kt (82%) create mode 100644 mediamp-api/src/commonMain/kotlin/MediampPlayerFactory.kt rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/features/AudioLevelController.kt (75%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/features/Buffering.kt (79%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/features/Feature.kt (69%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/features/PlaybackSpeed.kt (76%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/features/PlayerFeatures.kt (62%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/features/Screenshots.kt (59%) create mode 100644 mediamp-api/src/commonMain/kotlin/internal/MediampInternalApi.kt rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/internal/MonoTasker.kt (98%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-api/src/commonMain/kotlin}/internal/MutableTrackGroup.kt (90%) create mode 100644 mediamp-api/src/iosMain/kotlin/MediampPlayerFactory.ios.kt create mode 100644 mediamp-api/src/jvmMain/kotlin/MediampPlayerFactory.jvm.kt create mode 100644 mediamp-backend-exoplayer/build.gradle.kts create mode 100644 mediamp-backend-exoplayer/src/androidTest/kotlin/package.kt rename mediamp-core/src/androidMain/kotlin/PlayerState.android.kt => mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerMediampPlayer.kt (89%) create mode 100644 mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerSurface.kt rename mediamp-core/src/androidMain/kotlin/media/VideoDataDataSource.kt => mediamp-backend-exoplayer/src/main/kotlin/internal/SeekableInputDataSource.kt (91%) create mode 100644 mediamp-backend-exoplayer/src/main/kotlin/package.kt create mode 100644 mediamp-backend-exoplayer/src/main/resources/META-INF/services/org.openani.mediamp.MediampPlayerFactory create mode 100644 mediamp-backend-vlc/build.gradle.kts rename {mediamp-core/src/desktopMain/kotlin/core => mediamp-backend-vlc/src/main/kotlin}/ComposeMediaPlayerComponent.kt (96%) rename mediamp-core/src/desktopMain/kotlin/core/MediaPlayer.desktop.kt => mediamp-backend-vlc/src/main/kotlin/MediaPlayerSurfaceWithVlc.kt (74%) rename {mediamp-core/src/desktopMain/kotlin/core => mediamp-backend-vlc/src/main/kotlin}/SkiaBitmapVideoSurface.kt (94%) rename {mediamp-core/src/desktopMain/kotlin/core => mediamp-backend-vlc/src/main/kotlin}/SkiaVideoSurface.kt (92%) rename mediamp-core/src/desktopMain/kotlin/core/VlcjVideoMediampPlayer.kt => mediamp-backend-vlc/src/main/kotlin/VlcVideoMediampPlayer.kt (95%) rename {mediamp-core/src/desktopMain/kotlin => mediamp-backend-vlc/src/main/kotlin/internal}/io/SeekableInputCallbackMedia.kt (81%) rename {mediamp-core => mediamp-compose}/build.gradle.kts (90%) rename {mediamp-core => mediamp-compose}/src/androidInstrumentedTest/kotlin/package.kt (100%) rename {mediamp-core/src/jvmMain => mediamp-compose/src/androidMain}/kotlin/package.kt (100%) rename {mediamp-core/src/commonMain/kotlin/core => mediamp-compose/src/commonMain/kotlin/compose}/MediaPlayerSurface.kt (86%) create mode 100644 mediamp-compose/src/commonMain/kotlin/package.kt create mode 100644 mediamp-compose/src/commonTest/kotlin/package.kt create mode 100644 mediamp-compose/src/desktopMain/kotlin/package.kt rename mediamp-core/src/iosMain/kotlin/core/VideoPlayer.ios.kt => mediamp-compose/src/iosMain/kotlin/compose/MediaPlayerSurface.ios.kt (56%) create mode 100644 mediamp-compose/src/iosMain/kotlin/package.kt create mode 100644 mediamp-compose/src/jvmMain/kotlin/compose/MediaPlayerSurface.jvm.kt create mode 100644 mediamp-compose/src/jvmMain/kotlin/package.kt create mode 100644 mediamp-compose/src/jvmTest/kotlin/package.kt rename {mediamp-core => mediamp-compose}/src/jvmTest/resources/screenshots/EpisodeVideoControllerTest.touch___swipeToSeek_shows_detached_slider.png (100%) delete mode 100644 mediamp-core/src/androidMain/kotlin/core/VideoPlayer.android.kt delete mode 100644 mediamp-core/src/androidMain/kotlin/package.kt delete mode 100644 mediamp-core/src/commonMain/kotlin/core/state/PlayerStateFactory.kt delete mode 100644 mediamp-core/src/commonMain/kotlin/package.kt delete mode 100644 mediamp-core/src/commonTest/kotlin/package.kt delete mode 100644 mediamp-core/src/desktopMain/kotlin/package.kt delete mode 100644 mediamp-core/src/iosMain/kotlin/package.kt delete mode 100644 mediamp-core/src/jvmTest/kotlin/package.kt diff --git a/README.md b/README.md index 4091dca..7123225 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 . ``` - 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 . -``` \ No newline at end of file diff --git a/mediamp-api/build.gradle.kts b/mediamp-api/build.gradle.kts index c33744b..b1fce98 100644 --- a/mediamp-api/build.gradle.kts +++ b/mediamp-api/build.gradle.kts @@ -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 { } diff --git a/mediamp-api/src/commonMain/kotlin/MediampInternalApi.kt b/mediamp-api/src/commonMain/kotlin/MediampInternalApi.kt deleted file mode 100644 index d9c9583..0000000 --- a/mediamp-api/src/commonMain/kotlin/MediampInternalApi.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.openani.mediamp - -/** - * Marks the annotated element as internal API of Mediamp. - * - * An internal API is subject to change without any notice and should not be used by you. - */ -@RequiresOptIn -public annotation class MediampInternalApi() diff --git a/mediamp-core/src/commonMain/kotlin/core/state/MediampPlayer.kt b/mediamp-api/src/commonMain/kotlin/MediampPlayer.kt similarity index 82% rename from mediamp-core/src/commonMain/kotlin/core/state/MediampPlayer.kt rename to mediamp-api/src/commonMain/kotlin/MediampPlayer.kt index f25feba..fea5c2f 100644 --- a/mediamp-core/src/commonMain/kotlin/core/state/MediampPlayer.kt +++ b/mediamp-api/src/commonMain/kotlin/MediampPlayer.kt @@ -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 @@ -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 @@ -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] @@ -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]. * @@ -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 + public val playbackState: StateFlow /** * The video data of the currently playing video. */ - val videoData: Flow + public val videoData: Flow /** * Sets the video source to play, by [opening][MediaSource.open] the [source], @@ -98,7 +103,7 @@ 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. @@ -106,20 +111,19 @@ interface MediampPlayer { * 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 + public val videoProperties: StateFlow /** * 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 + public val currentPositionMillis: StateFlow /** * Obtains the exact current playback position of the video in milliseconds. */ - @UiThread - fun getExactCurrentPositionMillis(): Long + public fun getExactCurrentPositionMillis(): Long /** @@ -127,21 +131,21 @@ interface MediampPlayer { * * There is no guarantee on the frequency of updates, but it should normally be updated at once per second. */ - val playbackProgress: Flow + public val playbackProgress: Flow /** * 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`. @@ -149,14 +153,14 @@ interface MediampPlayer { * * 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]. @@ -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 - val audioTracks: TrackGroup - val chapters: StateFlow> + public val subtitleTracks: TrackGroup + public val audioTracks: TrackGroup + public val chapters: StateFlow> /** * 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 { @@ -191,10 +195,10 @@ fun MediampPlayer.togglePause() { } @MediampInternalApi -abstract class AbstractMediampPlayer( +public abstract class AbstractMediampPlayer( parentCoroutineContext: CoroutineContext, ) : MediampPlayer { - protected val backgroundScope = CoroutineScope( + protected val backgroundScope: CoroutineScope = CoroutineScope( parentCoroutineContext + SupervisorJob(parentCoroutineContext[Job]), ).apply { coroutineContext.job.invokeOnCompletion { @@ -208,12 +212,12 @@ abstract class AbstractMediampPlayer( * Currently playing resource that should be closed when the controller is closed. * @see setVideoSource */ - protected val openResource = MutableStateFlow(null) + protected val openResource: MutableStateFlow = MutableStateFlow(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 = openResource.map { @@ -260,7 +264,7 @@ abstract class AbstractMediampPlayer( } - fun closeVideoSource() { + public fun closeVideoSource() { // TODO: 2024/12/16 proper synchronization? val value = openResource.value openResource.value = null @@ -288,7 +292,7 @@ abstract class AbstractMediampPlayer( 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() @@ -299,8 +303,8 @@ abstract class AbstractMediampPlayer( } -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. @@ -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(parentCoroutineContext) { + override val impl: Any get() = this override val playbackState: MutableStateFlow = MutableStateFlow(PlaybackState.PLAYING) override fun stopImpl() { @@ -367,7 +372,7 @@ class DummyMediampPlayer( durationMillis = 100_000, ), ) - override val currentPositionMillis = MutableStateFlow(10_000L) + override val currentPositionMillis: MutableStateFlow = MutableStateFlow(10_000L) override fun getExactCurrentPositionMillis(): Long { return currentPositionMillis.value } diff --git a/mediamp-api/src/commonMain/kotlin/MediampPlayerFactory.kt b/mediamp-api/src/commonMain/kotlin/MediampPlayerFactory.kt new file mode 100644 index 0000000..e86d497 --- /dev/null +++ b/mediamp-api/src/commonMain/kotlin/MediampPlayerFactory.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Modifier +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KClass + +/** + * Factory interface for creating a [MediampPlayer]. + */ +public interface MediampPlayerFactory { // SPI load on JVM + public val forClass: KClass + + /** + * Creates a new [MediampPlayer]. + * + * @param context the platform context to create the underlying player implementation. + * It is only used by the constructor and not stored. On Android, this must be the `android.content.Context`. On other platforms, this is ignored so it can be any object. + * @param parentCoroutineContext can pass in a [kotlinx.coroutines.Job] so that the player state is bound to the parent coroutine context scope. + */ + public fun create( + context: Any, // Not introducing an expect/actual because this will instead cause complexity + parentCoroutineContext: CoroutineContext = EmptyCoroutineContext + ): T + + @Composable + @NonRestartableComposable + public fun Surface( + mediampPlayer: T, + modifier: Modifier + ) +} + +/** + * Creates a new [MediampPlayer] using the first [MediampPlayerFactory] implementation found on the classpath. + */ +public expect fun MediampPlayer( + context: Any, + parentCoroutineContext: CoroutineContext = EmptyCoroutineContext +): MediampPlayer diff --git a/mediamp-core/src/commonMain/kotlin/core/features/AudioLevelController.kt b/mediamp-api/src/commonMain/kotlin/features/AudioLevelController.kt similarity index 75% rename from mediamp-core/src/commonMain/kotlin/core/features/AudioLevelController.kt rename to mediamp-api/src/commonMain/kotlin/features/AudioLevelController.kt index 80b4251..588bf66 100644 --- a/mediamp-core/src/commonMain/kotlin/core/features/AudioLevelController.kt +++ b/mediamp-api/src/commonMain/kotlin/features/AudioLevelController.kt @@ -7,38 +7,38 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.features +package org.openani.mediamp.features import kotlinx.coroutines.flow.StateFlow /** - * An optional feature of the [org.openani.mediamp.core.state.MediampPlayer] + * An optional feature of the [org.openani.mediamp.core.MediampPlayer] * that allows controlling the output audio volume and mute state. */ -interface AudioLevelController : Feature { +public interface AudioLevelController : Feature { /** * A hot flow of the current volume level in the range of `0.0` to [maxVolume]. * * `1.0` is the original volume level. */ - val volume: StateFlow + public val volume: StateFlow /** * The maximum volume level that is supported by the implementation. Typically `1.0` (original) or `2.0` (i.e. amplification). */ - val maxVolume: Float + public val maxVolume: Float /** * A hot flow of the current mute state, where `true` means the audio is muted. * When the audio is muted, it is not guaranteed that [volume] will emit `0.0`. */ - val isMute: StateFlow + public val isMute: StateFlow /** * Sets the mute state of the audio. * @param mute `true` to mute audio, `false` to unmute. */ - fun setMute(mute: Boolean) + public fun setMute(mute: Boolean) /** * Sets the volume level to [volume]. @@ -46,19 +46,19 @@ interface AudioLevelController : Feature { * Volume will be coerced in the range of `0.0` to [maxVolume], * so a value that is out of range will not cause an exception. */ - fun setVolume(volume: Float) + public fun setVolume(volume: Float) /** * Increases the volume by [value] (default is 0.05). * The resulting volume will be coerced to the range of `0.0` to [maxVolume]. */ - fun volumeUp(value: Float = 0.05f) + public fun volumeUp(value: Float = 0.05f) /** * Decreases the volume by [value] (default is 0.05). * The resulting volume will be coerced to the range of `0.0` to [maxVolume]. */ - fun volumeDown(value: Float = 0.05f) + public fun volumeDown(value: Float = 0.05f) - companion object Key : FeatureKey + public companion object Key : FeatureKey } diff --git a/mediamp-core/src/commonMain/kotlin/core/features/Buffering.kt b/mediamp-api/src/commonMain/kotlin/features/Buffering.kt similarity index 79% rename from mediamp-core/src/commonMain/kotlin/core/features/Buffering.kt rename to mediamp-api/src/commonMain/kotlin/features/Buffering.kt index 47a3b19..83b17fb 100644 --- a/mediamp-core/src/commonMain/kotlin/core/features/Buffering.kt +++ b/mediamp-api/src/commonMain/kotlin/features/Buffering.kt @@ -7,12 +7,12 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.features +package org.openani.mediamp.features import androidx.compose.runtime.Stable import kotlinx.coroutines.flow.Flow -interface Buffering : Feature { +public interface Buffering : Feature { // /** // * 区块列表. 每个区块的宽度由 [Chunk.weight] 决定. // * @@ -34,26 +34,26 @@ interface Buffering : Feature { /** * 是否正在 buffer (暂停视频中) */ - val isBuffering: Flow + public val isBuffering: Flow /** * `0..100` */ - val bufferedPercentage: Flow + public val bufferedPercentage: Flow - companion object Key : FeatureKey + public companion object Key : FeatureKey } // Not stable -interface Chunk { +public interface Chunk { @Stable - val weight: Float // always return the same value + public val weight: Float // always return the same value // This can change, and change will not notify compose state - val state: ChunkState + public val state: ChunkState } -enum class ChunkState { +public enum class ChunkState { /** * 初始状态 */ diff --git a/mediamp-core/src/commonMain/kotlin/core/features/Feature.kt b/mediamp-api/src/commonMain/kotlin/features/Feature.kt similarity index 69% rename from mediamp-core/src/commonMain/kotlin/core/features/Feature.kt rename to mediamp-api/src/commonMain/kotlin/features/Feature.kt index 0838bbf..81a79b6 100644 --- a/mediamp-core/src/commonMain/kotlin/core/features/Feature.kt +++ b/mediamp-api/src/commonMain/kotlin/features/Feature.kt @@ -7,16 +7,16 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.features +package org.openani.mediamp.features /** - * An optional feature of the [org.openani.mediamp.core.state.MediampPlayer]. + * An optional feature of the [org.openani.mediamp.core.MediampPlayer]. * - * Instances can be obtained using [PlayerFeatures.get] from the [org.openani.mediamp.core.state.MediampPlayer.features]. + * Instances can be obtained using [PlayerFeatures.get] from the [org.openani.mediamp.core.MediampPlayer.features]. */ -interface Feature +public interface Feature /** * A typed key for a feature. It is designed to be used with [PlayerFeatures.get] so that no casting or reflection is needed. */ -interface FeatureKey<@Suppress("unused") F : Feature> +public interface FeatureKey<@Suppress("unused") F : Feature> diff --git a/mediamp-core/src/commonMain/kotlin/core/features/PlaybackSpeed.kt b/mediamp-api/src/commonMain/kotlin/features/PlaybackSpeed.kt similarity index 76% rename from mediamp-core/src/commonMain/kotlin/core/features/PlaybackSpeed.kt rename to mediamp-api/src/commonMain/kotlin/features/PlaybackSpeed.kt index 27bbe6f..27b4fff 100644 --- a/mediamp-core/src/commonMain/kotlin/core/features/PlaybackSpeed.kt +++ b/mediamp-api/src/commonMain/kotlin/features/PlaybackSpeed.kt @@ -7,29 +7,29 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.features +package org.openani.mediamp.features import kotlinx.coroutines.flow.Flow -interface PlaybackSpeed : Feature { +public interface PlaybackSpeed : Feature { /** * A cold flow of the current playback speed. `1.0` by default. * * `1.0` is the original speed, `2.0` is double speed, `0.5` is half speed, etc. */ - val valueFlow: Flow + public val valueFlow: Flow /** * The current playback speed. */ - val value: Float + public val value: Float /** * Sets the playback speed to [speed]. * * Playback speed settings will continue to be in effect even if the video has been switched. */ - fun set(speed: Float) + public fun set(speed: Float) - companion object Key : FeatureKey + public companion object Key : FeatureKey } diff --git a/mediamp-core/src/commonMain/kotlin/core/features/PlayerFeatures.kt b/mediamp-api/src/commonMain/kotlin/features/PlayerFeatures.kt similarity index 62% rename from mediamp-core/src/commonMain/kotlin/core/features/PlayerFeatures.kt rename to mediamp-api/src/commonMain/kotlin/features/PlayerFeatures.kt index 80391cd..935ae00 100644 --- a/mediamp-core/src/commonMain/kotlin/core/features/PlayerFeatures.kt +++ b/mediamp-api/src/commonMain/kotlin/features/PlayerFeatures.kt @@ -7,24 +7,24 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.features +package org.openani.mediamp.features /** - * A container of optional features of the [org.openani.mediamp.core.state.MediampPlayer]. + * A container of optional features of the [org.openani.mediamp.core.MediampPlayer]. * @see Feature */ -sealed interface PlayerFeatures { +public sealed interface PlayerFeatures { /** * Obtains the feature instance associated with the given [key], * or `null` if the feature is not available. */ - operator fun get(key: FeatureKey): F? + public operator fun get(key: FeatureKey): F? /** * Obtains the feature instance associated with the given [key], * or throws [UnsupportedFeatureException] if the feature is not available. */ - fun getOrFail(key: FeatureKey): F { + public fun getOrFail(key: FeatureKey): F { return get(key) ?: throw UnsupportedFeatureException(key) } @@ -32,30 +32,36 @@ sealed interface PlayerFeatures { * Checks if the player supports the feature associated with the given [key]. * @return `true` if the feature is available, `false` otherwise. */ - fun supports(key: FeatureKey<*>): Boolean = get(key) != null + public fun supports(key: FeatureKey<*>): Boolean = get(key) != null } -fun playerFeaturesOf(vararg features: Pair, Feature>): PlayerFeatures { + +public fun playerFeaturesOf(vararg features: Pair, Feature>): PlayerFeatures { val map = features.toMap() return MapPlayerFeatures(map) } -inline fun buildPlayerFeatures(builder: PlayerFeaturesBuilder.() -> Unit): PlayerFeatures { +public fun playerFeaturesOf(features: Map, Feature>): PlayerFeatures { + val map = features.toMap() // copy + return MapPlayerFeatures(map) +} + +public inline fun buildPlayerFeatures(builder: PlayerFeaturesBuilder.() -> Unit): PlayerFeatures { return PlayerFeaturesBuilder().apply(builder).build() } -class PlayerFeaturesBuilder @PublishedApi internal constructor() { +public class PlayerFeaturesBuilder @PublishedApi internal constructor() { private val features = mutableMapOf, Feature>() - fun add(key: FeatureKey, feature: F) { + public fun add(key: FeatureKey, feature: F) { features[key] = feature } - fun build(): PlayerFeatures = playerFeaturesOf(*features.toList().toTypedArray()) + public fun build(): PlayerFeatures = playerFeaturesOf(features) } -class UnsupportedFeatureException(key: FeatureKey<*>) : +public class UnsupportedFeatureException(key: FeatureKey<*>) : UnsupportedOperationException("Feature $key is not supported") diff --git a/mediamp-core/src/commonMain/kotlin/core/features/Screenshots.kt b/mediamp-api/src/commonMain/kotlin/features/Screenshots.kt similarity index 59% rename from mediamp-core/src/commonMain/kotlin/core/features/Screenshots.kt rename to mediamp-api/src/commonMain/kotlin/features/Screenshots.kt index 916ca8c..5100dfa 100644 --- a/mediamp-core/src/commonMain/kotlin/core/features/Screenshots.kt +++ b/mediamp-api/src/commonMain/kotlin/features/Screenshots.kt @@ -7,16 +7,16 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.features +package org.openani.mediamp.features /** - * An optional feature of the [org.openani.mediamp.core.state.MediampPlayer] that allows taking screenshots of the current video frame. + * An optional feature of the [org.openani.mediamp.core.MediampPlayer] that allows taking screenshots of the current video frame. */ -interface Screenshots : Feature { +public interface Screenshots : Feature { /** * Take a screenshot of the current video frame and saves it to a file on the system filesystem. */ - suspend fun takeScreenshot(destinationFile: String) + public suspend fun takeScreenshot(destinationFile: String) - companion object Key : FeatureKey + public companion object Key : FeatureKey } diff --git a/mediamp-api/src/commonMain/kotlin/internal/MediampInternalApi.kt b/mediamp-api/src/commonMain/kotlin/internal/MediampInternalApi.kt new file mode 100644 index 0000000..172a6eb --- /dev/null +++ b/mediamp-api/src/commonMain/kotlin/internal/MediampInternalApi.kt @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.internal + +/** + * Marks the annotated element as internal API of Mediamp. + * + * An internal API is subject to change without any notice and should not be used by you. + */ +@RequiresOptIn +public annotation class MediampInternalApi() \ No newline at end of file diff --git a/mediamp-core/src/commonMain/kotlin/core/internal/MonoTasker.kt b/mediamp-api/src/commonMain/kotlin/internal/MonoTasker.kt similarity index 98% rename from mediamp-core/src/commonMain/kotlin/core/internal/MonoTasker.kt rename to mediamp-api/src/commonMain/kotlin/internal/MonoTasker.kt index a7b2729..95d57e8 100644 --- a/mediamp-core/src/commonMain/kotlin/core/internal/MonoTasker.kt +++ b/mediamp-api/src/commonMain/kotlin/internal/MonoTasker.kt @@ -7,7 +7,7 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.internal +package org.openani.mediamp.internal import androidx.compose.runtime.Stable import kotlinx.coroutines.CancellationException diff --git a/mediamp-core/src/commonMain/kotlin/core/internal/MutableTrackGroup.kt b/mediamp-api/src/commonMain/kotlin/internal/MutableTrackGroup.kt similarity index 90% rename from mediamp-core/src/commonMain/kotlin/core/internal/MutableTrackGroup.kt rename to mediamp-api/src/commonMain/kotlin/internal/MutableTrackGroup.kt index 65901cb..ce701b9 100644 --- a/mediamp-core/src/commonMain/kotlin/core/internal/MutableTrackGroup.kt +++ b/mediamp-api/src/commonMain/kotlin/internal/MutableTrackGroup.kt @@ -7,12 +7,13 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core.internal +package org.openani.mediamp.internal import kotlinx.coroutines.flow.MutableStateFlow import org.openani.mediamp.metadata.TrackGroup -internal class MutableTrackGroup internal constructor( +@MediampInternalApi +public class MutableTrackGroup( initialCandidates: List = emptyList(), ) : TrackGroup { override val current: MutableStateFlow = MutableStateFlow(null) diff --git a/mediamp-api/src/commonMain/kotlin/metadata/VideoProperties.kt b/mediamp-api/src/commonMain/kotlin/metadata/VideoProperties.kt index f58d66f..ecc6824 100644 --- a/mediamp-api/src/commonMain/kotlin/metadata/VideoProperties.kt +++ b/mediamp-api/src/commonMain/kotlin/metadata/VideoProperties.kt @@ -1,6 +1,15 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + package org.openani.mediamp.metadata -import org.openani.mediamp.MediampInternalApi +import org.openani.mediamp.internal.MediampInternalApi public class VideoProperties @MediampInternalApi public constructor( public val title: String?, diff --git a/mediamp-api/src/commonMain/kotlin/source/HttpStreamingMediaSource.kt b/mediamp-api/src/commonMain/kotlin/source/HttpStreamingMediaSource.kt index c8d826a..aab58a5 100644 --- a/mediamp-api/src/commonMain/kotlin/source/HttpStreamingMediaSource.kt +++ b/mediamp-api/src/commonMain/kotlin/source/HttpStreamingMediaSource.kt @@ -11,7 +11,7 @@ package org.openani.mediamp.source import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import org.openani.mediamp.MediampInternalApi +import org.openani.mediamp.internal.MediampInternalApi import org.openani.mediamp.io.SeekableInput import kotlin.coroutines.CoroutineContext diff --git a/mediamp-api/src/commonMain/kotlin/source/VideoData.kt b/mediamp-api/src/commonMain/kotlin/source/VideoData.kt index a7905ae..7918385 100644 --- a/mediamp-api/src/commonMain/kotlin/source/VideoData.kt +++ b/mediamp-api/src/commonMain/kotlin/source/VideoData.kt @@ -2,9 +2,9 @@ * Copyright (C) 2024 OpenAni and contributors. * * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. * - * https://github.com/open-ani/ani/blob/main/LICENSE + * https://github.com/open-ani/mediamp/blob/main/LICENSE */ package org.openani.mediamp.source @@ -12,7 +12,7 @@ package org.openani.mediamp.source import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.io.IOException -import org.openani.mediamp.MediampInternalApi +import org.openani.mediamp.internal.MediampInternalApi import org.openani.mediamp.io.SeekableInput import org.openani.mediamp.io.emptySeekableInput import kotlin.coroutines.CoroutineContext diff --git a/mediamp-api/src/iosMain/kotlin/MediampPlayerFactory.ios.kt b/mediamp-api/src/iosMain/kotlin/MediampPlayerFactory.ios.kt new file mode 100644 index 0000000..45f6f07 --- /dev/null +++ b/mediamp-api/src/iosMain/kotlin/MediampPlayerFactory.ios.kt @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + +import kotlin.coroutines.CoroutineContext + +public actual fun MediampPlayer( + context: Any, + parentCoroutineContext: CoroutineContext, +): MediampPlayer = TODO("Not implemented yet.") diff --git a/mediamp-api/src/jvmMain/kotlin/MediampPlayerFactory.jvm.kt b/mediamp-api/src/jvmMain/kotlin/MediampPlayerFactory.jvm.kt new file mode 100644 index 0000000..bad7cd9 --- /dev/null +++ b/mediamp-api/src/jvmMain/kotlin/MediampPlayerFactory.jvm.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + +import org.openani.mediamp.internal.MediampInternalApi +import java.util.ServiceLoader +import kotlin.coroutines.CoroutineContext + +public actual fun MediampPlayer( + context: Any, + parentCoroutineContext: CoroutineContext, +): MediampPlayer = + @OptIn(MediampInternalApi::class) + MediampPlayerFactoryLoader.first() + .create(context, parentCoroutineContext) + +@MediampInternalApi +public object MediampPlayerFactoryLoader { + private val factories = ServiceLoader.load(MediampPlayerFactory::class.java).toList() + + public fun first(): MediampPlayerFactory<*> = factories.firstOrNull() + ?: throw IllegalStateException("No MediampPlayerFactory implementation found on the classpath.") + + public fun getByInstance(mediampPlayer: MediampPlayer): MediampPlayerFactory<*> = factories.find { + it.forClass.isInstance(mediampPlayer) + } ?: throw IllegalStateException("No MediampPlayerFactory implementation found for $mediampPlayer.") +} diff --git a/mediamp-backend-exoplayer/build.gradle.kts b/mediamp-backend-exoplayer/build.gradle.kts new file mode 100644 index 0000000..9eb5053 --- /dev/null +++ b/mediamp-backend-exoplayer/build.gradle.kts @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +import com.vanniktech.maven.publish.AndroidMultiVariantLibrary +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + kotlin("android") + id("com.android.library") + kotlin("plugin.compose") + id("org.jetbrains.compose") + + alias(libs.plugins.vanniktech.mavenPublish) +} + +description = "MediaMP backend using ExoPlayer" + +android { + namespace = "org.openani.mediamp.backend.exoplayer" + compileSdk = property("android.compile.sdk").toString().toInt() + defaultConfig { + minSdk = getIntProperty("android.min.sdk") + } +} + +dependencies { + api(projects.mediampApi) + api(projects.mediampCompose) + implementation(libs.androidx.annotation) + + implementation(libs.androidx.media3.ui) + implementation(libs.androidx.media3.exoplayer) + + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.exoplayer.hls) +} + +mavenPublishing { + configure(AndroidMultiVariantLibrary(true, true, setOf("debug", "release"))) + + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + + signAllPublications() + + pom { + name = "MediaMP Core" + description = "Core library for MediaMP" + url = "https://github.com/open-ani/mediamp" + + licenses { + license { + name = "GNU General Public License, Version 3" + url = "https://github.com/open-ani/mediamp/blob/main/LICENSE" + distribution = "https://www.gnu.org/licenses/gpl-3.0.txt" + } + } + + developers { + developer { + id = "openani" + name = "OpenAni and contributors" + email = "support@openani.org" + } + } + + scm { + connection = "scm:git:https://github.com/open-ani/mediamp.git" + developerConnection = "scm:git:git@github.com:open-ani/mediamp.git" + url = "https://github.com/open-ani/mediamp" + } + } +} \ No newline at end of file diff --git a/mediamp-backend-exoplayer/src/androidTest/kotlin/package.kt b/mediamp-backend-exoplayer/src/androidTest/kotlin/package.kt new file mode 100644 index 0000000..1071d10 --- /dev/null +++ b/mediamp-backend-exoplayer/src/androidTest/kotlin/package.kt @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.backend.exoplayer + diff --git a/mediamp-core/src/androidMain/kotlin/PlayerState.android.kt b/mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerMediampPlayer.kt similarity index 89% rename from mediamp-core/src/androidMain/kotlin/PlayerState.android.kt rename to mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerMediampPlayer.kt index b864ac1..528ac28 100644 --- a/mediamp-core/src/androidMain/kotlin/PlayerState.android.kt +++ b/mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerMediampPlayer.kt @@ -9,7 +9,7 @@ @file:kotlin.OptIn(MediampInternalApi::class) -package org.openani.mediamp +package org.openani.mediamp.backend.exoplayer import android.content.Context import android.net.Uri @@ -17,6 +17,9 @@ import android.util.Pair import androidx.annotation.MainThread import androidx.annotation.OptIn import androidx.annotation.UiThread +import androidx.compose.runtime.Composable +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.ui.Modifier import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException @@ -43,16 +46,16 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.openani.mediamp.core.features.Buffering -import org.openani.mediamp.core.features.PlaybackSpeed -import org.openani.mediamp.core.features.PlayerFeatures -import org.openani.mediamp.core.features.buildPlayerFeatures -import org.openani.mediamp.core.internal.MonoTasker -import org.openani.mediamp.core.internal.MutableTrackGroup -import org.openani.mediamp.core.state.AbstractMediampPlayer -import org.openani.mediamp.core.state.PlaybackState -import org.openani.mediamp.core.state.PlayerStateFactory -import org.openani.mediamp.media.VideoDataDataSource +import org.openani.mediamp.AbstractMediampPlayer +import org.openani.mediamp.MediampPlayerFactory +import org.openani.mediamp.PlaybackState +import org.openani.mediamp.backend.exoplayer.internal.SeekableInputDataSource +import org.openani.mediamp.features.Buffering +import org.openani.mediamp.features.PlaybackSpeed +import org.openani.mediamp.features.PlayerFeatures +import org.openani.mediamp.features.buildPlayerFeatures +import org.openani.mediamp.internal.MediampInternalApi +import org.openani.mediamp.internal.MutableTrackGroup import org.openani.mediamp.metadata.AudioTrack import org.openani.mediamp.metadata.Chapter import org.openani.mediamp.metadata.SubtitleTrack @@ -63,26 +66,51 @@ import org.openani.mediamp.source.MediaSource import org.openani.mediamp.source.VideoData import org.openani.mediamp.source.emptyVideoData import kotlin.coroutines.CoroutineContext +import kotlin.reflect.KClass import kotlin.time.Duration.Companion.seconds import androidx.media3.common.Player as Media3Player -class ExoPlayerStateFactory : PlayerStateFactory { +class ExoPlayerMediampPlayerFactory : MediampPlayerFactory { + override val forClass: KClass get() = ExoPlayerMediampPlayer::class + @OptIn(UnstableApi::class) + @Deprecated( + "Use create(context: Context, parentCoroutineContext: CoroutineContext) instead", + level = DeprecationLevel.HIDDEN, + ) override fun create( + context: Any, + parentCoroutineContext: CoroutineContext + ): ExoPlayerMediampPlayer { + require(context is Context) { "The context argument must be android.content.Context on Android" } + return create(context, parentCoroutineContext) + } + + fun create( context: Context, parentCoroutineContext: CoroutineContext - ): org.openani.mediamp.core.state.MediampPlayer = - ExoPlayerMediampPlayer(context, parentCoroutineContext) + ): ExoPlayerMediampPlayer { + return ExoPlayerMediampPlayer(context, parentCoroutineContext) + } + + @Composable + @NonRestartableComposable + override fun Surface( + mediampPlayer: ExoPlayerMediampPlayer, + modifier: Modifier + ): Unit = ExoPlayerSurface(mediampPlayer, modifier) } +/** + * @see ExoPlayerMediampPlayerFactory + */ @OptIn(UnstableApi::class) @kotlin.OptIn(MediampInternalApi::class) -internal class ExoPlayerMediampPlayer @UiThread constructor( +class ExoPlayerMediampPlayer @UiThread constructor( context: Context, parentCoroutineContext: CoroutineContext, -) : AbstractMediampPlayer(parentCoroutineContext), - AutoCloseable { +) : AbstractMediampPlayer(parentCoroutineContext) { class ExoPlayerData( mediaSource: MediaSource<*>, videoData: VideoData, @@ -145,7 +173,7 @@ internal class ExoPlayerMediampPlayer @UiThread constructor( data.createInput() } val factory = ProgressiveMediaSource.Factory { - VideoDataDataSource(data, file) + SeekableInputDataSource(data, file) } return ExoPlayerData( source, @@ -162,9 +190,7 @@ internal class ExoPlayerMediampPlayer @UiThread constructor( ) } - private val updateVideoPropertiesTasker = MonoTasker(backgroundScope) - - val exoPlayer = kotlin.run { + val exoPlayer = run { ExoPlayer.Builder(context).apply { setTrackSelector( object : DefaultTrackSelector(context) { @@ -278,14 +304,10 @@ internal class ExoPlayerMediampPlayer @UiThread constructor( val duration = duration // 注意, 要把所有 UI 属性全都读出来然后 captured 到 background -- ExoPlayer 所有属性都需要在主线程 - - updateVideoPropertiesTasker.launch(Dispatchers.IO) { - // This is in background - videoProperties.value = VideoProperties( - title = title?.toString(), - durationMillis = duration, - ) - } + videoProperties.value = VideoProperties( + title = title?.toString(), + durationMillis = duration, + ) return true } @@ -331,6 +353,7 @@ internal class ExoPlayerMediampPlayer @UiThread constructor( ) } } + override val impl: ExoPlayer get() = exoPlayer override val videoProperties = MutableStateFlow(null) diff --git a/mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerSurface.kt b/mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerSurface.kt new file mode 100644 index 0000000..9836cc9 --- /dev/null +++ b/mediamp-backend-exoplayer/src/main/kotlin/ExoPlayerSurface.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.backend.exoplayer + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.ui.PlayerView + +@Composable +fun ExoPlayerSurface( + mediampPlayer: ExoPlayerMediampPlayer, + modifier: Modifier = Modifier, + configuration: PlayerView.() -> Unit = {}, +) { + AndroidView( + factory = { context -> + PlayerView(context).apply { + useController = false + this.player = mediampPlayer.exoPlayer + configuration() + } + }, + modifier, + onRelease = { + // TODO: 2024/12/22 release player + }, + update = { view -> + view.player = mediampPlayer.exoPlayer + }, + ) + +} diff --git a/mediamp-core/src/androidMain/kotlin/media/VideoDataDataSource.kt b/mediamp-backend-exoplayer/src/main/kotlin/internal/SeekableInputDataSource.kt similarity index 91% rename from mediamp-core/src/androidMain/kotlin/media/VideoDataDataSource.kt rename to mediamp-backend-exoplayer/src/main/kotlin/internal/SeekableInputDataSource.kt index 773b17a..8b0e9be 100644 --- a/mediamp-core/src/androidMain/kotlin/media/VideoDataDataSource.kt +++ b/mediamp-backend-exoplayer/src/main/kotlin/internal/SeekableInputDataSource.kt @@ -2,20 +2,19 @@ * Copyright (C) 2024 OpenAni and contributors. * * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. * - * https://github.com/open-ani/ani/blob/main/LICENSE + * https://github.com/open-ani/mediamp/blob/main/LICENSE */ @file:androidx.annotation.OptIn(UnstableApi::class) -package org.openani.mediamp.media +package org.openani.mediamp.backend.exoplayer.internal import android.net.Uri import androidx.media3.common.C import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.BaseDataSource -import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSpec import kotlinx.coroutines.runBlocking import org.openani.mediamp.io.SeekableInput @@ -27,12 +26,13 @@ import kotlin.time.measureTimedValue private const val ENABLE_READ_LOG = false private const val ENABLE_TRACE_LOG = false + /** - * Wrap of an Ani [VideoData] into a ExoPlayer [DataSource]. + * Wrap of an Ani [org.openani.mediamp.source.VideoData] into a ExoPlayer [androidx.media3.datasource.DataSource]. * * This class will not close [videoData]. */ -class VideoDataDataSource( +internal class SeekableInputDataSource( private val videoData: VideoData, private val file: SeekableInput, ) : BaseDataSource(true) { diff --git a/mediamp-backend-exoplayer/src/main/kotlin/package.kt b/mediamp-backend-exoplayer/src/main/kotlin/package.kt new file mode 100644 index 0000000..6efc298 --- /dev/null +++ b/mediamp-backend-exoplayer/src/main/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.backend.exoplayer diff --git a/mediamp-backend-exoplayer/src/main/resources/META-INF/services/org.openani.mediamp.MediampPlayerFactory b/mediamp-backend-exoplayer/src/main/resources/META-INF/services/org.openani.mediamp.MediampPlayerFactory new file mode 100644 index 0000000..9480fdf --- /dev/null +++ b/mediamp-backend-exoplayer/src/main/resources/META-INF/services/org.openani.mediamp.MediampPlayerFactory @@ -0,0 +1,10 @@ +# +# Copyright (C) 2024 OpenAni and contributors. +# +# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. +# Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. +# +# https://github.com/open-ani/mediamp/blob/main/LICENSE +# + +org.openani.mediamp.backend.exoplayer.ExoPlayerMediampPlayerFactory \ No newline at end of file diff --git a/mediamp-backend-vlc/build.gradle.kts b/mediamp-backend-vlc/build.gradle.kts new file mode 100644 index 0000000..57845b5 --- /dev/null +++ b/mediamp-backend-vlc/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +import com.vanniktech.maven.publish.JavaLibrary +import com.vanniktech.maven.publish.JavadocJar +import com.vanniktech.maven.publish.SonatypeHost + +plugins { + kotlin("jvm") + kotlin("plugin.compose") + id("org.jetbrains.compose") + + alias(libs.plugins.vanniktech.mavenPublish) +} + +dependencies { + api(projects.mediampApi) + api(projects.mediampCompose) + implementation(libs.vlcj) + implementation(libs.jna) + implementation(libs.jna.platform) +} + +mavenPublishing { + configure(JavaLibrary(JavadocJar.Empty(), true)) + + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + + signAllPublications() + + pom { + name = "MediaMP Core" + description = "Core library for MediaMP" + url = "https://github.com/open-ani/mediamp" + + licenses { + license { + name = "GNU General Public License, Version 3" + url = "https://github.com/open-ani/mediamp/blob/main/LICENSE" + distribution = "https://www.gnu.org/licenses/gpl-3.0.txt" + } + } + + developers { + developer { + id = "openani" + name = "OpenAni and contributors" + email = "support@openani.org" + } + } + + scm { + connection = "scm:git:https://github.com/open-ani/mediamp.git" + developerConnection = "scm:git:git@github.com:open-ani/mediamp.git" + url = "https://github.com/open-ani/mediamp" + } + } +} \ No newline at end of file diff --git a/mediamp-core/src/desktopMain/kotlin/core/ComposeMediaPlayerComponent.kt b/mediamp-backend-vlc/src/main/kotlin/ComposeMediaPlayerComponent.kt similarity index 96% rename from mediamp-core/src/desktopMain/kotlin/core/ComposeMediaPlayerComponent.kt rename to mediamp-backend-vlc/src/main/kotlin/ComposeMediaPlayerComponent.kt index 8761ac1..154d3db 100644 --- a/mediamp-core/src/desktopMain/kotlin/core/ComposeMediaPlayerComponent.kt +++ b/mediamp-backend-vlc/src/main/kotlin/ComposeMediaPlayerComponent.kt @@ -1,4 +1,13 @@ -package org.openani.mediamp.core +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.backend.vlc import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -25,7 +34,6 @@ import java.awt.Graphics2D import java.awt.image.BufferedImage import java.awt.image.DataBufferInt - open class ComposeMediaPlayerComponent @JvmOverloads constructor( mediaPlayerFactory: MediaPlayerFactory? = null, fullScreenStrategy: FullScreenStrategy? = null, @@ -294,7 +302,7 @@ open class ComposeMediaPlayerComponent @JvmOverloads constructor( } } - private var preferredSize: IntSize = IntSize.Zero + private var preferredSize: IntSize = IntSize.Companion.Zero /** * Used when the default buffer format callback is invoked to setup a new video buffer. @@ -362,4 +370,4 @@ open class ComposeMediaPlayerComponent @JvmOverloads constructor( ) } -} +} \ No newline at end of file diff --git a/mediamp-core/src/desktopMain/kotlin/core/MediaPlayer.desktop.kt b/mediamp-backend-vlc/src/main/kotlin/MediaPlayerSurfaceWithVlc.kt similarity index 74% rename from mediamp-core/src/desktopMain/kotlin/core/MediaPlayer.desktop.kt rename to mediamp-backend-vlc/src/main/kotlin/MediaPlayerSurfaceWithVlc.kt index 61dcea2..6c0a4f1 100644 --- a/mediamp-core/src/desktopMain/kotlin/core/MediaPlayer.desktop.kt +++ b/mediamp-backend-vlc/src/main/kotlin/MediaPlayerSurfaceWithVlc.kt @@ -7,45 +7,23 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core +package org.openani.mediamp.backend.vlc import androidx.compose.foundation.Canvas import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -import org.openani.mediamp.core.state.MediampPlayer import kotlin.math.roundToInt @Composable -actual fun MediaPlayerSurface( - mediampPlayer: MediampPlayer, - modifier: Modifier, +fun MediaPlayerSurfaceWithVlc( + mediampPlayer: VlcVideoMediampPlayer, + modifier: Modifier = Modifier, ) { - check(mediampPlayer is VlcjVideoMediampPlayer) - - val mediaPlayer = mediampPlayer.player - val isFullscreen = false - LaunchedEffect(isFullscreen) { - /* - * To be able to access window in the commented code below, - * extend the player composable function from WindowScope. - * See https://github.com/JetBrains/compose-jb/issues/176#issuecomment-812514936 - * and its subsequent comments. - * - * We could also just fullscreen the whole window: - * `window.placement = WindowPlacement.Fullscreen` - * See https://github.com/JetBrains/compose-multiplatform/issues/1489 - */ - // mediaPlayer.fullScreen().strategy(ExclusiveModeFullScreenStrategy(window)) - mediaPlayer.fullScreen().toggle() - } -// DisposableEffect(Unit) { onDispose(mediaPlayer::release) } - val frameSizeCalculator = remember { FrameSizeCalculator() } diff --git a/mediamp-core/src/desktopMain/kotlin/core/SkiaBitmapVideoSurface.kt b/mediamp-backend-vlc/src/main/kotlin/SkiaBitmapVideoSurface.kt similarity index 94% rename from mediamp-core/src/desktopMain/kotlin/core/SkiaBitmapVideoSurface.kt rename to mediamp-backend-vlc/src/main/kotlin/SkiaBitmapVideoSurface.kt index b74890b..64d8b25 100644 --- a/mediamp-core/src/desktopMain/kotlin/core/SkiaBitmapVideoSurface.kt +++ b/mediamp-backend-vlc/src/main/kotlin/SkiaBitmapVideoSurface.kt @@ -2,12 +2,12 @@ * Copyright (C) 2024 OpenAni and contributors. * * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. * - * https://github.com/open-ani/ani/blob/main/LICENSE + * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core +package org.openani.mediamp.backend.vlc import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -18,7 +18,7 @@ import org.jetbrains.skia.Bitmap import org.jetbrains.skia.ColorAlphaType import org.jetbrains.skia.ColorType import org.jetbrains.skia.ImageInfo -import org.openani.mediamp.core.SkiaBitmapVideoSurface.Companion.ALLOWED_DRAW_FRAMES +import org.openani.mediamp.backend.vlc.SkiaBitmapVideoSurface.Companion.ALLOWED_DRAW_FRAMES import uk.co.caprica.vlcj.player.base.MediaPlayer import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurface diff --git a/mediamp-core/src/desktopMain/kotlin/core/SkiaVideoSurface.kt b/mediamp-backend-vlc/src/main/kotlin/SkiaVideoSurface.kt similarity index 92% rename from mediamp-core/src/desktopMain/kotlin/core/SkiaVideoSurface.kt rename to mediamp-backend-vlc/src/main/kotlin/SkiaVideoSurface.kt index 04aa056..ed786eb 100644 --- a/mediamp-core/src/desktopMain/kotlin/core/SkiaVideoSurface.kt +++ b/mediamp-backend-vlc/src/main/kotlin/SkiaVideoSurface.kt @@ -1,4 +1,13 @@ -package org.openani.mediamp.core +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.backend.vlc import com.sun.jna.Pointer import com.sun.jna.ptr.IntByReference @@ -58,13 +67,14 @@ class SkiaVideoSurface( private inner class SetupCallback : libvlc_video_format_cb { override fun format( opaque: PointerByReference, + chroma: PointerByReference, width: IntByReference, height: IntByReference, pitches: PointerByReference, lines: PointerByReference ): Int { - val imageInfo = ImageInfo.makeN32Premul(width.value, height.value) + val imageInfo = ImageInfo.Companion.makeN32Premul(width.value, height.value) bitmap = Bitmap() check( bitmap.allocPixels(imageInfo), @@ -143,4 +153,4 @@ class SkiaVideoSurface( // renderCallback.display(mediaPlayer, nativeBuffers.buffers(), bufferFormat) } } -} +} \ No newline at end of file diff --git a/mediamp-core/src/desktopMain/kotlin/core/VlcjVideoMediampPlayer.kt b/mediamp-backend-vlc/src/main/kotlin/VlcVideoMediampPlayer.kt similarity index 95% rename from mediamp-core/src/desktopMain/kotlin/core/VlcjVideoMediampPlayer.kt rename to mediamp-backend-vlc/src/main/kotlin/VlcVideoMediampPlayer.kt index 3f7e713..2630e67 100644 --- a/mediamp-core/src/desktopMain/kotlin/core/VlcjVideoMediampPlayer.kt +++ b/mediamp-backend-vlc/src/main/kotlin/VlcVideoMediampPlayer.kt @@ -9,7 +9,7 @@ @file:OptIn(MediampInternalApi::class) -package org.openani.mediamp.core +package org.openani.mediamp.backend.vlc import androidx.compose.runtime.Stable import kotlinx.coroutines.Dispatchers @@ -29,19 +29,19 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.openani.mediamp.MediampInternalApi -import org.openani.mediamp.core.VlcjVideoMediampPlayer.VlcjData -import org.openani.mediamp.core.features.AudioLevelController -import org.openani.mediamp.core.features.Buffering -import org.openani.mediamp.core.features.PlaybackSpeed -import org.openani.mediamp.core.features.PlayerFeatures -import org.openani.mediamp.core.features.Screenshots -import org.openani.mediamp.core.features.buildPlayerFeatures -import org.openani.mediamp.core.internal.MutableTrackGroup -import org.openani.mediamp.core.state.AbstractMediampPlayer -import org.openani.mediamp.core.state.MediampPlayer -import org.openani.mediamp.core.state.PlaybackState -import org.openani.mediamp.io.SeekableInputCallbackMedia +import org.openani.mediamp.AbstractMediampPlayer +import org.openani.mediamp.MediampPlayer +import org.openani.mediamp.PlaybackState +import org.openani.mediamp.backend.vlc.VlcVideoMediampPlayer.VlcjData +import org.openani.mediamp.backend.vlc.internal.io.SeekableInputCallbackMedia +import org.openani.mediamp.features.AudioLevelController +import org.openani.mediamp.features.Buffering +import org.openani.mediamp.features.PlaybackSpeed +import org.openani.mediamp.features.PlayerFeatures +import org.openani.mediamp.features.Screenshots +import org.openani.mediamp.features.buildPlayerFeatures +import org.openani.mediamp.internal.MediampInternalApi +import org.openani.mediamp.internal.MutableTrackGroup import org.openani.mediamp.metadata.AudioTrack import org.openani.mediamp.metadata.Chapter import org.openani.mediamp.metadata.SubtitleTrack @@ -73,9 +73,8 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.math.roundToInt - @Stable -class VlcjVideoMediampPlayer(parentCoroutineContext: CoroutineContext) : MediampPlayer, +class VlcVideoMediampPlayer(parentCoroutineContext: CoroutineContext) : MediampPlayer, AbstractMediampPlayer(parentCoroutineContext) { companion object { private val createPlayerLock = ReentrantLock() // 如果同时加载可能会 SIGSEGV @@ -105,6 +104,7 @@ class VlcjVideoMediampPlayer(parentCoroutineContext: CoroutineContext) : Mediamp player.videoSurface().set(this) // 只能 attach 一次 attach(player) } + override val impl: EmbeddedMediaPlayer get() = player override val playbackState: MutableStateFlow = MutableStateFlow(PlaybackState.PAUSED_BUFFERING) private val buffering = VlcBuffering() @@ -159,7 +159,7 @@ class VlcjVideoMediampPlayer(parentCoroutineContext: CoroutineContext) : Mediamp val data = source.open() - val awaitContext = SupervisorJob(backgroundScope.coroutineContext[Job]) + val awaitContext = SupervisorJob(backgroundScope.coroutineContext[Job.Key]) val input = data.createInput() return VlcjData( source, @@ -241,10 +241,10 @@ class VlcjVideoMediampPlayer(parentCoroutineContext: CoroutineContext) : Mediamp private val audioLevelController = VlcAudioLevelController() override val features: PlayerFeatures = buildPlayerFeatures { - add(Screenshots, screenshots) - add(Buffering, buffering) - add(AudioLevelController, audioLevelController) - add(PlaybackSpeed, playbackSpeed) + add(Screenshots.Key, screenshots) + add(Buffering.Key, buffering) + add(AudioLevelController.Key, audioLevelController) + add(PlaybackSpeed.Key, playbackSpeed) } init { @@ -564,6 +564,7 @@ class VlcjVideoMediampPlayer(parentCoroutineContext: CoroutineContext) : Mediamp } } + private object Logger { inline fun trace(message: () -> String) { println("INFO: ${message()}") diff --git a/mediamp-core/src/desktopMain/kotlin/io/SeekableInputCallbackMedia.kt b/mediamp-backend-vlc/src/main/kotlin/internal/io/SeekableInputCallbackMedia.kt similarity index 81% rename from mediamp-core/src/desktopMain/kotlin/io/SeekableInputCallbackMedia.kt rename to mediamp-backend-vlc/src/main/kotlin/internal/io/SeekableInputCallbackMedia.kt index 7f923b4..d4018ad 100644 --- a/mediamp-core/src/desktopMain/kotlin/io/SeekableInputCallbackMedia.kt +++ b/mediamp-backend-vlc/src/main/kotlin/internal/io/SeekableInputCallbackMedia.kt @@ -2,13 +2,14 @@ * Copyright (C) 2024 OpenAni and contributors. * * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. * - * https://github.com/open-ani/ani/blob/main/LICENSE + * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.io +package org.openani.mediamp.backend.vlc.internal.io +import org.openani.mediamp.io.SeekableInput import uk.co.caprica.vlcj.media.callback.DefaultCallbackMedia internal class SeekableInputCallbackMedia( diff --git a/mediamp-core/build.gradle.kts b/mediamp-compose/build.gradle.kts similarity index 90% rename from mediamp-core/build.gradle.kts rename to mediamp-compose/build.gradle.kts index edb198b..39fc788 100644 --- a/mediamp-core/build.gradle.kts +++ b/mediamp-compose/build.gradle.kts @@ -1,4 +1,12 @@ -import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + import com.vanniktech.maven.publish.JavadocJar import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.SonatypeHost diff --git a/mediamp-core/src/androidInstrumentedTest/kotlin/package.kt b/mediamp-compose/src/androidInstrumentedTest/kotlin/package.kt similarity index 100% rename from mediamp-core/src/androidInstrumentedTest/kotlin/package.kt rename to mediamp-compose/src/androidInstrumentedTest/kotlin/package.kt diff --git a/mediamp-core/src/jvmMain/kotlin/package.kt b/mediamp-compose/src/androidMain/kotlin/package.kt similarity index 100% rename from mediamp-core/src/jvmMain/kotlin/package.kt rename to mediamp-compose/src/androidMain/kotlin/package.kt diff --git a/mediamp-core/src/commonMain/kotlin/core/MediaPlayerSurface.kt b/mediamp-compose/src/commonMain/kotlin/compose/MediaPlayerSurface.kt similarity index 86% rename from mediamp-core/src/commonMain/kotlin/core/MediaPlayerSurface.kt rename to mediamp-compose/src/commonMain/kotlin/compose/MediaPlayerSurface.kt index 83a38fd..d42706d 100644 --- a/mediamp-core/src/commonMain/kotlin/core/MediaPlayerSurface.kt +++ b/mediamp-compose/src/commonMain/kotlin/compose/MediaPlayerSurface.kt @@ -7,15 +7,15 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core +package org.openani.mediamp.compose import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import org.openani.mediamp.core.state.MediampPlayer +import org.openani.mediamp.MediampPlayer /** - * Displays the media content (e.g. a video) of [MediampPlayer]. + * Displays the media content (e.g. a video) of [org.openani.mediamp.MediampPlayer]. * * There is no control bar or any other UI elements, it's instead, similar to a [androidx.compose.ui.graphics.Canvas]. * diff --git a/mediamp-compose/src/commonMain/kotlin/package.kt b/mediamp-compose/src/commonMain/kotlin/package.kt new file mode 100644 index 0000000..d23c065 --- /dev/null +++ b/mediamp-compose/src/commonMain/kotlin/package.kt @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun Common(modifier: Modifier = Modifier) { + Text("1") +} \ No newline at end of file diff --git a/mediamp-compose/src/commonTest/kotlin/package.kt b/mediamp-compose/src/commonTest/kotlin/package.kt new file mode 100644 index 0000000..71dc8e3 --- /dev/null +++ b/mediamp-compose/src/commonTest/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp \ No newline at end of file diff --git a/mediamp-compose/src/desktopMain/kotlin/package.kt b/mediamp-compose/src/desktopMain/kotlin/package.kt new file mode 100644 index 0000000..f45db7c --- /dev/null +++ b/mediamp-compose/src/desktopMain/kotlin/package.kt @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + diff --git a/mediamp-core/src/iosMain/kotlin/core/VideoPlayer.ios.kt b/mediamp-compose/src/iosMain/kotlin/compose/MediaPlayerSurface.ios.kt similarity index 56% rename from mediamp-core/src/iosMain/kotlin/core/VideoPlayer.ios.kt rename to mediamp-compose/src/iosMain/kotlin/compose/MediaPlayerSurface.ios.kt index bd8c062..7b86134 100644 --- a/mediamp-core/src/iosMain/kotlin/core/VideoPlayer.ios.kt +++ b/mediamp-compose/src/iosMain/kotlin/compose/MediaPlayerSurface.ios.kt @@ -7,22 +7,16 @@ * https://github.com/open-ani/mediamp/blob/main/LICENSE */ -package org.openani.mediamp.core +package org.openani.mediamp.compose import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import org.openani.mediamp.core.state.MediampPlayer +import org.openani.mediamp.MediampPlayer -/** - * Displays a video player itself. There is no control bar or any other UI elements. - * - * The size of the video player is undefined by default. It may take the entire screen or vise versa. - * Please apply a size [Modifier] to control the size of the video player. - */ @Composable actual fun MediaPlayerSurface( mediampPlayer: MediampPlayer, - modifier: Modifier + modifier: Modifier, ) { - // TODO IOS VideoPlayer + TODO("Not yet implemented") } \ No newline at end of file diff --git a/mediamp-compose/src/iosMain/kotlin/package.kt b/mediamp-compose/src/iosMain/kotlin/package.kt new file mode 100644 index 0000000..f45db7c --- /dev/null +++ b/mediamp-compose/src/iosMain/kotlin/package.kt @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + diff --git a/mediamp-compose/src/jvmMain/kotlin/compose/MediaPlayerSurface.jvm.kt b/mediamp-compose/src/jvmMain/kotlin/compose/MediaPlayerSurface.jvm.kt new file mode 100644 index 0000000..5fd4d61 --- /dev/null +++ b/mediamp-compose/src/jvmMain/kotlin/compose/MediaPlayerSurface.jvm.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import org.openani.mediamp.MediampPlayer +import org.openani.mediamp.MediampPlayerFactory +import org.openani.mediamp.MediampPlayerFactoryLoader +import org.openani.mediamp.internal.MediampInternalApi + +@OptIn(MediampInternalApi::class) +@Composable +actual fun MediaPlayerSurface( + mediampPlayer: MediampPlayer, + modifier: Modifier +) { + val factory = remember(mediampPlayer) { + @Suppress("UNCHECKED_CAST") + MediampPlayerFactoryLoader.getByInstance(mediampPlayer) + as MediampPlayerFactory + } + + factory.Surface(mediampPlayer, modifier) +} diff --git a/mediamp-compose/src/jvmMain/kotlin/package.kt b/mediamp-compose/src/jvmMain/kotlin/package.kt new file mode 100644 index 0000000..f45db7c --- /dev/null +++ b/mediamp-compose/src/jvmMain/kotlin/package.kt @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp + diff --git a/mediamp-compose/src/jvmTest/kotlin/package.kt b/mediamp-compose/src/jvmTest/kotlin/package.kt new file mode 100644 index 0000000..ddfce8d --- /dev/null +++ b/mediamp-compose/src/jvmTest/kotlin/package.kt @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2024 OpenAni and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. + * + * https://github.com/open-ani/mediamp/blob/main/LICENSE + */ + +package org.openani.mediamp diff --git a/mediamp-core/src/jvmTest/resources/screenshots/EpisodeVideoControllerTest.touch___swipeToSeek_shows_detached_slider.png b/mediamp-compose/src/jvmTest/resources/screenshots/EpisodeVideoControllerTest.touch___swipeToSeek_shows_detached_slider.png similarity index 100% rename from mediamp-core/src/jvmTest/resources/screenshots/EpisodeVideoControllerTest.touch___swipeToSeek_shows_detached_slider.png rename to mediamp-compose/src/jvmTest/resources/screenshots/EpisodeVideoControllerTest.touch___swipeToSeek_shows_detached_slider.png diff --git a/mediamp-core/src/androidMain/kotlin/core/VideoPlayer.android.kt b/mediamp-core/src/androidMain/kotlin/core/VideoPlayer.android.kt deleted file mode 100644 index 071c37d..0000000 --- a/mediamp-core/src/androidMain/kotlin/core/VideoPlayer.android.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2024 OpenAni and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. - * - * https://github.com/open-ani/mediamp/blob/main/LICENSE - */ - -@file:androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) - -package org.openani.mediamp.core - -import android.graphics.Color -import android.graphics.Typeface -import android.view.View -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import androidx.media3.ui.CaptionStyleCompat -import androidx.media3.ui.PlayerView -import androidx.media3.ui.PlayerView.ControllerVisibilityListener -import org.openani.mediamp.ExoPlayerMediampPlayer -import org.openani.mediamp.core.state.MediampPlayer - -@Composable -actual fun MediaPlayerSurface( - mediampPlayer: MediampPlayer, - modifier: Modifier -) = MediaPlayerSurface(mediampPlayer, modifier, configuration = {}) - -@Composable -fun MediaPlayerSurface( - mediampPlayer: MediampPlayer, - modifier: Modifier = Modifier, - configuration: PlayerView.() -> Unit = {}, -) { - AndroidView( - factory = { context -> - PlayerView(context).apply { - val videoView = this - controllerAutoShow = false - useController = false - controllerHideOnTouch = false - subtitleView?.apply { - this.setStyle( - CaptionStyleCompat( - Color.WHITE, - 0x000000FF, - 0x00000000, - CaptionStyleCompat.EDGE_TYPE_OUTLINE, - Color.BLACK, - Typeface.DEFAULT, - ), - ) - } - (mediampPlayer as? ExoPlayerMediampPlayer)?.let { - this.player = it.exoPlayer - setControllerVisibilityListener( - ControllerVisibilityListener { visibility -> - if (visibility == View.VISIBLE) { - videoView.hideController() - } - }, - ) - } - configuration() - } - }, - modifier, - onRelease = { - // TODO: 2024/12/22 release player - }, - update = { view -> - (mediampPlayer as? ExoPlayerMediampPlayer)?.let { - view.player = it.exoPlayer - } - }, - ) -} diff --git a/mediamp-core/src/androidMain/kotlin/package.kt b/mediamp-core/src/androidMain/kotlin/package.kt deleted file mode 100644 index d503958..0000000 --- a/mediamp-core/src/androidMain/kotlin/package.kt +++ /dev/null @@ -1,2 +0,0 @@ -package org.openani.mediamp - diff --git a/mediamp-core/src/commonMain/kotlin/core/state/PlayerStateFactory.kt b/mediamp-core/src/commonMain/kotlin/core/state/PlayerStateFactory.kt deleted file mode 100644 index f5e0f1c..0000000 --- a/mediamp-core/src/commonMain/kotlin/core/state/PlayerStateFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (C) 2024 OpenAni and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. - * - * https://github.com/open-ani/mediamp/blob/main/LICENSE - */ - -package org.openani.mediamp.core.state - -import kotlin.coroutines.CoroutineContext - -fun interface PlayerStateFactory { - /** - * Creates a new [MediampPlayer] - * [parentCoroutineContext] must have a [kotlinx.coroutines.Job] so that the player state is bound to the parent coroutine context scope. - * - * @param context the platform context to create the underlying player implementation. - * It is only used by the constructor and not stored. - */ - fun create(context: C, parentCoroutineContext: CoroutineContext): MediampPlayer -} diff --git a/mediamp-core/src/commonMain/kotlin/package.kt b/mediamp-core/src/commonMain/kotlin/package.kt deleted file mode 100644 index d50a3f3..0000000 --- a/mediamp-core/src/commonMain/kotlin/package.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.openani.mediamp - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun Common(modifier: Modifier = Modifier) { - Text("1") -} \ No newline at end of file diff --git a/mediamp-core/src/commonTest/kotlin/package.kt b/mediamp-core/src/commonTest/kotlin/package.kt deleted file mode 100644 index e530ee3..0000000 --- a/mediamp-core/src/commonTest/kotlin/package.kt +++ /dev/null @@ -1 +0,0 @@ -package org.openani.mediamp \ No newline at end of file diff --git a/mediamp-core/src/desktopMain/kotlin/package.kt b/mediamp-core/src/desktopMain/kotlin/package.kt deleted file mode 100644 index d503958..0000000 --- a/mediamp-core/src/desktopMain/kotlin/package.kt +++ /dev/null @@ -1,2 +0,0 @@ -package org.openani.mediamp - diff --git a/mediamp-core/src/iosMain/kotlin/package.kt b/mediamp-core/src/iosMain/kotlin/package.kt deleted file mode 100644 index d503958..0000000 --- a/mediamp-core/src/iosMain/kotlin/package.kt +++ /dev/null @@ -1,2 +0,0 @@ -package org.openani.mediamp - diff --git a/mediamp-core/src/jvmTest/kotlin/package.kt b/mediamp-core/src/jvmTest/kotlin/package.kt deleted file mode 100644 index e35747e..0000000 --- a/mediamp-core/src/jvmTest/kotlin/package.kt +++ /dev/null @@ -1 +0,0 @@ -package org.openani.mediamp diff --git a/mediamp-source-kotlinx-io/src/commonMain/kotlin/source/SystemFileMediaSource.kt b/mediamp-source-kotlinx-io/src/commonMain/kotlin/source/SystemFileMediaSource.kt index d74df6f..dd037d9 100644 --- a/mediamp-source-kotlinx-io/src/commonMain/kotlin/source/SystemFileMediaSource.kt +++ b/mediamp-source-kotlinx-io/src/commonMain/kotlin/source/SystemFileMediaSource.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.withContext import kotlinx.io.IOException import kotlinx.io.files.Path import kotlinx.io.files.SystemFileSystem -import org.openani.mediamp.MediampInternalApi +import org.openani.mediamp.internal.MediampInternalApi import org.openani.mediamp.io.SeekableInput import org.openani.mediamp.io.SystemFileSeekableInput import kotlin.coroutines.CoroutineContext diff --git a/settings.gradle.kts b/settings.gradle.kts index 0c27cb5..3339803 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,9 +2,9 @@ * Copyright (C) 2024 OpenAni and contributors. * * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link. + * Use of this source code is governed by the Apache-2.0 license, which can be found at the following link. * - * https://github.com/open-ani/ani/blob/main/LICENSE + * https://github.com/open-ani/mediamp/blob/main/LICENSE */ rootProject.name = "mediamp" @@ -23,7 +23,11 @@ plugins { } include(":mediamp-api") -include(":mediamp-core") +include(":mediamp-compose") + +include(":mediamp-backend-vlc") +include(":mediamp-backend-exoplayer") + //include(":mediamp-preview") include(":mediamp-source-kotlinx-io")