Fix duplicate commands from key presses/MediaSession (#663)

## Description
Fixes duplicate play/pause commands from a remote button press and
`MediaSession`.

This ended up being a bit complicated because Wholphin overrides the
default play/pause behavior for the skip back setting and to show the
controller on pause. This means using a forwarding player which meant
implementing more functionality into `MpvPlayer`.

### Related issues
Fixes #653
This commit is contained in:
Ray 2026-01-09 16:59:37 -05:00 committed by GitHub
parent 2cd34692e9
commit ee73e7d723
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 167 additions and 43 deletions

View file

@ -0,0 +1,27 @@
package com.github.damontecres.wholphin.ui.playback
import androidx.media3.common.ForwardingSimpleBasePlayer
import androidx.media3.common.Player
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
import com.github.damontecres.wholphin.preferences.skipBackOnResume
import com.github.damontecres.wholphin.ui.seekBack
import com.google.common.util.concurrent.ListenableFuture
import timber.log.Timber
class MediaSessionPlayer(
player: Player,
private val controllerViewState: ControllerViewState,
private val playbackPreferences: PlaybackPreferences,
) : ForwardingSimpleBasePlayer(player) {
override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
Timber.v("handleSetPlayWhenReady: playWhenReady=$playWhenReady")
if (!playWhenReady && player.isPlaying) {
controllerViewState.showControls()
} else if (playWhenReady) {
playbackPreferences.skipBackOnResume?.let {
player.seekBack(it)
}
}
return super.handleSetPlayWhenReady(playWhenReady)
}
}

View file

@ -61,27 +61,8 @@ class PlaybackKeyHandler(
}
} else if (isMedia(it)) {
when (it.key) {
Key.MediaPlay -> {
Util.handlePlayButtonAction(player)
skipBackOnResume?.let {
player.seekBack(it)
}
}
Key.MediaPause -> {
Util.handlePauseButtonAction(player)
controllerViewState.showControls()
}
Key.MediaPlayPause -> {
Util.handlePlayPauseButtonAction(player)
if (!player.isPlaying) {
controllerViewState.showControls()
} else {
skipBackOnResume?.let {
player.seekBack(it)
}
}
Key.MediaPlay, Key.MediaPause, Key.MediaPlayPause -> {
// no-op, MediaSession will handle
}
Key.MediaFastForward, Key.MediaSkipForward -> {

View file

@ -135,7 +135,7 @@ class PlaybackViewModel
val player by lazy {
playerFactory.createVideoPlayer()
}
private val mediaSession: MediaSession
private var mediaSession: MediaSession? = null
internal val mutex = Mutex()
val controllerViewState =
@ -179,15 +179,11 @@ class PlaybackViewModel
}
jobs.forEach { it.cancel() }
player.release()
mediaSession.release()
mediaSession?.release()
}
viewModelScope.launch(ExceptionHandler()) { controllerViewState.observe() }
player.addListener(this)
(player as? ExoPlayer)?.addAnalyticsListener(this)
mediaSession =
MediaSession
.Builder(context, player)
.build()
jobs.add(subscribe())
jobs.add(listenForTranscodeReason())
}
@ -284,6 +280,18 @@ class PlaybackViewModel
} else {
throw IllegalArgumentException("Item is not playable and not PlaybackList: ${queriedItem.type}")
}
val sessionPlayer =
MediaSessionPlayer(
player,
controllerViewState,
preferences.appPreferences.playbackPreferences,
)
mediaSession =
MediaSession
.Builder(context, sessionPlayer)
.build()
val item = BaseItem.from(base, api)
val played =
@ -1058,7 +1066,7 @@ class PlaybackViewModel
Timber.v("release")
activityListener?.release()
player.release()
mediaSession.release()
mediaSession?.release()
activityListener = null
}