From 06c1d622bb1f3272a975a85258fd561ec6b6eedf Mon Sep 17 00:00:00 2001 From: coulterj Date: Tue, 3 Nov 2026 10:18:41 -0600 Subject: [PATCH] Fix HDHomeRun Live TV direct playback by including the live stream ID (#2163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- .../wholphin/ui/playback/PlaybackViewModel.kt | 1 + .../ui/playback/PlaybackViewModelTests.kt | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) 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 e51ccfbb..24bd8ee9 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 @@ -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, 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 7189adb1..87e5670a 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 @@ -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() + verify(exactly = 1) { mockPlayer.setMediaItem(capture(mediaItem), any()) } + 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) {