Fix HDHomeRun Live TV direct playback by including the live stream ID (#2163)

## Description

I was having trouble playing HDHomeRun channels through Jellyfin in
Wholphin. They started quickly in the official Jellyfin app, but
Wholphin would sometimes spin or fall back when switching channels.

This change includes the live stream ID returned by Jellyfin when
building the Direct Play URL. It also adds a regression test.

### Related issues

Related to #2014 and possibly #1903. My testing was with an HDHomeRun; I
haven’t verified the TVHeadend setups in those reports.

### Testing

Tested on a Google TV Streamer with multiple local channels, FAST
channels, and movie playback. The tested content used Direct Play, and
the HDHomeRun tuners were released after stopping.

All 18 playback tests and the formatting check passed after the
requested test cleanup.

## Screenshots

No UI changes.

## AI or LLM usage

I used OpenAI Codex to help investigate the issue, implement the change
in PlaybackViewModel.kt, add the regression test in
PlaybackViewModelTests.kt, and run validation. Codex also helped draft
this description.
This commit is contained in:
coulterj 2026-11-03 10:18:41 -06:00 • committed by GitHub
parent 38bd690c85
commit 06c1d622bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 0 deletions

View file

@ -717,6 +717,7 @@ class PlaybackViewModel
api.videosApi.getVideoStreamUrl(
itemId = itemId,
mediaSourceId = source.id,
liveStreamId = source.liveStreamId,
static = true,
tag = source.eTag,
playSessionId = response.playSessionId,

View file

@ -292,6 +292,49 @@ class PlaybackViewModelTests {
return viewModel
}
@Test
fun `Direct play includes the opened live stream ID`() =
runTest(testDispatcher) {
setupPreferences { cinemaMode = false }
val liveStreamId = "opened-live-stream"
val liveSource = mediaSource.copy(liveStreamId = liveStreamId)
coEvery { mockUserLibraryApi.getItem(movie.id) } returns successResponse(movie)
coEvery { mockMediaInfoApi.getPostedPlaybackInfo(any(), any()) } returns
successResponse(
PlaybackInfoResponse(
mediaSources = listOf(liveSource),
playSessionId = playSessionId,
),
)
every {
mockVideosApi.getVideoStreamUrl(
itemId = movie.id,
mediaSourceId = liveSource.id,
liveStreamId = liveSource.liveStreamId,
static = true,
tag = liveSource.eTag,
playSessionId = playSessionId,
)
} returns videoStreamUrl
createViewModel(Destination.Playback(movie.id, 0L))
val mediaItem = slot<MediaItem>()
verify(exactly = 1) { mockPlayer.setMediaItem(capture(mediaItem), any<Long>()) }
val playbackUri = mediaItem.captured.localConfiguration?.uri
Assert.assertEquals(videoStreamUrl, playbackUri.toString())
verify(exactly = 1) {
mockVideosApi.getVideoStreamUrl(
itemId = movie.id,
mediaSourceId = liveSource.id,
liveStreamId = liveStreamId,
static = true,
tag = liveSource.eTag,
playSessionId = playSessionId,
)
}
}
@Test
fun `Play intro first`() =
runTest(testDispatcher) {