From 8b40e89b8a43be528d67369905e0e78e112575a9 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:24:25 -0500 Subject: [PATCH] Update MPV playback threading model (#481) There's no user facing changes in this PR This PR updates `MpvPlayer` to use a threading model more similar to [ExoPlayer's model](https://developer.android.com/reference/androidx/media3/exoplayer/ExoPlayer#threading-model). There's now a dedicated thread looper message queue between the UI interactions on the main thread and calls to `libmpv`. This is necessary because previously, the main thread would make the calls into `libmpv`. `libmpv` uses internal mutex locking for some operations which meant that the main thread would be blocked momentarily. Now, the UI issues a command (eg seek to X position) and the command is added to the thread looper queue and processed on that separate thread. This uses the standard Android Handler/Looper mechanisms. Related to #235 --- .../wholphin/services/AppUpgradeHandler.kt | 10 + .../wholphin/util/mpv/MpvLogger.kt | 21 + .../wholphin/util/mpv/MpvPlayer.kt | 546 ++++++++++++------ app/src/main/jni/log.h | 2 +- scripts/mpv/README.md | 14 +- 5 files changed, 411 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvLogger.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 67b55a1b..08dc0d77 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -187,4 +187,14 @@ suspend fun upgradeApp( } } } + + if (previous.isEqualOrBefore(Version.fromString("0.3.6-52-g0"))) { + if (Build.MODEL.equals("shield android tv", ignoreCase = true)) { + appPreferences.updateData { + it.updateMpvOptions { + useGpuNext = false + } + } + } + } } 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 new file mode 100644 index 00000000..300d36f0 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvLogger.kt @@ -0,0 +1,21 @@ +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 index f0b358be..a31638c4 100644 --- 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 @@ -3,7 +3,10 @@ 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 @@ -35,6 +38,7 @@ 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 @@ -42,9 +46,12 @@ import com.github.damontecres.wholphin.util.mpv.MPVLib.MpvEvent.MPV_EVENT_AUDIO_ 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.seconds @@ -57,20 +64,22 @@ import kotlin.time.Duration.Companion.seconds @OptIn(UnstableApi::class) class MpvPlayer( private val context: Context, - enableHardwareDecoding: Boolean, - useGpuNext: Boolean, + private val enableHardwareDecoding: Boolean, + private val useGpuNext: Boolean, ) : BasePlayer(), MPVLib.EventObserver, - TrackSelector.InvalidationListener { + TrackSelector.InvalidationListener, + Handler.Callback { companion object { private const val DEBUG = false } - private var isPaused: Boolean = true private var surface: Surface? = null + private val playbackState = AtomicReference(PlaybackState.EMPTY) + // This looper will sent events to the main thread private val looper = Util.getCurrentOrMainLooper() - private val handler = Handler(looper) + private val mainHandler = Handler(looper) private val listeners = ListenerSet( looper, @@ -81,19 +90,16 @@ class MpvPlayer( private val availableCommands: Player.Commands private val trackSelector = DefaultTrackSelector(context) - private var mediaItem: MediaItem? = null - private var startPositionMs: Long = 0L - private var durationMs: Long = 0L - private var positionMs: Long = -1L - private var playbackState: Int = STATE_READY + // 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 - @Volatile - private var isLoadingFile = false - init { Timber.v("config-dir=${context.filesDir.path}") MPVLib.create(context) @@ -120,8 +126,11 @@ class MpvPlayer( MPVLib.setOptionString("idle", "yes") // MPVLib.setOptionString("sub-fonts-dir", File(context.filesDir, "fonts").absolutePath) - MPVLib.addObserver(this) - MPVProperty.observedProperties.forEach(MPVLib::observeProperty) + internalHandler.post { + MPVLib.addObserver(this@MpvPlayer) + MPVProperty.observedProperties.forEach(MPVLib::observeProperty) + MPVLib.addLogObserver(MpvLogger()) + } availableCommands = Player.Commands @@ -167,13 +176,11 @@ class MpvPlayer( mediaItems: List, resetPosition: Boolean, ) { - throwIfReleased() - if (DEBUG) Timber.v("setMediaItems") + throwIfReleased() mediaItems.firstOrNull()?.let { - mediaItem = it if (surface != null) { - loadFile(it) + sendCommand(MpvCommand.LOAD_FILE, MediaAndPosition(it, C.TIME_UNSET)) } } } @@ -184,8 +191,11 @@ class MpvPlayer( startPositionMs: Long, ) { if (DEBUG) Timber.v("setMediaItems") - this.startPositionMs = startPositionMs - setMediaItems(mediaItems.subList(startIndex, mediaItems.size), false) + throwIfReleased() + sendCommand( + MpvCommand.LOAD_FILE, + MediaAndPosition(mediaItems[startIndex], startPositionMs), + ) } override fun addMediaItems( @@ -214,17 +224,19 @@ class MpvPlayer( override fun prepare() { if (DEBUG) Timber.v("prepare") - durationMs = 0L - positionMs = -1L - playbackState = STATE_READY + playbackState.update { + it.copy( + state = Player.STATE_READY, + ) + } } override fun getPlaybackState(): Int { if (DEBUG) Timber.v("getPlaybackState") - return playbackState + return playbackState.load().state } - override fun getPlaybackSuppressionReason(): Int = Player.PLAYBACK_SUPPRESSION_REASON_NONE + override fun getPlaybackSuppressionReason(): Int = PLAYBACK_SUPPRESSION_REASON_NONE override fun getPlayerError(): PlaybackException? { TODO("Not yet implemented") @@ -232,32 +244,21 @@ class MpvPlayer( override fun setPlayWhenReady(playWhenReady: Boolean) { if (isReleased) return - if (DEBUG) Timber.v("setPlayWhenReady") - if (playWhenReady) { - MPVLib.setPropertyBoolean("pause", false) - } else { - MPVLib.setPropertyBoolean("pause", true) - } - notifyListeners(EVENT_PLAY_WHEN_READY_CHANGED) { - onPlayWhenReadyChanged( - playWhenReady, - PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, - ) - } + if (DEBUG) Timber.v("setPlayWhenReady: $playWhenReady") + sendCommand(MpvCommand.PLAY_PAUSE, playWhenReady) } override fun getPlayWhenReady(): Boolean { if (DEBUG) Timber.v("getPlayWhenReady") if (isReleased) return false - val isPaused = MPVLib.getPropertyBoolean("pause") ?: this.isPaused - return !isPaused + return !playbackState.load().isPaused } override fun setRepeatMode(repeatMode: Int) { if (DEBUG) Timber.v("setRepeatMode") } - override fun getRepeatMode(): Int = Player.REPEAT_MODE_OFF + override fun getRepeatMode(): Int = REPEAT_MODE_OFF override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) { if (DEBUG) Timber.v("setShuffleModeEnabled") @@ -265,7 +266,7 @@ class MpvPlayer( override fun getShuffleModeEnabled(): Boolean = false - override fun isLoading(): Boolean = isLoadingFile + override fun isLoading(): Boolean = playbackState.load().isLoadingFile override fun getSeekBackIncrement(): Long = 10_000 @@ -275,30 +276,34 @@ class MpvPlayer( override fun setPlaybackParameters(playbackParameters: PlaybackParameters) { if (DEBUG) Timber.v("setPlaybackParameters") - MPVLib.setPropertyDouble("speed", playbackParameters.speed.toDouble()) + sendCommand(MpvCommand.SET_SPEED, playbackParameters.speed) } override fun getPlaybackParameters(): PlaybackParameters { if (DEBUG) Timber.v("getPlaybackParameters") - val speed = MPVLib.getPropertyDouble("speed")?.toFloat() ?: 1f - return PlaybackParameters(speed) + return PlaybackParameters(playbackState.load().speed) } override fun stop() { if (DEBUG) Timber.v("stop") if (isReleased) return pause() - mediaItem = null - positionMs = -1L - durationMs = 0L - playbackState = STATE_IDLE + 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") + internalHandler.removeCallbacks(updatePlaybackState) + playbackState.update { + PlaybackState.EMPTY + } if (!isReleased) { + thread.quit() MPVLib.removeObserver(this) clearVideoSurfaceView(null) MPVLib.destroy() @@ -309,7 +314,7 @@ class MpvPlayer( override fun getCurrentTracks(): Tracks { if (DEBUG) Timber.v("getCurrentTracks") if (isReleased) return Tracks.EMPTY - return getTracks() + return playbackState.load().tracks } override fun getTrackSelectionParameters(): TrackSelectionParameters { @@ -323,16 +328,16 @@ class MpvPlayer( override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) { Timber.v("TrackSelection: setTrackSelectionParameters %s", parameters) if (isReleased) return - val tracks = getTracks() + val tracks = playbackState.load().tracks if (C.TRACK_TYPE_TEXT in parameters.disabledTrackTypes) { // Subtitles disabled Timber.v("TrackSelection: disabling subtitles") - MPVLib.setPropertyString("sid", "no") + sendCommand(MpvCommand.SET_TRACK_SELECTION, TrackSelection("sid", "no")) } if (C.TRACK_TYPE_AUDIO in parameters.disabledTrackTypes) { // Audio disabled Timber.v("TrackSelection: disabling audio") - MPVLib.setPropertyString("aid", "no") + sendCommand(MpvCommand.SET_TRACK_SELECTION, TrackSelection("aid", "no")) } Timber.v("TrackSelection: Got ${parameters.overrides.size} overrides") parameters.overrides.forEach { (trackGroup, trackSelectionOverride) -> @@ -350,7 +355,10 @@ class MpvPlayer( } Timber.v("TrackSelection: activating %s %s '%s'", propertyName, trackId, id) if (trackId != null && propertyName != null) { - MPVLib.setPropertyString(propertyName, trackId) + sendCommand( + MpvCommand.SET_TRACK_SELECTION, + TrackSelection(propertyName, trackId), + ) true } else { false @@ -397,24 +405,13 @@ class MpvPlayer( override fun getDuration(): Long { if (DEBUG) Timber.v("getDuration") - if (isReleased) { - return durationMs - } - val duration = - MPVLib.getPropertyDouble("duration/full")?.seconds?.inWholeMilliseconds - ?: durationMs - return duration + return playbackState.load().durationMs } override fun getCurrentPosition(): Long { if (DEBUG) Timber.v("getCurrentPosition") - if (isReleased) { - return positionMs - } - val position = - MPVLib.getPropertyDouble("time-pos/full")?.seconds?.inWholeMilliseconds - ?: positionMs - return position + val state = playbackState.load() + return state.positionMs } override fun getBufferedPosition(): Long { @@ -425,7 +422,7 @@ class MpvPlayer( override fun getTotalBufferedDuration(): Long { if (DEBUG) Timber.v("getTotalBufferedDuration") if (isReleased) return 0 - return MPVLib.getPropertyDouble("demuxer-cache-duration")?.seconds?.inWholeMilliseconds ?: 0 + return playbackState.load().bufferMs } override fun isPlayingAd(): Boolean { @@ -475,9 +472,8 @@ class MpvPlayer( MPVLib.attachSurface(surface) MPVLib.setOptionString("force-window", "yes") Timber.d("Attached surface") - mediaItem?.let(::loadFile) - if (mediaItem == null) { - Timber.w("mediaItem is null in setVideoSurfaceView") + playbackState.load().media?.let { + sendCommand(MpvCommand.LOAD_FILE, it) } } else { clearVideoSurfaceView(null) @@ -485,11 +481,14 @@ class MpvPlayer( } override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { - Timber.d("clearVideoSurfaceView") - MPVLib.detachSurface() - MPVLib.setPropertyString("vo", "null") - MPVLib.setPropertyString("force-window", "no") - mediaItem = null + if (surface == surfaceView?.holder?.surface) { + Timber.d("clearVideoSurfaceView") + MPVLib.detachSurface() + MPVLib.setPropertyString("vo", "null") + MPVLib.setPropertyString("force-window", "no") + } else { + Timber.w("clearVideoSurfaceView called with different surface") + } } override fun setVideoTextureView(textureView: TextureView?): Unit = throw UnsupportedOperationException() @@ -499,13 +498,7 @@ class MpvPlayer( override fun getVideoSize(): VideoSize { if (DEBUG) Timber.v("getVideoSize") if (isReleased) return VideoSize.UNKNOWN - val width = MPVLib.getPropertyInt("width") - val height = MPVLib.getPropertyInt("height") - return if (width != null && height != null) { - VideoSize(width, height) - } else { - VideoSize.UNKNOWN - } + return playbackState.load().videoSize } override fun getSurfaceSize(): Size = throw UnsupportedOperationException() @@ -566,7 +559,11 @@ class MpvPlayer( if (mediaItemIndex == C.INDEX_UNSET) { return } - MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0) + sendCommand(MpvCommand.SEEK, positionMs) + } + + override fun onTrackSelectionsInvalidated() { + // no-op } override fun eventProperty(property: String) { @@ -579,7 +576,11 @@ class MpvPlayer( ) { if (DEBUG) Timber.v("eventPropertyLong: $property=$value") when (property) { - MPVProperty.POSITION -> positionMs = value.seconds.inWholeMilliseconds + MPVProperty.POSITION -> { + playbackState.update { + it.copy(positionMs = value.seconds.inWholeMilliseconds) + } + } } } @@ -590,7 +591,9 @@ class MpvPlayer( if (DEBUG) Timber.v("eventPropertyBoolean: $property=$value") when (property) { MPVProperty.PAUSED -> { - isPaused = value + playbackState.update { + it.copy(isPaused = value) + } notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(!value) } } } @@ -609,52 +612,58 @@ class MpvPlayer( ) { Timber.v("eventPropertyDouble: $property=$value") when (property) { - MPVProperty.DURATION -> durationMs = value.seconds.inWholeMilliseconds + MPVProperty.DURATION -> { + playbackState.update { + it.copy(durationMs = value.seconds.inWholeMilliseconds) + } + } } } override fun event(eventId: Int) { + Timber.v("event: thread=${Thread.currentThread().name}, eventId=$eventId") when (eventId) { -// MPV_EVENT_START_FILE -> { -// } + MPV_EVENT_START_FILE -> { + internalHandler.post(updatePlaybackState) + } + MPV_EVENT_FILE_LOADED -> { - isLoadingFile = false + playbackState.update { + it.copy(isLoadingFile = false) + } notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) } Timber.d("event: MPV_EVENT_FILE_LOADED") - mediaItem?.let { - it.localConfiguration?.subtitleConfigurations?.forEach { + internalHandler.post(updatePlaybackState) + 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'") - MPVLib.command(arrayOf("sub-add", url, "auto", title)) + if (it.language.isNotNullOrBlank()) { + MPVLib.command(arrayOf("sub-add", url, "auto", title, it.language!!)) + } else { + MPVLib.command(arrayOf("sub-add", url, "auto", title)) + } } } notifyListeners(EVENT_RENDERED_FIRST_FRAME) { onRenderedFirstFrame() } notifyListeners(EVENT_IS_PLAYING_CHANGED) { onIsPlayingChanged(true) } - getTracks().let { - notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) } - } + updateTracksAndNotify() } MPV_EVENT_PLAYBACK_RESTART -> { Timber.d("event: MPV_EVENT_PLAYBACK_RESTART") - getTracks().let { - notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) } - } + updateTracksAndNotify() } MPV_EVENT_AUDIO_RECONFIG -> { Timber.d("event: MPV_EVENT_AUDIO_RECONFIG") - getTracks().let { - notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) } - } + updateTracksAndNotify() } MPV_EVENT_VIDEO_RECONFIG -> { Timber.d("event: MPV_EVENT_VIDEO_RECONFIG") - getTracks().let { - notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(it) } - } + updateTracksAndNotify() } MPV_EVENT_END_FILE -> { @@ -676,6 +685,9 @@ class MpvPlayer( 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) } @@ -704,25 +716,44 @@ class MpvPlayer( } } - private fun loadFile(mediaItem: MediaItem) { - isLoadingFile = true + private fun updateTracksAndNotify() { + val tracks = createTracks() + playbackState.update { + it.copy(tracks = tracks) + } + notifyListeners(EVENT_TRACKS_CHANGED) { onTracksChanged(tracks) } + } + + private fun loadFile(media: MediaAndPosition) { + Timber.v("loadFile: media=$media") + playbackState.update { + it.copy( + isLoadingFile = true, + media = media, + ) + } notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) } - val url = mediaItem.localConfiguration?.uri.toString() - if (startPositionMs > 0) { + val url = + media.mediaItem.localConfiguration + ?.uri + .toString() + if (media.startPositionMs > 0) { MPVLib.command( arrayOf( "loadfile", url, "replace", "-1", - "start=${startPositionMs / 1000.0}", + "start=${media.startPositionMs / 1000.0}", ), ) } else { MPVLib.command(arrayOf("loadfile", url, "replace", "-1")) } - MPVLib.setPropertyString("vo", "gpu") + if (enableHardwareDecoding) { + MPVLib.setOptionString("vo", if (useGpuNext) "gpu-next" else "gpu") + } Timber.d("Called loadfile") } @@ -736,7 +767,7 @@ class MpvPlayer( eventId: Int, block: Player.Listener.() -> Unit, ) { - handler.post { + mainHandler.post { listeners.queueEvent(eventId) { block.invoke(it) } @@ -744,78 +775,14 @@ class MpvPlayer( } } - private fun getTracks(): 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() - .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() - - val trackGroup = TrackGroup(format) - val group = - Tracks.Group( - trackGroup, - false, - intArrayOf(C.FORMAT_HANDLED), - booleanArrayOf(isSelected), - ) - group - } else { - null - } - } - return Tracks(groups) - } - - override fun onTrackSelectionsInvalidated() { - // no-op - } - var subtitleDelaySeconds: Double get() { if (isReleased) return 0.0 - return MPVLib.getPropertyDouble("sub-delay") ?: 0.0 + return playbackState.load().subtitleDelay } set(value) { if (isReleased) return - MPVLib.setPropertyDouble("sub-delay", value) + sendCommand(MpvCommand.SET_SUBTITLE_DELAY, value) } var subtitleDelay: Duration @@ -827,11 +794,230 @@ class MpvPlayer( if (isReleased) return subtitleDelaySeconds = value.inWholeMilliseconds / 1000.0 } + + private val updatePlaybackState: Runnable = + Runnable { + if (playbackState.load().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 + } + + playbackState.update { + it.copy( + timestamp = System.currentTimeMillis(), + positionMs = positionMs, + bufferMs = bufferMs, + durationMs = durationMs, + speed = speed, + isPaused = paused, + videoSize = videoSize, + ) + } + } + + private fun sendCommand( + cmd: MpvCommand, + obj: Any?, + ) { + internalHandler.obtainMessage(cmd.ordinal, obj).sendToTarget() + } + + override fun handleMessage(msg: Message): Boolean { + val cmd = MpvCommand.entries[msg.what] + Timber.v("handleMessage: cmd=$cmd") + when (cmd) { + MpvCommand.PLAY_PAUSE -> { + val playWhenReady = msg.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) = msg.obj as TrackSelection + MPVLib.setPropertyString(propertyName, trackId) + updateTracksAndNotify() + } + + MpvCommand.SEEK -> { + val positionMs = msg.obj as Long + MPVLib.setPropertyDouble("time-pos", positionMs / 1000.0) + playbackState.update { + it.copy(positionMs = positionMs) + } + } + + MpvCommand.SET_SPEED -> { + val value = msg.obj as Float + MPVLib.setPropertyDouble("speed", value.toDouble()) + playbackState.update { + it.copy(speed = value) + } + } + + MpvCommand.SET_SUBTITLE_DELAY -> { + val value = msg.obj as Double + MPVLib.setPropertyDouble("sub-delay", value) + playbackState.update { + it.copy(subtitleDelay = value) + } + } + + MpvCommand.LOAD_FILE -> { + loadFile(msg.obj as MediaAndPosition) + } + } + return true + } } fun MPVLib.setPropertyColor( property: String, color: Color, -) = MPVLib.setPropertyString(property, color.mpvFormat) +) = 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, +) { + 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, + ) + } +} + +private data class TrackSelection( + val property: String, + val trackId: String, +) + +private data class MediaAndPosition( + val mediaItem: MediaItem, + val startPositionMs: Long, +) + +enum class MpvCommand { + PLAY_PAUSE, + SEEK, + SET_TRACK_SELECTION, + SET_SPEED, + SET_SUBTITLE_DELAY, + LOAD_FILE, +} diff --git a/app/src/main/jni/log.h b/app/src/main/jni/log.h index 58fbda05..bef941b2 100644 --- a/app/src/main/jni/log.h +++ b/app/src/main/jni/log.h @@ -2,7 +2,7 @@ #include -#define DEBUG 1 +#define DEBUG 0 #define LOG_TAG "mpv" #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) diff --git a/scripts/mpv/README.md b/scripts/mpv/README.md index 80eeb407..4d41c03a 100644 --- a/scripts/mpv/README.md +++ b/scripts/mpv/README.md @@ -4,4 +4,16 @@ This scripts are adapted from https://github.com/mpv-android/mpv-android/tree/ae ## Instructions -TODO +```bash +cd scripts/mpv +./get_dependencies + +export NDK_PATH=... # Such as ~/Library/Android/sdk/ndk/29.0.14206865 +# Build arm64 +PATH="$PATH:$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin" ./buildall.sh --clean --arch arm64 mpv +# Build arm32 +PATH="$PATH:$NDK_PATH/toolchains/llvm/prebuilt/darwin-x86_64/bin" ./buildall.sh mpv + +cd ../.. +env PREFIX32="$(realpath scripts/mpv/prefix/armv7l)" PREFIX64="$(realpath scripts/mpv/prefix/arm64)" "$NDK_PATH/ndk-build" -C app/src/main -j && cp -fr app/src/main/libs/ app/src/main/jnilibs/ +```