mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
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:
parent
2cd34692e9
commit
ee73e7d723
4 changed files with 167 additions and 43 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ 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
|
||||
|
||||
/**
|
||||
|
|
@ -231,11 +232,6 @@ class MpvPlayer(
|
|||
|
||||
override fun prepare() {
|
||||
if (DEBUG) Timber.v("prepare")
|
||||
playbackState.update {
|
||||
it.copy(
|
||||
state = Player.STATE_READY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPlaybackState(): Int {
|
||||
|
|
@ -395,8 +391,7 @@ class MpvPlayer(
|
|||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
if (DEBUG) Timber.v("getCurrentTimeline")
|
||||
// TODO
|
||||
return Timeline.EMPTY
|
||||
return playbackState.load().timeline
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex(): Int {
|
||||
|
|
@ -528,7 +523,7 @@ class MpvPlayer(
|
|||
return playbackState.load().videoSize
|
||||
}
|
||||
|
||||
override fun getSurfaceSize(): Size = throw UnsupportedOperationException()
|
||||
override fun getSurfaceSize(): Size = surfaceHolder?.surfaceFrame?.let { Size(it.width(), it.height()) } ?: Size.UNKNOWN
|
||||
|
||||
override fun getCurrentCues(): CueGroup = CueGroup.EMPTY_TIME_ZERO
|
||||
|
||||
|
|
@ -655,12 +650,15 @@ class MpvPlayer(
|
|||
}
|
||||
|
||||
MPV_EVENT_FILE_LOADED -> {
|
||||
playbackState.update {
|
||||
it.copy(isLoadingFile = false)
|
||||
}
|
||||
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(false) }
|
||||
Timber.d("event: MPV_EVENT_FILE_LOADED")
|
||||
internalHandler.post(updatePlaybackState)
|
||||
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()
|
||||
|
|
@ -767,10 +765,63 @@ class MpvPlayer(
|
|||
|
||||
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) }
|
||||
|
|
@ -838,7 +889,8 @@ class MpvPlayer(
|
|||
|
||||
private val updatePlaybackState: Runnable =
|
||||
Runnable {
|
||||
if (playbackState.load().media == null) {
|
||||
val state = playbackState.load()
|
||||
if (state.media == null) {
|
||||
return@Runnable
|
||||
}
|
||||
val positionMs =
|
||||
|
|
@ -861,6 +913,53 @@ class MpvPlayer(
|
|||
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(),
|
||||
|
|
@ -870,6 +969,13 @@ class MpvPlayer(
|
|||
speed = speed,
|
||||
isPaused = paused,
|
||||
videoSize = videoSize,
|
||||
timeline = timeline,
|
||||
)
|
||||
}
|
||||
notifyListeners(EVENT_TIMELINE_CHANGED) {
|
||||
onTimelineChanged(
|
||||
timeline,
|
||||
TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1080,12 +1186,13 @@ private data class PlaybackState(
|
|||
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 = true,
|
||||
isLoadingFile = false,
|
||||
media = null,
|
||||
positionMs = C.TIME_UNSET,
|
||||
durationMs = C.TIME_UNSET,
|
||||
|
|
@ -1096,6 +1203,7 @@ private data class PlaybackState(
|
|||
videoSize = VideoSize.UNKNOWN,
|
||||
state = Player.STATE_IDLE,
|
||||
subtitleDelay = 0.0,
|
||||
timeline = Timeline.EMPTY,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue