Reset subtitle delay when the new track has none saved #73

Merged
justinzeus merged 1 commit from fm/sub-offset-reset into fm/tv-round2 2026-11-08 01:37:40 +01:00
2 changed files with 80 additions and 1 deletions

View file

@ -1669,6 +1669,9 @@ class PlaybackViewModel
} }
} }
/**
* Load the enabled subtitle track's saved delay, or zero if no delay is saved.
*/
suspend fun loadSubtitleDelay() { suspend fun loadSubtitleDelay() {
state.value.currentPlayback?.let { state.value.currentPlayback?.let {
if (it.subtitleIndexEnabled) { if (it.subtitleIndexEnabled) {
@ -1681,7 +1684,9 @@ class PlaybackViewModel
it.subtitleIndex, it.subtitleIndex,
it.item.id, it.item.id,
) )
updateCurrentPlayback { it?.copy(subtitleDelay = result.delayMs.milliseconds) } }
updateCurrentPlayback {
it?.copy(subtitleDelay = result?.delayMs?.milliseconds ?: Duration.ZERO)
} }
} }
} }

View file

@ -3,16 +3,19 @@ package com.github.damontecres.wholphin.ui.playback
import android.content.Context import android.content.Context
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.TrackSelectionParameters
import com.github.damontecres.wholphin.data.ItemPlaybackDao import com.github.damontecres.wholphin.data.ItemPlaybackDao
import com.github.damontecres.wholphin.data.ItemPlaybackRepository import com.github.damontecres.wholphin.data.ItemPlaybackRepository
import com.github.damontecres.wholphin.data.ServerRepository import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem 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.JellyfinServer
import com.github.damontecres.wholphin.data.model.JellyfinUser import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.Playlist
import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.data.model.PlaylistItem
import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType import com.github.damontecres.wholphin.data.model.SeriesTrackChoiceType
import com.github.damontecres.wholphin.data.model.ServerUserConfig 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.AppPreferences
import com.github.damontecres.wholphin.preferences.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlaybackPreferences
import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.ShowNextUpWhen
@ -82,6 +85,7 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -292,6 +296,76 @@ class PlaybackViewModelTests {
return viewModel 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<Int>(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<Long>()) }
}
@Test @Test
fun `Direct play includes the opened live stream ID`() = fun `Direct play includes the opened live stream ID`() =
runTest(testDispatcher) { runTest(testDispatcher) {