diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8607abd88..6aeb611d7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -42,3 +42,6 @@ jobs: ./gradlew clean assembleDefaultDebug testDefaultDebugUnitTest --no-daemon apks=$(find app/build/outputs \( -name '*.apk' -or -name '*.aab' \) -print0 | tr '\0' ',' | sed 's/,$//') echo "apks=$apks" >> "$GITHUB_OUTPUT" + env: + ORG_GRADLE_PROJECT_WholphinExtensionsUsername: "${{ secrets.EXTENSIONS_USERNAME }}" + ORG_GRADLE_PROJECT_WholphinExtensionsPassword: "${{ secrets.EXTENSIONS_PASSWORD }}" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7f5bbcecf..952d230e5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,12 +20,21 @@ plugins { alias(libs.plugins.openapi.generator) } -val isCI = if (System.getenv("CI") != null) System.getenv("CI").toBoolean() else false -val shouldSign = isCI && System.getenv("KEY_ALIAS") != null -val ffmpegModuleExists = project.file("libs/lib-decoder-ffmpeg-release.aar").exists() -val av1ModuleExists = project.file("libs/lib-decoder-av1-release.aar").exists() -val mpvModuleExists = project.file("libs/wholphin-mpv-release.aar").exists() -val extensionsRepoActive = project.hasProperty("WholphinExtensionsUsername") +val isCI = providers.environmentVariable("CI").orElse("false").map { it.toBoolean() } +val shouldSign = + isCI.zip( + providers.environmentVariable("KEY_ALIAS").orElse("").map { it.isNotBlank() }, + ) { isCI, hasKey -> + isCI && hasKey + } +val ffmpegModuleExists = + providers.provider { project.file("libs/lib-decoder-ffmpeg-release.aar").exists() } +val av1ModuleExists = + providers.provider { project.file("libs/lib-decoder-av1-release.aar").exists() } +val mpvModuleExists = + providers.provider { project.file("libs/wholphin-mpv-release.aar").exists() } +val extensionsRepoActive = + providers.provider { project.hasProperty("WholphinExtensionsUsername") } val gitTags = providers @@ -63,7 +72,7 @@ configure { } signingConfigs { - if (shouldSign) { + if (shouldSign.get()) { create("ci") { file("ci.keystore").writeBytes( Base64.getDecoder().decode(System.getenv("SIGNING_KEY")), @@ -88,7 +97,7 @@ configure { "proguard-rules.pro", ) isDebuggable = false - if (shouldSign) { + if (shouldSign.get()) { signingConfig = signingConfigs.getByName("ci") } else { val localPropertiesFile = project.rootProject.file("local.properties") @@ -346,28 +355,28 @@ dependencies { debugImplementation(libs.androidx.compose.ui.test.manifest) coreLibraryDesugaring(libs.desugar.jdk.libs) - if (ffmpegModuleExists) { + if (ffmpegModuleExists.get()) { logger.info("Using local ffmpeg decoder") implementation(files("libs/lib-decoder-ffmpeg-release.aar")) - } else if (extensionsRepoActive) { + } else if (extensionsRepoActive.get()) { logger.info("Using prebuilt ffmpeg decoder") implementation(libs.wholphin.extensions.ffmpeg) } else { logger.warn("Media3 ffmpeg decoder was NOT found") } - if (av1ModuleExists) { + if (av1ModuleExists.get()) { logger.info("Using local av1 decoder") implementation(files("libs/lib-decoder-av1-release.aar")) - } else if (extensionsRepoActive) { + } else if (extensionsRepoActive.get()) { logger.info("Using prebuilt av1 decoder") implementation(libs.wholphin.extensions.av1) } else { logger.warn("Media3 av1 decoder was NOT found") } - if (mpvModuleExists) { + if (mpvModuleExists.get()) { logger.info("Using local libMPV build") implementation(files("libs/wholphin-mpv-release.aar")) - } else if (extensionsRepoActive) { + } else if (extensionsRepoActive.get()) { logger.info("Using prebuilt libMPV") implementation(libs.wholphin.extensions.mpv) } else { diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index da00fd3d4..1977f9d4b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -24,12 +24,12 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.video.MediaCodecVideoRenderer import androidx.media3.exoplayer.video.VideoRendererEventListener import androidx.media3.extractor.DefaultExtractorsFactory +import com.github.damontecres.wholphin.mpv.MpvPlayer import com.github.damontecres.wholphin.preferences.AssPlaybackMode import com.github.damontecres.wholphin.preferences.MediaExtensionStatus import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient -import com.github.damontecres.wholphin.util.mpv.MpvPlayer import dagger.hilt.android.qualifiers.ApplicationContext import io.github.peerless2012.ass.media.AssHandler import io.github.peerless2012.ass.media.factory.AssRenderersFactory diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index d3c858257..a8d06d0bf 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -68,6 +68,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist +import com.github.damontecres.wholphin.mpv.MpvPlayer import com.github.damontecres.wholphin.preferences.AssPlaybackMode import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.UserPreferences @@ -91,7 +92,6 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.Media3SubtitleOverride -import com.github.damontecres.wholphin.util.mpv.MpvPlayer import io.github.peerless2012.ass.media.widget.AssSubtitleView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt index b7f3d7645..0411c5bab 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.unit.sp import androidx.media3.common.util.UnstableApi import androidx.media3.ui.CaptionStyleCompat import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.mpv.MPVLib import com.github.damontecres.wholphin.preferences.AppChoicePreference import com.github.damontecres.wholphin.preferences.AppClickablePreference import com.github.damontecres.wholphin.preferences.AppDestinationPreference @@ -24,7 +25,6 @@ import com.github.damontecres.wholphin.preferences.SubtitlePreferences import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup -import com.github.damontecres.wholphin.util.mpv.MPVLib import com.github.damontecres.wholphin.util.mpv.setPropertyColor import timber.log.Timber diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MPVLib.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MPVLib.kt deleted file mode 100644 index fad62f2bd..000000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MPVLib.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.github.damontecres.wholphin.util.mpv - -/* -This file is copied from https://github.com/mpv-android/mpv-android/blob/master/app/src/main/java/is/xyz/mpv/MPVLib.kt - -Copyright (c) 2016 Ilya Zhuravlev -Copyright (c) 2016 sfan5 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, publish, distribute, -sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT -NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -import android.content.Context -import android.graphics.Bitmap -import android.view.Surface -import java.util.concurrent.CopyOnWriteArrayList - -// Wrapper for native library - -@Suppress("unused") -object MPVLib { - init { - val libs = arrayOf("mpv", "player") - for (lib in libs) { - System.loadLibrary(lib) - } - } - - external fun create(appctx: Context) - - external fun init() - - external fun destroy() - - external fun attachSurface(surface: Surface) - - external fun detachSurface() - - external fun command(cmd: Array) - - external fun setOptionString( - name: String, - value: String, - ): Int - - external fun grabThumbnail(dimension: Int): Bitmap? - - external fun getPropertyInt(property: String): Int? - - external fun setPropertyInt( - property: String, - value: Int, - ) - - external fun getPropertyDouble(property: String): Double? - - external fun setPropertyDouble( - property: String, - value: Double, - ) - - external fun getPropertyBoolean(property: String): Boolean? - - external fun setPropertyBoolean( - property: String, - value: Boolean, - ) - - external fun getPropertyString(property: String): String? - - external fun setPropertyString( - property: String, - value: String, - ) - - external fun observeProperty( - property: String, - format: Int, - ) - - private val observers = CopyOnWriteArrayList() - - @JvmStatic - fun addObserver(o: EventObserver) { - observers.add(o) - } - - @JvmStatic - fun removeObserver(o: EventObserver) { - observers.remove(o) - } - - @JvmStatic - fun eventProperty( - property: String, - value: Long, - ) { - for (o in observers) { - o.eventProperty(property, value) - } - } - - @JvmStatic - fun eventProperty( - property: String, - value: Boolean, - ) { - for (o in observers) { - o.eventProperty(property, value) - } - } - - @JvmStatic - fun eventProperty( - property: String, - value: Double, - ) { - for (o in observers) { - o.eventProperty(property, value) - } - } - - @JvmStatic - fun eventProperty( - property: String, - value: String, - ) { - for (o in observers) { - o.eventProperty(property, value) - } - } - - @JvmStatic - fun eventProperty(property: String) { - for (o in observers) { - o.eventProperty(property) - } - } - - @JvmStatic - fun event(eventId: Int) { - for (o in observers) { - o.event(eventId) - } - } - - @JvmStatic - fun eventEndFile( - reason: Int, - error: Int, - ) { - for (o in observers) { - o.eventEndFile(reason, error) - } - } - - private val log_observers = CopyOnWriteArrayList() - - @JvmStatic - fun addLogObserver(o: LogObserver) { - log_observers.add(o) - } - - @JvmStatic - fun removeLogObserver(o: LogObserver) { - log_observers.remove(o) - } - - @JvmStatic - fun logMessage( - prefix: String, - level: Int, - text: String, - ) { - for (o in log_observers) { - o.logMessage(prefix, level, text) - } - } - - interface EventObserver { - fun eventProperty(property: String) - - fun eventProperty( - property: String, - value: Long, - ) - - fun eventProperty( - property: String, - value: Boolean, - ) - - fun eventProperty( - property: String, - value: String, - ) - - fun eventProperty( - property: String, - value: Double, - ) - - fun event(eventId: Int) - - fun eventEndFile( - reason: Int, - error: Int, - ) - } - - interface LogObserver { - fun logMessage( - prefix: String, - level: Int, - text: String, - ) - } - - object MpvFormat { - const val MPV_FORMAT_NONE: Int = 0 - const val MPV_FORMAT_STRING: Int = 1 - const val MPV_FORMAT_OSD_STRING: Int = 2 - const val MPV_FORMAT_FLAG: Int = 3 - const val MPV_FORMAT_INT64: Int = 4 - const val MPV_FORMAT_DOUBLE: Int = 5 - const val MPV_FORMAT_NODE: Int = 6 - const val MPV_FORMAT_NODE_ARRAY: Int = 7 - const val MPV_FORMAT_NODE_MAP: Int = 8 - const val MPV_FORMAT_BYTE_ARRAY: Int = 9 - } - - object MpvEvent { - const val MPV_EVENT_NONE: Int = 0 - const val MPV_EVENT_SHUTDOWN: Int = 1 - const val MPV_EVENT_LOG_MESSAGE: Int = 2 - const val MPV_EVENT_GET_PROPERTY_REPLY: Int = 3 - const val MPV_EVENT_SET_PROPERTY_REPLY: Int = 4 - const val MPV_EVENT_COMMAND_REPLY: Int = 5 - const val MPV_EVENT_START_FILE: Int = 6 - const val MPV_EVENT_END_FILE: Int = 7 - const val MPV_EVENT_FILE_LOADED: Int = 8 - - @Deprecated("") - const val MPV_EVENT_IDLE: Int = 11 - - @Deprecated("") - const val MPV_EVENT_TICK: Int = 14 - const val MPV_EVENT_CLIENT_MESSAGE: Int = 16 - const val MPV_EVENT_VIDEO_RECONFIG: Int = 17 - const val MPV_EVENT_AUDIO_RECONFIG: Int = 18 - const val MPV_EVENT_SEEK: Int = 20 - const val MPV_EVENT_PLAYBACK_RESTART: Int = 21 - const val MPV_EVENT_PROPERTY_CHANGE: Int = 22 - const val MPV_EVENT_QUEUE_OVERFLOW: Int = 24 - const val MPV_EVENT_HOOK: Int = 25 - } - - object MpvLogLevel { - const val MPV_LOG_LEVEL_NONE: Int = 0 - const val MPV_LOG_LEVEL_FATAL: Int = 10 - const val MPV_LOG_LEVEL_ERROR: Int = 20 - const val MPV_LOG_LEVEL_WARN: Int = 30 - const val MPV_LOG_LEVEL_INFO: Int = 40 - const val MPV_LOG_LEVEL_V: Int = 50 - const val MPV_LOG_LEVEL_DEBUG: Int = 60 - const val MPV_LOG_LEVEL_TRACE: Int = 70 - } - - object MpvEndFileReason { - const val MPV_END_FILE_REASON_EOF: Int = 0 - const val MPV_END_FILE_REASON_STOP: Int = 2 - const val MPV_END_FILE_REASON_QUIT: Int = 3 - const val MPV_END_FILE_REASON_ERROR: Int = 4 - const val MPV_END_FILE_REASON_REDIRECT: Int = 5 - } - - object MpvError { - const val MPV_ERROR_SUCCESS = 0 - const val MPV_ERROR_EVENT_QUEUE_FULL = -1 - const val MPV_ERROR_NOMEM = -2 - const val MPV_ERROR_UNINITIALIZED = -3 - const val MPV_ERROR_INVALID_PARAMETER = -4 - const val MPV_ERROR_OPTION_NOT_FOUND = -5 - const val MPV_ERROR_OPTION_FORMAT = -6 - const val MPV_ERROR_OPTION_ERROR = -7 - const val MPV_ERROR_PROPERTY_NOT_FOUND = -8 - const val MPV_ERROR_PROPERTY_FORMAT = -9 - const val MPV_ERROR_PROPERTY_UNAVAILABLE = -10 - const val MPV_ERROR_PROPERTY_ERROR = -11 - const val MPV_ERROR_COMMAND = -12 - const val MPV_ERROR_LOADING_FAILED = -13 - const val MPV_ERROR_AO_INIT_FAILED = -14 - const val MPV_ERROR_VO_INIT_FAILED = -15 - const val MPV_ERROR_NOTHING_TO_PLAY = -16 - const val MPV_ERROR_UNKNOWN_FORMAT = -17 - const val MPV_ERROR_UNSUPPORTED = -18 - const val MPV_ERROR_NOT_IMPLEMENTED = -19 - const val MPV_ERROR_GENERIC = -20 - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MPVProperty.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MPVProperty.kt deleted file mode 100644 index f9a0669ab..000000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MPVProperty.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.damontecres.wholphin.util.mpv - -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_DOUBLE -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_FLAG -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_INT64 -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_NONE -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvFormat.MPV_FORMAT_STRING - -object MPVProperty { - const val POSITION = "time-pos" - const val POSITION_FULL = "time-pos/full" - const val DURATION = "duration/full" - const val PAUSED = "pause" - const val PAUSED_FOR_CACHE = "paused-for-cache" - const val SPEED = "speed" - const val TRACK_LIST = "track-list" - const val ASPECT = "video-params/aspect" - const val ROTATE = "video-params/rotate" - const val TRACK_VIDEO = "current-tracks/video/image" - const val METADATA = "metadata" - const val HWDEC = "hwdec-current" - const val MUTE = "mute" - const val TRACK_AUDIO = "current-tracks/audio/selected" - const val SEEKABLE = "seekable" - const val SUBTITLE_TEXT = "sub-text" - - val observedProperties = - mapOf( - POSITION to MPV_FORMAT_INT64, -// POSITION_FULL to MPV_FORMAT_DOUBLE, - DURATION to MPV_FORMAT_DOUBLE, - PAUSED to MPV_FORMAT_FLAG, - PAUSED_FOR_CACHE to MPV_FORMAT_FLAG, - SPEED to MPV_FORMAT_STRING, - TRACK_LIST to MPV_FORMAT_NONE, - ASPECT to MPV_FORMAT_DOUBLE, - ROTATE to MPV_FORMAT_DOUBLE, - TRACK_VIDEO to MPV_FORMAT_NONE, - METADATA to MPV_FORMAT_NONE, - HWDEC to MPV_FORMAT_NONE, - MUTE to MPV_FORMAT_FLAG, - TRACK_AUDIO to MPV_FORMAT_NONE, - SEEKABLE to MPV_FORMAT_FLAG, - SUBTITLE_TEXT to MPV_FORMAT_STRING, - ) -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvLogger.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvLogger.kt deleted file mode 100644 index 300d36f05..000000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvLogger.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.damontecres.wholphin.util.mpv - -import timber.log.Timber - -class MpvLogger : MPVLib.LogObserver { - override fun logMessage( - prefix: String, - level: Int, - text: String, - ) { - // https://github.com/mpv-player/mpv/blob/122abdfec3124bfc92a2918a70ca8150eee68338/include/mpv/client.h#L1423 - when { - level <= 10 -> Timber.wtf("%s, %s", prefix, text) - level <= 20 -> Timber.e("%s, %s", prefix, text) - level <= 30 -> Timber.w("%s, %s", prefix, text) - level <= 40 -> Timber.i("%s, %s", prefix, text) - level <= 50 -> Timber.d("%s, %s", prefix, text) - else -> Timber.v("%s, %s", prefix, text) - } - } -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt deleted file mode 100644 index 77613be39..000000000 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt +++ /dev/null @@ -1,1239 +0,0 @@ -package com.github.damontecres.wholphin.util.mpv - -import android.content.Context -import android.os.Build -import android.os.Handler -import android.os.HandlerThread -import android.os.Looper -import android.os.Message -import android.os.Process -import android.view.Surface -import android.view.SurfaceHolder -import android.view.SurfaceView -import android.view.TextureView -import androidx.annotation.OptIn -import androidx.compose.ui.graphics.Color -import androidx.media3.common.AudioAttributes -import androidx.media3.common.BasePlayer -import androidx.media3.common.C -import androidx.media3.common.DeviceInfo -import androidx.media3.common.Format -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import androidx.media3.common.MimeTypes -import androidx.media3.common.PlaybackException -import androidx.media3.common.PlaybackParameters -import androidx.media3.common.Player -import androidx.media3.common.Timeline -import androidx.media3.common.TrackGroup -import androidx.media3.common.TrackSelectionParameters -import androidx.media3.common.Tracks -import androidx.media3.common.VideoSize -import androidx.media3.common.text.CueGroup -import androidx.media3.common.util.Clock -import androidx.media3.common.util.ListenerSet -import androidx.media3.common.util.Size -import androidx.media3.common.util.UnstableApi -import androidx.media3.common.util.Util -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector -import androidx.media3.exoplayer.trackselection.TrackSelector -import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter -import com.github.damontecres.wholphin.ui.isNotNullOrBlank -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_EOF -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_ERROR -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEndFileReason.MPV_END_FILE_REASON_STOP -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_AUDIO_RECONFIG -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_END_FILE -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_FILE_LOADED -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_PLAYBACK_RESTART -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_START_FILE -import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_VIDEO_RECONFIG -import timber.log.Timber -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.concurrent.atomics.update -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds - -/** - * This is barebones implementation of a [Player] which plays content using libmpv - * - * It doesn't support every feature or emit every event - */ -@kotlin.OptIn(ExperimentalAtomicApi::class) -@OptIn(UnstableApi::class) -class MpvPlayer( - private val context: Context, - private val enableHardwareDecoding: Boolean, - private val useGpuNext: Boolean, -) : BasePlayer(), - MPVLib.EventObserver, - TrackSelector.InvalidationListener, - Handler.Callback, - SurfaceHolder.Callback { - companion object { - private const val DEBUG = false - - private val initLock = Any() - } - - private var surface: Surface? = null - private val playbackState = AtomicReference(PlaybackState.EMPTY) - private val mpvLogger = MpvLogger() - - // This looper will sent events to the main thread - private val looper = Util.getCurrentOrMainLooper() - private val mainHandler = Handler(looper) - private val listeners = - ListenerSet( - looper, - Clock.DEFAULT, - ) { listener, eventFlags -> - listener.onEvents(this@MpvPlayer, Player.Events(eventFlags)) - } - private var availableCommands: Player.Commands = Player.Commands.Builder().build() - private val trackSelector = DefaultTrackSelector(context) - - // This thread/looper will receive commands from the main thread to execute - private val thread: HandlerThread = - HandlerThread("MpvPlayer:Playback", Process.THREAD_PRIORITY_AUDIO) - .also { it.start() } - private val internalHandler: Handler = Handler(thread.looper, this) - - @Volatile - var isReleased = false - private set - - init { - sendCommand(MpvCommand.INITIALIZE, null) - } - - private fun init() { - Timber.v("config-dir=${context.filesDir.path}") - MPVLib.addLogObserver(mpvLogger) - - Timber.v("Creating MPVLib") - MPVLib.create(context) - MPVLib.setOptionString("config", "yes") - MPVLib.setOptionString("config-dir", context.filesDir.path) - - if (enableHardwareDecoding) { - MPVLib.setOptionString("hwdec", "mediacodec,mediacodec-copy") - MPVLib.setOptionString("vo", if (useGpuNext) "gpu-next" else "gpu") - } else { - MPVLib.setOptionString("hwdec", "no") - } - MPVLib.setOptionString("gpu-context", "android") - - MPVLib.setOptionString("opengl-es", "yes") - MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1") - val cacheMegs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) 64 else 32 - MPVLib.setOptionString("demuxer-max-bytes", "${cacheMegs * 1024 * 1024}") - MPVLib.setOptionString("demuxer-max-back-bytes", "${cacheMegs * 1024 * 1024}") - - Timber.v("Initializing MPVLib") - MPVLib.init() - - MPVLib.setOptionString("force-window", "no") - MPVLib.setOptionString("idle", "yes") -// MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath) - - MPVLib.addObserver(this@MpvPlayer) - MPVProperty.observedProperties.forEach(MPVLib::observeProperty) - - availableCommands = - Player.Commands - .Builder() - .addAll( - COMMAND_PLAY_PAUSE, - COMMAND_PREPARE, - COMMAND_STOP, - COMMAND_SET_SPEED_AND_PITCH, - COMMAND_SET_SHUFFLE_MODE, - COMMAND_SET_REPEAT_MODE, - COMMAND_GET_CURRENT_MEDIA_ITEM, - COMMAND_GET_TIMELINE, - COMMAND_GET_METADATA, -// COMMAND_SET_PLAYLIST_METADATA, - COMMAND_SET_MEDIA_ITEM, -// COMMAND_CHANGE_MEDIA_ITEMS, - COMMAND_GET_TRACKS, -// COMMAND_GET_AUDIO_ATTRIBUTES, -// COMMAND_SET_AUDIO_ATTRIBUTES, -// COMMAND_GET_VOLUME, -// COMMAND_SET_VOLUME, - COMMAND_SET_VIDEO_SURFACE, -// COMMAND_GET_TEXT, - COMMAND_RELEASE, - ).build() - trackSelector.init(this, DefaultBandwidthMeter.getSingletonInstance(context)) - } - - override fun getApplicationLooper(): Looper = looper - - override fun addListener(listener: Player.Listener) { - if (DEBUG) Timber.v("addListener") - listeners.add(listener) - } - - override fun removeListener(listener: Player.Listener) { - if (DEBUG) Timber.v("removeListener") - listeners.remove(listener) - } - - override fun setMediaItems( - mediaItems: List, - resetPosition: Boolean, - ) { - if (DEBUG) Timber.v("setMediaItems") - throwIfReleased() - mediaItems.firstOrNull()?.let { - if (surface != null) { - sendCommand(MpvCommand.LOAD_FILE, MediaAndPosition(it, C.TIME_UNSET)) - } - } - } - - override fun setMediaItems( - mediaItems: List, - startIndex: Int, - startPositionMs: Long, - ) { - if (DEBUG) Timber.v("setMediaItems") - throwIfReleased() - sendCommand( - MpvCommand.LOAD_FILE, - MediaAndPosition(mediaItems[startIndex], startPositionMs), - ) - } - - override fun addMediaItems( - index: Int, - mediaItems: List, - ): Unit = throw UnsupportedOperationException() - - override fun moveMediaItems( - fromIndex: Int, - toIndex: Int, - newIndex: Int, - ): Unit = throw UnsupportedOperationException() - - override fun replaceMediaItems( - fromIndex: Int, - toIndex: Int, - mediaItems: List, - ): Unit = throw UnsupportedOperationException() - - override fun removeMediaItems( - fromIndex: Int, - toIndex: Int, - ): Unit = throw UnsupportedOperationException() - - override fun getAvailableCommands(): Player.Commands = availableCommands - - override fun prepare() { - if (DEBUG) Timber.v("prepare") - } - - override fun getPlaybackState(): Int { - if (DEBUG) Timber.v("getPlaybackState") - return playbackState.load().state - } - - override fun getPlaybackSuppressionReason(): Int = PLAYBACK_SUPPRESSION_REASON_NONE - - override fun getPlayerError(): PlaybackException? { - // TODO - return null - } - - override fun setPlayWhenReady(playWhenReady: Boolean) { - if (isReleased) return - if (DEBUG) Timber.v("setPlayWhenReady: $playWhenReady") - sendCommand(MpvCommand.PLAY_PAUSE, playWhenReady) - } - - override fun getPlayWhenReady(): Boolean { - if (DEBUG) Timber.v("getPlayWhenReady") - if (isReleased) return false - return !playbackState.load().isPaused - } - - override fun setRepeatMode(repeatMode: Int) { - if (DEBUG) Timber.v("setRepeatMode") - } - - override fun getRepeatMode(): Int = REPEAT_MODE_OFF - - override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) { - if (DEBUG) Timber.v("setShuffleModeEnabled") - } - - override fun getShuffleModeEnabled(): Boolean = false - - override fun isLoading(): Boolean = playbackState.load().isLoadingFile - - override fun getSeekBackIncrement(): Long = 10_000 - - override fun getSeekForwardIncrement(): Long = 30_000 - - override fun getMaxSeekToPreviousPosition(): Long = 10_000 - - override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { - if (DEBUG) Timber.v("setPlaybackParameters") - sendCommand(MpvCommand.SET_SPEED, playbackParameters.speed) - } - - override fun getPlaybackParameters(): PlaybackParameters { - if (DEBUG) Timber.v("getPlaybackParameters") - return PlaybackParameters(playbackState.load().speed) - } - - override fun stop() { - if (DEBUG) Timber.v("stop") - if (isReleased) return - pause() - internalHandler.removeCallbacks(updatePlaybackState) - playbackState.update { - PlaybackState.EMPTY - } - notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) } - notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(STATE_IDLE) } - } - - override fun release() { - Timber.i("release") - playbackState.update { - PlaybackState.EMPTY - } - if (!isReleased) { - internalHandler.removeCallbacks(updatePlaybackState) - MPVLib.removeObserver(this@MpvPlayer) - sendCommand(MpvCommand.DESTROY, null) - thread.quitSafely() - } - isReleased = true - } - - override fun getCurrentTracks(): Tracks { - if (DEBUG) Timber.v("getCurrentTracks") - if (isReleased) return Tracks.EMPTY - return playbackState.load().tracks - } - - override fun getTrackSelectionParameters(): TrackSelectionParameters { - if (DEBUG) Timber.v("getTrackSelectionParameters") - - return TrackSelectionParameters - .Builder() - .build() - } - - override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) { - Timber.v("TrackSelection: setTrackSelectionParameters %s", parameters) - if (isReleased) return - val tracks = playbackState.load().tracks - if (C.TRACK_TYPE_TEXT in parameters.disabledTrackTypes) { - // Subtitles disabled - Timber.v("TrackSelection: disabling subtitles") - sendCommand(MpvCommand.SET_TRACK_SELECTION, TrackSelection("sid", "no")) - } - if (C.TRACK_TYPE_AUDIO in parameters.disabledTrackTypes) { - // Audio disabled - Timber.v("TrackSelection: disabling audio") - sendCommand(MpvCommand.SET_TRACK_SELECTION, TrackSelection("aid", "no")) - } - Timber.v("TrackSelection: Got ${parameters.overrides.size} overrides") - parameters.overrides.forEach { (trackGroup, trackSelectionOverride) -> - val result = - tracks.groups.firstOrNull { it.mediaTrackGroup == trackGroup }?.let { - val id = it.mediaTrackGroup.getFormat(0).id - val splits = id?.split(":") - val trackId = splits?.getOrNull(1) - val propertyName = - when (it.mediaTrackGroup.type) { - C.TRACK_TYPE_AUDIO -> "aid" - C.TRACK_TYPE_VIDEO -> "vid" - C.TRACK_TYPE_TEXT -> "sid" - else -> null - } - Timber.v("TrackSelection: activating %s %s '%s'", propertyName, trackId, id) - if (trackId != null && propertyName != null) { - sendCommand( - MpvCommand.SET_TRACK_SELECTION, - TrackSelection(propertyName, trackId), - ) - true - } else { - false - } - } - if (result != true) { - Timber.w( - "Did not find track to select for type=%s, id=%s", - trackGroup.type, - trackGroup.getFormat(0).id, - ) - } - } - } - - override fun getMediaMetadata(): MediaMetadata { - if (DEBUG) Timber.v("getMediaMetadata") - return MediaMetadata.EMPTY - } - - override fun getPlaylistMetadata(): MediaMetadata { - if (DEBUG) Timber.v("getPlaylistMetadata") - return MediaMetadata.EMPTY - } - - override fun setPlaylistMetadata(mediaMetadata: MediaMetadata): Unit = throw UnsupportedOperationException() - - override fun getCurrentTimeline(): Timeline { - if (DEBUG) Timber.v("getCurrentTimeline") - return playbackState.load().timeline - } - - override fun getCurrentPeriodIndex(): Int { - if (DEBUG) Timber.v("getCurrentPeriodIndex") - // TODO - return 0 - } - - override fun getCurrentMediaItemIndex(): Int { - if (DEBUG) Timber.v("getCurrentMediaItemIndex") - return 0 - } - - override fun getDuration(): Long { - if (DEBUG) Timber.v("getDuration") - return playbackState.load().durationMs - } - - override fun getCurrentPosition(): Long { - if (DEBUG) Timber.v("getCurrentPosition") - val state = playbackState.load() - return state.positionMs - } - - override fun getBufferedPosition(): Long { - if (DEBUG) Timber.v("getBufferedPosition") - return currentPosition + totalBufferedDuration - } - - override fun getTotalBufferedDuration(): Long { - if (DEBUG) Timber.v("getTotalBufferedDuration") - if (isReleased) return 0 - return playbackState.load().bufferMs - } - - override fun isPlayingAd(): Boolean { - if (DEBUG) Timber.v("isPlayingAd") - return false - } - - override fun getCurrentAdGroupIndex(): Int = C.INDEX_UNSET - - override fun getCurrentAdIndexInAdGroup(): Int = C.INDEX_UNSET - - override fun getContentPosition(): Long = currentPosition - - override fun getContentBufferedPosition(): Long = bufferedPosition - - override fun getAudioAttributes(): AudioAttributes = throw UnsupportedOperationException() - - override fun setVolume(volume: Float): Unit = throw UnsupportedOperationException() - - override fun getVolume(): Float = 1f - - override fun mute() { - volume = 0f - } - - override fun unmute() { - volume = 1f - } - - override fun clearVideoSurface(): Unit = throw UnsupportedOperationException() - - override fun clearVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException() - - override fun setVideoSurface(surface: Surface?): Unit = throw UnsupportedOperationException() - - override fun setVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException() - - override fun clearVideoSurfaceHolder(surfaceHolder: SurfaceHolder?): Unit = throw UnsupportedOperationException() - - private var surfaceHolder: SurfaceHolder? = null - - override fun setVideoSurfaceView(surfaceView: SurfaceView?) { - if (DEBUG) Timber.v("setVideoSurfaceView") - if (surfaceView != null) { - this.surfaceHolder?.removeCallback(this) - this.surfaceHolder = surfaceView.holder - if (surfaceView.holder != null) { - val surface = surfaceView.holder?.surface - surfaceView.holder.addCallback(this) - Timber.v("Got surface holder: isValid=${surface?.isValid}") - if (surface != null && surface.isValid) { - Timber.v("Queued attach") - sendCommand(MpvCommand.ATTACH_SURFACE, surface) - return - } - } - } - clearVideoSurfaceView(null) - } - - override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { - if (surface != null && surface == surfaceView?.holder?.surface) { - Timber.d("clearVideoSurfaceView") - sendCommand(MpvCommand.ATTACH_SURFACE, null) - } else { - Timber.w("clearVideoSurfaceView called with different surface: %s", surfaceView) - } - } - - override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int, - ) { - Timber.v("surfaceChanged: format=$format, width=$width, height=$height") - } - - override fun surfaceCreated(holder: SurfaceHolder) { - Timber.v("surfaceCreated") - sendCommand(MpvCommand.ATTACH_SURFACE, holder.surface) - } - - override fun surfaceDestroyed(holder: SurfaceHolder) { - Timber.v("surfaceDestroyed") - sendCommand(MpvCommand.ATTACH_SURFACE, null) - } - - override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException() - - override fun clearVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException() - - override fun getVideoSize(): VideoSize { - if (DEBUG) Timber.v("getVideoSize") - if (isReleased) return VideoSize.UNKNOWN - return playbackState.load().videoSize - } - - override fun getSurfaceSize(): Size = surfaceHolder?.surfaceFrame?.let { Size(it.width(), it.height()) } ?: Size.UNKNOWN - - override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO - - override fun getDeviceInfo(): DeviceInfo { - if (DEBUG) Timber.v("getDeviceInfo") - return DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build() - } - - override fun getDeviceVolume(): Int = throw UnsupportedOperationException() - - override fun isDeviceMuted(): Boolean = throw UnsupportedOperationException() - - @Deprecated("Deprecated in Java") - override fun setDeviceVolume(volume: Int): Unit = throw UnsupportedOperationException() - - override fun setDeviceVolume( - volume: Int, - flags: Int, - ): Unit = throw UnsupportedOperationException() - - @Deprecated("Deprecated in Java") - override fun increaseDeviceVolume(): Unit = throw UnsupportedOperationException() - - override fun increaseDeviceVolume(flags: Int): Unit = throw UnsupportedOperationException() - - @Deprecated("Deprecated in Java") - override fun decreaseDeviceVolume(): Unit = throw UnsupportedOperationException() - - override fun decreaseDeviceVolume(flags: Int): Unit = throw UnsupportedOperationException() - - @Deprecated("Deprecated in Java") - override fun setDeviceMuted(muted: Boolean): Unit = throw UnsupportedOperationException() - - override fun setDeviceMuted( - muted: Boolean, - flags: Int, - ): Unit = throw UnsupportedOperationException() - - override fun setAudioAttributes( - audioAttributes: AudioAttributes, - handleAudioFocus: Boolean, - ): Unit = throw UnsupportedOperationException() - - override fun seekTo( - mediaItemIndex: Int, - positionMs: Long, - seekCommand: Int, - isRepeatingCurrentItem: Boolean, - ) { - if (DEBUG) Timber.v("seekTo") - if (isReleased) { - Timber.w("seekTo called after release") - return - } - if (mediaItemIndex == C.INDEX_UNSET) { - return - } - sendCommand(MpvCommand.SEEK, positionMs) - } - - override fun onTrackSelectionsInvalidated() { - // no-op - } - - override fun eventProperty(property: String) { - if (DEBUG) Timber.v("eventProperty: $property") - } - - override fun eventProperty( - property: String, - value: Long, - ) { - if (DEBUG) Timber.v("eventPropertyLong: $property=$value") - when (property) { - MPVProperty.POSITION -> { - playbackState.update { - it.copy(positionMs = value.seconds.inWholeMilliseconds) - } - } - } - } - - override fun eventProperty( - property: String, - value: Boolean, - ) { - if (DEBUG) Timber.v("eventPropertyBoolean: $property=$value") - when (property) { - MPVProperty.PAUSED -> { - playbackState.update { - it.copy(isPaused = value) - } - notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) } - } - } - } - - override fun eventProperty( - property: String, - value: String, - ) { - if (DEBUG) Timber.v("eventPropertyString: $property=$value") - } - - override fun eventProperty( - property: String, - value: Double, - ) { - Timber.v("eventPropertyDouble: $property=$value") - when (property) { - MPVProperty.DURATION -> { - playbackState.update { - it.copy(durationMs = value.seconds.inWholeMilliseconds) - } - } - } - } - - override fun event(eventId: Int) { - Timber.v("event: eventId=%s", eventId) - when (eventId) { - MPV_EVENT_START_FILE -> { - internalHandler.post(updatePlaybackState) - } - - MPV_EVENT_FILE_LOADED -> { - Timber.d("event: MPV_EVENT_FILE_LOADED") - playbackState.update { - it.copy( - isLoadingFile = false, - ) - } - updatePlaybackState.run() - notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) } - - playbackState.load().media?.mediaItem?.let { media -> - media.localConfiguration?.subtitleConfigurations?.forEach { - val url = it.uri.toString() - val title = it.label ?: "External Subtitles" - Timber.v("Adding external subtitle track '$title'") - if (it.language.isNotNullOrBlank()) { - MPVLib.command(arrayOf("sub-add", url, "select", title, it.language!!)) - } else { - MPVLib.command(arrayOf("sub-add", url, "select", title)) - } - } - } - notifyListeners(EVENT_RENDERED_FIRST_FRAME) { onRenderedFirstFrame() } - notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(true) } - updateTracksAndNotify() - } - - MPV_EVENT_PLAYBACK_RESTART -> { - Timber.d("event: MPV_EVENT_PLAYBACK_RESTART") - updateTracksAndNotify() - } - - MPV_EVENT_AUDIO_RECONFIG -> { - Timber.d("event: MPV_EVENT_AUDIO_RECONFIG") - updateTracksAndNotify() - } - - MPV_EVENT_VIDEO_RECONFIG -> { - Timber.d("event: MPV_EVENT_VIDEO_RECONFIG") - updateTracksAndNotify() - updateVideoSizeAndNotify() - } - - MPV_EVENT_END_FILE -> { - Timber.d("event: MPV_EVENT_END_FILE") - // Handled by eventEndFile - } - - else -> { - Timber.v("event: $eventId") - } - } - } - - override fun eventEndFile( - reason: Int, - error: Int, - ) { - Timber.d("MPV_EVENT_END_FILE: %s %s", reason, error) - notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(false) } - when (reason) { - MPV_END_FILE_REASON_EOF -> { - playbackState.update { - it.copy(state = Player.STATE_ENDED) - } - notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { - onPlaybackStateChanged(STATE_ENDED) - } - } - - MPV_END_FILE_REASON_STOP -> { - // User initiated (eg stop, play next, etc) - } - - MPV_END_FILE_REASON_ERROR -> { - Timber.e("libmpv error, error=%s", error) - notifyListeners(EVENT_PLAYER_ERROR) { - onPlayerError( - PlaybackException( - "libmpv error", - null, - error, - ), - ) - } - } - - else -> { - // no-op - } - } - } - - private fun updateTracksAndNotify() { - val tracks = createTracks() - playbackState.update { - it.copy(tracks = tracks) - } - notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(tracks) } - } - - private fun updateVideoSizeAndNotify() { - val width = MPVLib.getPropertyInt("width") - val height = MPVLib.getPropertyInt("height") - val videoSize = - if (width != null && height != null) { - VideoSize(width, height) - } else { - VideoSize.UNKNOWN - } - playbackState.update { it.copy(videoSize = videoSize) } - notifyListeners(EVENT_VIDEO_SIZE_CHANGED) { onVideoSizeChanged(videoSize) } - } - - private fun loadFile(media: MediaAndPosition) { - Timber.v("loadFile: media=$media") - val timeline = - object : Timeline() { - override fun getWindowCount(): Int = 1 - - override fun getWindow( - windowIndex: Int, - window: Window, - defaultPositionProjectionUs: Long, - ): Window = - window.set( - media.mediaItem.mediaId, - media.mediaItem, - null, - C.TIME_UNSET, - C.TIME_UNSET, - C.TIME_UNSET, - true, - true, - media.mediaItem.liveConfiguration, - 0L, - C.TIME_UNSET, - 0, - 0, - 0, - ) - - override fun getPeriodCount(): Int = 1 - - override fun getPeriod( - periodIndex: Int, - period: Period, - setIds: Boolean, - ): Period = - period.set( - media.mediaItem.mediaId, - media.mediaItem.mediaId, - 0, - C.TIME_UNSET, - 0, - ) - - override fun getIndexOfPeriod(uid: Any): Int = 0 - - override fun getUidOfPeriod(periodIndex: Int) = media.mediaItem.mediaId - } - playbackState.update { - it.copy( - isLoadingFile = true, - state = STATE_READY, - media = media, - timeline = timeline, - ) - } - notifyListeners(EVENT_TIMELINE_CHANGED) { - onTimelineChanged( - timeline, - TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, - ) - } - notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) } - val url = - media.mediaItem.localConfiguration - ?.uri - .toString() - if (media.startPositionMs > 0) { - MPVLib.command( - arrayOf( - "loadfile", - url, - "replace", - "-1", - "start=${media.startPositionMs / 1000.0}", - ), - ) - } else { - MPVLib.command(arrayOf("loadfile", url, "replace", "-1")) - } - - if (enableHardwareDecoding) { - MPVLib.setOptionString("vo", if (useGpuNext) "gpu-next" else "gpu") - } - Timber.d("Called loadfile") - } - - private fun throwIfReleased() { - if (isReleased) { - throw IllegalStateException("Cannot access MpvPlayer after it is released") - } - } - - private fun notifyListeners( - eventId: Int, - block: Player.Listener.() -> Unit, - ) { - mainHandler.post { - listeners.queueEvent(eventId) { - block.invoke(it) - } - listeners.flushEvents() - } - } - - var subtitleDelaySeconds: Double - get() { - if (isReleased) return 0.0 - return playbackState.load().subtitleDelay - } - set(value) { - if (isReleased) return - sendCommand(MpvCommand.SET_SUBTITLE_DELAY, value) - } - - var subtitleDelay: Duration - get() { - if (isReleased) return Duration.ZERO - return subtitleDelaySeconds.seconds - } - set(value) { - if (isReleased) return - subtitleDelaySeconds = value.inWholeMilliseconds / 1000.0 - } - - private val updatePlaybackState: Runnable = - Runnable { - val state = playbackState.load() - if (state.media == null) { - return@Runnable - } - val positionMs = - MPVLib.getPropertyDouble("time-pos/full")?.seconds?.inWholeMilliseconds - ?: C.TIME_UNSET - val bufferMs = - MPVLib.getPropertyDouble("demuxer-cache-duration")?.seconds?.inWholeMilliseconds - ?: C.TIME_UNSET - val durationMs = - MPVLib.getPropertyDouble("duration/full")?.seconds?.inWholeMilliseconds - ?: C.TIME_UNSET - val speed = MPVLib.getPropertyDouble("speed")?.toFloat() ?: 1f - val paused = MPVLib.getPropertyBoolean("pause") ?: false - val width = MPVLib.getPropertyInt("width") - val height = MPVLib.getPropertyInt("height") - val videoSize = - if (width != null && height != null) { - VideoSize(width, height) - } else { - VideoSize.UNKNOWN - } - - val mediaItem = state.media.mediaItem - val timeline = - object : Timeline() { - override fun getWindowCount(): Int = 1 - - override fun getWindow( - windowIndex: Int, - window: Window, - defaultPositionProjectionUs: Long, - ): Window = - window.set( - mediaItem.mediaId, - mediaItem, - null, - C.TIME_UNSET, - C.TIME_UNSET, - C.TIME_UNSET, - true, - false, - mediaItem.liveConfiguration, - 0L, - if (durationMs != C.TIME_UNSET) durationMs.milliseconds.inWholeMicroseconds else C.TIME_UNSET, - 0, - 0, - 0, - ) - - override fun getPeriodCount(): Int = 1 - - override fun getPeriod( - periodIndex: Int, - period: Period, - setIds: Boolean, - ): Period = - period.set( - mediaItem.mediaId, - mediaItem.mediaId, - 0, - state.durationMs.milliseconds.inWholeMicroseconds, - 0, - ) - - override fun getIndexOfPeriod(uid: Any): Int = 0 - - override fun getUidOfPeriod(periodIndex: Int) = mediaItem.mediaId - } - - playbackState.update { - it.copy( - timestamp = System.currentTimeMillis(), - positionMs = positionMs, - bufferMs = bufferMs, - durationMs = durationMs, - speed = speed, - isPaused = paused, - videoSize = videoSize, - timeline = timeline, - ) - } - notifyListeners(EVENT_TIMELINE_CHANGED) { - onTimelineChanged( - timeline, - TIMELINE_CHANGE_REASON_SOURCE_UPDATE, - ) - } - } - - private fun sendCommand( - cmd: MpvCommand, - obj: Any?, - ) { - internalHandler.obtainMessage(cmd.ordinal, obj).sendToTarget() - } - - private val queuedCommands = mutableListOf>() - - override fun handleMessage(msg: Message): Boolean { - val cmd = MpvCommand.entries[msg.what] - if (isReleased && cmd != MpvCommand.DESTROY) { - Timber.w("Player is released, ignoring command %s", cmd) - return true - } - if (surface == null && !cmd.isLifecycle) { - // If libmpv isn't ready, ueue the messages - // Note: this means nothing will play until it is attached to a surface, - // so MpvPlayer can't be used for background audio/music playback - Timber.v("MPV is not initialized/attached yet, queue cmd %s", cmd) - queuedCommands.add(Pair(cmd, msg.obj)) - } else { - handleCommand(cmd, msg.obj) - } - return true - } - - private fun handleCommand( - cmd: MpvCommand, - obj: Any?, - ) { - Timber.d("handleCommand: cmd=$cmd") - when (cmd) { - MpvCommand.PLAY_PAUSE -> { - val playWhenReady = obj as Boolean - MPVLib.setPropertyBoolean("pause", !playWhenReady) - playbackState.update { - it.copy(isPaused = !playWhenReady) - } - notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) { - onPlayWhenReadyChanged( - playWhenReady, - PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, - ) - } - } - - MpvCommand.SET_TRACK_SELECTION -> { - val (propertyName, trackId) = obj as TrackSelection - MPVLib.setPropertyString(propertyName, trackId) - updateTracksAndNotify() - } - - MpvCommand.SEEK -> { - val positionMs = obj as Long - MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0) - playbackState.update { - it.copy(positionMs = positionMs) - } - } - - MpvCommand.SET_SPEED -> { - val value = obj as Float - MPVLib.setPropertyDouble("speed", value.toDouble()) - playbackState.update { - it.copy(speed = value) - } - } - - MpvCommand.SET_SUBTITLE_DELAY -> { - val value = obj as Double - MPVLib.setPropertyDouble("sub-delay", value) - playbackState.update { - it.copy(subtitleDelay = value) - } - } - - MpvCommand.LOAD_FILE -> { - loadFile(obj as MediaAndPosition) - } - - MpvCommand.ATTACH_SURFACE -> { - val surface = obj as Surface? - if (surface == null || (this.surface != null && this.surface != surface)) { - // If clearing or changing the surface - MPVLib.detachSurface() - MPVLib.setPropertyString("vo", "null") - MPVLib.setPropertyString("force-window", "no") - Timber.d("Detached surface") - } - if (surface != null) { - MPVLib.attachSurface(surface) - this.surface = surface - MPVLib.setOptionString("force-window", "yes") - Timber.d("Attached surface") - if (queuedCommands.isNotEmpty()) { - Timber.d("Processing queued commands") - while (queuedCommands.isNotEmpty()) { - val msg = queuedCommands.removeAt(0) - handleCommand(msg.first, msg.second) - } - } - } - } - - MpvCommand.INITIALIZE -> { - synchronized(initLock) { - init() - } - } - - MpvCommand.DESTROY -> { - synchronized(initLock) { - MPVLib.setPropertyBoolean("pause", true) - MPVLib.removeLogObserver(mpvLogger) - MPVLib.destroy() - Timber.d("MPVLib destroyed") - } - } - } - } -} - -fun MPVLib.setPropertyColor( - property: String, - color: Color, -) = setPropertyString(property, color.mpvFormat) - -private val Color.mpvFormat: String get() = "$red/$green/$blue/$alpha" - -@OptIn(UnstableApi::class) -private fun createTracks(): Tracks { - val trackCount = MPVLib.getPropertyInt("track-list/count") ?: return Tracks.EMPTY - val groups = - (0.. - val type = MPVLib.getPropertyString("track-list/$idx/type") - val id = MPVLib.getPropertyInt("track-list/$idx/id") - val lang = MPVLib.getPropertyString("track-list/$idx/lang") - val codec = MPVLib.getPropertyString("track-list/$idx/codec") - val codecDescription = MPVLib.getPropertyString("track-list/$idx/codec-desc") - val isDefault = MPVLib.getPropertyBoolean("track-list/$idx/default") ?: false - val isForced = MPVLib.getPropertyBoolean("track-list/$idx/forced") ?: false - val isExternal = MPVLib.getPropertyBoolean("track-list/$idx/external") ?: false - val isSelected = MPVLib.getPropertyBoolean("track-list/$idx/selected") ?: false - val channelCount = MPVLib.getPropertyInt("track-list/$idx/demux-channel-count") - val title = MPVLib.getPropertyString("track-list/$idx/title") - - if (type != null && id != null) { - // TODO do we need the real mimetypes? - val mimeType = - when (type) { - "video" -> MimeTypes.BASE_TYPE_VIDEO + "/todo" - "audio" -> MimeTypes.BASE_TYPE_AUDIO + "/todo" - "sub" -> MimeTypes.BASE_TYPE_TEXT + "/todo" - else -> "unknown/todo" - } - var flags = 0 - if (isDefault) flags = flags or C.SELECTION_FLAG_DEFAULT - if (isForced) flags = flags or C.SELECTION_FLAG_FORCED - val builder = - Format - .Builder() - .apply { - if (isExternal) { - setId("$idx:e:$id") - } else { - setId("$idx:$id") - } - }.setCodecs(codec) - .setSampleMimeType(mimeType) - .setLanguage(lang) - .setLabel(listOfNotNull(title, codecDescription).joinToString(",")) - .setSelectionFlags(flags) - if (type == "video" && isSelected) { - builder.setWidth(MPVLib.getPropertyInt("width") ?: -1) - builder.setHeight(MPVLib.getPropertyInt("height") ?: -1) - } - channelCount?.let(builder::setChannelCount) - val format = builder.build() -// Timber.v("$idx=$format") - - val trackGroup = TrackGroup(format) - val group = - Tracks.Group( - trackGroup, - false, - intArrayOf(C.FORMAT_HANDLED), - booleanArrayOf(isSelected), - ) - group - } else { - null - } - } - return Tracks(groups) -} - -private data class PlaybackState( - val timestamp: Long, - val isLoadingFile: Boolean, - val media: MediaAndPosition?, - val positionMs: Long, - val bufferMs: Long, - val durationMs: Long, - val isPaused: Boolean, - val speed: Float, - val subtitleDelay: Double, - val videoSize: VideoSize, - @param:Player.State val state: Int, - val tracks: Tracks, - val timeline: Timeline, -) { - companion object { - val EMPTY = - PlaybackState( - timestamp = C.TIME_UNSET, - isLoadingFile = false, - media = null, - positionMs = C.TIME_UNSET, - durationMs = C.TIME_UNSET, - tracks = Tracks.EMPTY, - bufferMs = C.TIME_UNSET, - isPaused = false, - speed = 1f, - videoSize = VideoSize.UNKNOWN, - state = Player.STATE_IDLE, - subtitleDelay = 0.0, - timeline = Timeline.EMPTY, - ) - } -} - -private data class TrackSelection( - val property: String, - val trackId: String, -) - -private data class MediaAndPosition( - val mediaItem: MediaItem, - val startPositionMs: Long, -) - -enum class MpvCommand( - val isLifecycle: Boolean, -) { - PLAY_PAUSE(false), - SEEK(false), - SET_TRACK_SELECTION(false), - SET_SPEED(false), - SET_SUBTITLE_DELAY(false), - LOAD_FILE(false), - ATTACH_SURFACE(true), - INITIALIZE(true), - DESTROY(true), -} diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/Utils.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/Utils.kt new file mode 100644 index 000000000..18e67f636 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/Utils.kt @@ -0,0 +1,11 @@ +package com.github.damontecres.wholphin.util.mpv + +import androidx.compose.ui.graphics.Color +import com.github.damontecres.wholphin.mpv.MPVLib + +fun MPVLib.setPropertyColor( + property: String, + color: Color, +) = setPropertyString(property, color.mpvFormat) + +private val Color.mpvFormat: String get() = "$red/$green/$blue/$alpha" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef483c5be..4d906b3a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ kotlinxCoroutinesTest = "1.10.2" coreTesting = "2.2.0" openapi-generator = "7.22.0" runner = "1.7.0" -wholphin-extensions = "0.1.2" +wholphin-extensions = "0.2.0" [libraries] wholphin-extensions-mpv = { module = "com.github.damontecres.wholphin.mpv:wholphin-mpv", version.ref = "wholphin-extensions" }