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..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 @@ -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() @@ -1088,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 @@ -1100,7 +1113,7 @@ class MusicService : MediaLibraryService() { if (!replayGainEnabled) { pendingReplayGainVolume = null if (!engine.isTransitionRunning()) { - setPlayerVolume(player, userSelectedVolume) + setPlayerVolume(engine.masterPlayer, userSelectedVolume) } return } @@ -1112,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 } @@ -1140,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 ) @@ -1173,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) @@ -2842,6 +2862,13 @@ 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. + */ + /** * Bridges a suspend block into a [ListenableFuture] for Media3 callback methods. */ @@ -2856,5 +2883,4 @@ class MusicService : MediaLibraryService() { } return future } - }