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
Showing only changes of commit 8da699ca8b - Show all commits

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() {
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)
}
}
}

View file

@ -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<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
fun `Direct play includes the opened live stream ID`() =
runTest(testDispatcher) {