From 8da699ca8baa41d14741e6824ba183214a792bec Mon Sep 17 00:00:00 2001 From: justinzeus Date: Sat, 7 Nov 2026 23:56:49 +0100 Subject: [PATCH] Reset subtitle delay when the new track has none saved --- .../wholphin/ui/playback/PlaybackViewModel.kt | 7 +- .../ui/playback/PlaybackViewModelTests.kt | 74 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 9cbbe551..c5228f07 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -1669,6 +1669,9 @@ class PlaybackViewModel } } + /** + * Load the enabled subtitle track's saved delay, or zero if no delay is saved. + */ suspend fun loadSubtitleDelay() { state.value.currentPlayback?.let { if (it.subtitleIndexEnabled) { @@ -1681,7 +1684,9 @@ class PlaybackViewModel it.subtitleIndex, it.item.id, ) - updateCurrentPlayback { it?.copy(subtitleDelay = result.delayMs.milliseconds) } + } + updateCurrentPlayback { + it?.copy(subtitleDelay = result?.delayMs?.milliseconds ?: Duration.ZERO) } } } diff --git a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt index 87e5670a..064f70e8 100644 --- a/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt +++ b/app/src/test/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModelTests.kt @@ -3,16 +3,19 @@ package com.github.damontecres.wholphin.ui.playback import android.content.Context import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.TrackSelectionParameters import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.data.model.ItemTrackModification import com.github.damontecres.wholphin.data.model.JellyfinServer import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType import com.github.damontecres.wholphin.data.model.ServerUserConfig +import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.ShowNextUpWhen @@ -82,6 +85,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -292,6 +296,76 @@ class PlaybackViewModelTests { return viewModel } + @Test + fun `Subtitle track switches load saved delays or zero and ignore disabled subtitles`() = + runTest(testDispatcher) { + setupPreferences { cinemaMode = false } + val tracks = + TestTracks + .Builder() + .addVideo() + .addAudio() + .addSubtitle(2) + val source = tracks.buildMediaSourceInfo() + val subtitles = source.mediaStreams!!.filter { it.type == MediaStreamType.SUBTITLE } + val trackA = subtitles[0].index + val trackB = subtitles[1].index + coEvery { mockUserLibraryApi.getItem(movie.id) } returns successResponse(movie) + coEvery { mockMediaInfoApi.getPostedPlaybackInfo(any(), any()) } returns + successResponse( + PlaybackInfoResponse( + mediaSources = listOf(source), + playSessionId = playSessionId, + ), + ) + coEvery { mockStreamChoiceService.chooseSource(any(), any()) } returns source + coEvery { + mockStreamChoiceService.chooseAudioStream(any(), any(), any(), any(), any()) + } returns + StreamChoiceResult( + source.mediaStreams!!.first { it.type == MediaStreamType.AUDIO }, + StreamChoiceReason.Unknown, + ) + coEvery { + mockStreamChoiceService.chooseSubtitleStream(any(), any(), any(), any(), any(), any()) + } returns StreamChoiceResult(subtitles[0], StreamChoiceReason.Unknown) + coEvery { + mockStreamChoiceService.resolveSubtitleIndex(any(), any(), any(), any(), any()) + } answers { arg(3) } + every { mockPlayer.currentTracks } returns tracks.buildForExoPlayer().toTracks() + every { mockPlayer.trackSelectionParameters } returns TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT + coEvery { mockItemPlaybackRepository.getTrackModifications(movie.id, trackA) } returns + ItemTrackModification(user.rowId, movie.id, trackA, 2000L) + + val cases = + listOf( + Triple(trackB, null, Duration.ZERO), + Triple(trackB, -750L, (-750).milliseconds), + Triple(TrackIndex.DISABLED, null, 2.seconds), + ) + cases.forEach { (targetIndex, savedDelayMs, expectedDelay) -> + coEvery { mockItemPlaybackRepository.getTrackModifications(movie.id, trackB) } returns + savedDelayMs?.let { + ItemTrackModification(user.rowId, movie.id, trackB, it) + } + val viewModel = createViewModel(Destination.Playback(movie.id, 0L)) + val initialPlayback = viewModel.state.value.currentPlayback + Assert.assertEquals(trackA, initialPlayback?.subtitleIndex) + Assert.assertEquals(2.seconds, initialPlayback?.subtitleDelay) + + viewModel.changeSubtitleStream(targetIndex).join() + testScheduler.advanceUntilIdle() + + val playback = viewModel.state.value.currentPlayback + Assert.assertEquals(targetIndex, playback?.subtitleIndex) + Assert.assertEquals(expectedDelay, playback?.subtitleDelay) + } + coVerify(exactly = 0) { + mockItemPlaybackRepository.getTrackModifications(movie.id, TrackIndex.DISABLED) + } + verify(exactly = cases.size) { mockPlayer.setMediaItem(any(), any()) } + } + @Test fun `Direct play includes the opened live stream ID`() = runTest(testDispatcher) {