From d041e1189ad6c7ca200ac48e8e5f790a4cf8e276 Mon Sep 17 00:00:00 2001 From: Amonoman Date: Fri, 24 Apr 2026 20:59:15 +0200 Subject: [PATCH 1/3] fix: remove spurious seekTo on STATE_READY that caused Opus stuttering --- .../pixelplay/data/service/MusicService.kt | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 5d654377e..35d2e4dc6 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -973,9 +973,7 @@ class MusicService : MediaLibraryService() { private val playerListener = object : Player.Listener { override fun onVolumeChanged(volume: Float) { - if (engine.isTransitionRunning()) { - return - } + if (engine.isTransitionRunning()) return val expectedVolume = expectedReplayGainVolume if (expectedVolume != null && abs(expectedVolume - volume) < 0.001f) { expectedReplayGainVolume = null @@ -1006,12 +1004,15 @@ class MusicService : MediaLibraryService() { } else -> clearHeadsetReconnectResume() } + requestWidgetFullUpdate(force = true) + mediaSession?.let { refreshMediaSessionUi(it) } + schedulePlaybackSnapshotPersist() } - + override fun onAvailableCommandsChanged(availableCommands: Player.Commands) { - val canSeek = availableCommands.contains(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) - val player = engine.masterPlayer - Timber.tag(TAG).w("onAvailableCommandsChanged. Can Seek Command? $canSeek. IsSeekable? ${player.isCurrentMediaItemSeekable}. Duration: ${player.duration}") + val canSeek = availableCommands.contains(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) + val player = engine.masterPlayer + Timber.tag(TAG).w("onAvailableCommandsChanged. Can Seek Command? $canSeek. IsSeekable? ${player.isCurrentMediaItemSeekable}. Duration: ${player.duration}") } override fun onPlaybackStateChanged(playbackState: Int) { @@ -1028,7 +1029,19 @@ class MusicService : MediaLibraryService() { schedulePlaybackSnapshotPersist(immediate = timeline.isEmpty) } - override fun onMediaItemTransition(item: MediaItem?, reason: Int) { + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION || + reason == Player.DISCONTINUITY_REASON_SEEK + ) { + applyReplayGain(mediaSession?.player?.currentMediaItem) + } + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { val eotTargetSongId = endOfTrackTimerSongId if (!eotTargetSongId.isNullOrBlank()) { if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { @@ -1045,11 +1058,12 @@ class MusicService : MediaLibraryService() { engine.masterPlayer.pause() Timber.tag(TAG).d("Paused playback at end of track from Wear timer") } - } else if (item?.mediaId != eotTargetSongId) { + } else if (mediaItem?.mediaId != eotTargetSongId) { endOfTrackTimerSongId = null Timber.tag(TAG).d("Cleared end-of-track timer after manual track change") } } + applyReplayGain(mediaSession?.player?.currentMediaItem) requestWidgetAndWearRefreshWithFollowUp() mediaSession?.let { refreshMediaSessionUiWithFollowUp(it) } schedulePlaybackSnapshotPersist() @@ -2842,6 +2856,14 @@ class MusicService : MediaLibraryService() { } } + /** + * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods. + */ + /** + * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods. + */ + // ... restlicher Code ... + /** * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods. */ @@ -2856,5 +2878,4 @@ class MusicService : MediaLibraryService() { } return future } - } From d3b3aee7b5d9d4391bdf957700e6021eff0f8bc1 Mon Sep 17 00:00:00 2001 From: Amonoman Date: Fri, 24 Apr 2026 21:00:53 +0200 Subject: [PATCH 2/3] fix: apply ReplayGain immediately during crossfade to prevent silent tracks on skip --- .../pixelplay/data/service/MusicService.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 35d2e4dc6..0917b6c4e 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -1102,7 +1102,6 @@ class MusicService : MediaLibraryService() { * Reads RG tags from the file and adjusts player.volume accordingly. */ private fun applyReplayGain(mediaItem: MediaItem?) { - val player = engine.masterPlayer replayGainJob?.cancel() replayGainRequestToken += 1 val requestToken = replayGainRequestToken @@ -1114,7 +1113,7 @@ class MusicService : MediaLibraryService() { if (!replayGainEnabled) { pendingReplayGainVolume = null if (!engine.isTransitionRunning()) { - setPlayerVolume(player, userSelectedVolume) + setPlayerVolume(engine.masterPlayer, userSelectedVolume) } return } @@ -1126,7 +1125,7 @@ class MusicService : MediaLibraryService() { if (filePath.isNullOrBlank()) { Timber.tag(TAG).d("ReplayGain: No file path for track, keeping user-selected volume") if (!engine.isTransitionRunning()) { - setPlayerVolume(player, userSelectedVolume) + setPlayerVolume(engine.masterPlayer, userSelectedVolume) } return } @@ -1154,14 +1153,18 @@ class MusicService : MediaLibraryService() { ) if (engine.isTransitionRunning()) { - // Store for application after transition completes + // Store for application after transition completes. + // If the transition was interrupted (e.g. user skipped during crossfade), + // onTransitionFinished() may never fire for this pending value — so we + // also apply it immediately to masterPlayer so volume is never lost. pendingReplayGainVolume = volume - Timber.tag(TAG).d("ReplayGain: Stored pending volume=%.2f for %s (transition running)", + setPlayerVolume(engine.masterPlayer, volume) + Timber.tag(TAG).d("ReplayGain: Applied + stored pending volume=%.2f for %s (transition running)", volume, mediaItem.mediaMetadata.title ) } else { pendingReplayGainVolume = null - setPlayerVolume(player, volume) + setPlayerVolume(engine.masterPlayer, volume) Timber.tag(TAG).d("ReplayGain: Applied volume=%.2f for %s", volume, mediaItem.mediaMetadata.title ) @@ -1187,8 +1190,11 @@ class MusicService : MediaLibraryService() { } if (pending != null) { + // pending was already applied to masterPlayer during the transition to ensure + // volume is never lost if the transition was interrupted (e.g. user skipped). + // Re-applying here is a no-op in volume terms but confirms the final state. setPlayerVolume(player, pending) - Timber.tag(TAG).d("ReplayGain: Transition finished, applied pending volume=%.2f", pending) + Timber.tag(TAG).d("ReplayGain: Transition finished, confirmed pending volume=%.2f", pending) } else { // No pending volume was computed during transition, trigger full computation applyReplayGain(mediaSession?.player?.currentMediaItem) From b4ef15d58758ff5bbdb83a76e3f9d63fbac29cf9 Mon Sep 17 00:00:00 2001 From: Amonoman Date: Fri, 24 Apr 2026 23:43:41 +0200 Subject: [PATCH 3/3] Remove some comments in MusicService.kt --- .../java/com/theveloper/pixelplay/data/service/MusicService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt index 0917b6c4e..f2d7c4a22 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt @@ -2868,7 +2868,6 @@ class MusicService : MediaLibraryService() { /** * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods. */ - // ... restlicher Code ... /** * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods.