Repository navigation
Fix Live TV HLS playback: bogus audio encoder and unplayable direct played manifests - #17768
Merged
crobibero merged 1 commit intoSep 5, 2026
Conversation
Contributor
Jellyfin API (v 12.0.0)ResultNo differences. Specifications are equivalent |
|
IDisposable
approved these changes
Sep 3, 2026
Member
|
Can this still allow direct play? I think it basically reverts #17682 |
Member
|
Tuners: For 12.0 All tested clients (web, Android TV, Swiftin, Mobile) previously direct-played (near-instant startup). With PR applied all clients remuxued (Android TV client took 10secs for the stream to start ). |
gnattu
approved these changes
Sep 5, 2026
gnattu
left a comment
Member
There was a problem hiding this comment.
It should be okay if tuners are confirmed to work with this
This was referenced Sep 6, 2026
cewert
added a commit
to jellyrock/jellyrock
that referenced
this pull request
Sep 22, 2026
…irect play The override that plays a 12.x live HLS channel from its own URL treated every server "no" as jellyfin/jellyfin#17768's. The answer does not say which rule declined, and three older rules, which 10.11 applied and JellyRock obeyed, were overridden too: the user's "Force transcoding of remote media sources such as Live TV" policy on a remote source, the tuner's "Auto-loop live streams", and the server's internet streaming bitrate limit. getUpstreamHlsBlocker() replaces shouldDirectPlayUpstreamHls() and returns which rule keeps a channel on the server's stream (UpstreamHlsBlocker), which the loader logs. The live and forced-transcode checks moved into it from the call site: the forced-transcode one is what stops a failed upstream from being retried forever, and it had no test. The request is an interface (UpstreamHlsRequest), so renaming forceTranscoding at the call site fails the build with argument-type-mismatch (tried). Of the transcode reasons, only ContainerBitrateExceedsLimit counts. Measured 2026-09-21 against the local 10.11.11 and 12.1.0 servers, on one opened h264/aac fixture channel, with the same PlaybackInfo request and an HEVC transcode target: 10.11 allowed direct play with no reasons, and 12.1 declined with VideoCodecNotSupported. For a manifest, 12.x skips its direct-play check, so its codec reasons say only whether its own transcode can copy the stream. A 1 Mbps cap added ContainerBitrateExceedsLimit on both. Not visible to the client, accepted: the tuner's "Simultaneous stream limit". The server counts it when a live stream opens, and a retry of the same channel joins the stream already open (consumer count 2, then 3, in the 12.1 log) instead of taking a second slot, so the fallback is not refused. Verified 2026-09-21 on a Streaming Stick 4K, against the local 12.1.0, 12.0.0 and 10.11.11 servers with the synthetic HLS fixture: - 12.1 and 12.0, separate-audio channel: upstream, playing in 2.6 s, no server ffmpeg. 12.1 muxed: 2.0 s. - 10.11: blocker notDeclined, the server's own direct play, 3.8 s. - 12.1 with the policy on: a remote source logs remoteSourcePolicy and takes the server's stream; a LAN source still plays upstream. - Auto-loop on: blocker autoLoop on 12.1 and on 10.11. The server's own stream did not start within 70 s on either, so that is 10.11's behaviour, not a new one. - A playlist that answers while its stream 404s on the device: direct play fails, the retry logs forcedTranscode and plays the server's stream at 32-37 s, with the stream limit at 0 and at 1. - A remote source with the policy off (Apple's example stream): upstream after a 23-537 ms preflight. That stream is on demand, so it ended at once and the player restarted the channel in a loop, which is PlayerHostView's existing handling of a live channel that finishes. - Unit 4116/4116 on device. Disabling the five new checks together fails exactly their six specs; deleting the bitrate check fails its two, and widening it to any reason fails the codec one.
Merged
2 of 6 tasks
cewert
added a commit
to jellyrock/jellyrock
that referenced
this pull request
Sep 22, 2026
#990) # Overview Jellyfin 12.0 (jellyfin/jellyfin#17768) stopped offering direct play for every `m3u` tuner channel whose URL is an HLS manifest. The server's replacement, remuxing the stream itself, drops audio carried as a separate HLS rendition (channels play silent) and starts about 30 s slower. JellyRock never used the relay that change was fixing: it plays the channel's absolute upstream URL. So when 12.x declines direct play but still returns that URL, JellyRock now plays it directly again, as on 10.11. The server's "no" does not say which rule declined, so the override keeps the server's stream wherever a rule older than 12.0 applies, as 10.11 did: the retry that forces a transcode, the tuner's "Auto-loop live streams", the user's "Force transcoding of remote media sources such as Live TV", and the internet streaming bitrate limit. The override is gated by a device preflight. A Roku handed a URL it cannot reach buffers indefinitely instead of failing, so the app first fetches the playlist from the device itself and plays it directly only on HTTP 200 with an `#EXTM3U` body within 3 s. Anything else stays on the server's stream. ## Changes - `liveTv.getUpstreamHlsBlocker()` decides when to override. It returns which rule keeps a channel on the server's stream, as an `UpstreamHlsBlocker` value the loader logs, or `NONE`. It overrides only the server's explicit "no" on an absolute `http(s)` HLS URL that does not go through Jellyfin (the server's own host and port, the `/LiveTv/LiveStreamFiles/` relay, `localhost` / `127.0.0.1`), and never for DASH. - It keeps the server's older reasons: the retry that forces a transcode (which also stops a failed upstream from being retried forever), `RequiresLooping`, the user's `ForceRemoteSourceTranscoding` policy on an `IsRemote` source, and `ContainerBitrateExceedsLimit`. Other transcode reasons are ignored: for a manifest, 12.x skips its direct-play check, and its codec reasons say only whether its own transcode can copy the stream. - The request is a typed `UpstreamHlsRequest`, so a missing or misspelled field fails the build rather than silently disabling a check. - `LoadVideoContentTask.upstreamPlaylistAnswers()` is the device preflight (`timeouts.LIVE_UPSTREAM_PREFLIGHT_MS` = 3000, verdict from `liveTv.isHlsPlaylistResponse()`). It decodes `gzip`, because some CDNs serve pre-compressed playlists even when the request doesn't ask for them. - The playback error dialog's "Play method" read "Transcoding" for every playback, because `VideoPlayerView` never copied `isDirectPlaySupported` from the loaded content. It now does; that field's only reader is the dialog. - Unit specs for both pure functions; each `getUpstreamHlsBlocker` case asserts which blocker applies. `playback.md` and the `live-hls-upstream-direct-play` decision note updated. Accepted, and not visible to the client: the tuner's "Simultaneous stream limit", which 10.11 applied by declining direct play. The server still counts it when the live stream opens, and a retry of the same channel joins that stream instead of taking a second slot. Verified 2026-09-21 on a Streaming Stick 4K, against local 10.11.11, 12.0.0 and 12.1.0 servers with a synthetic HLS fixture: - **12.1 and 12.0, separate-audio channel:** upstream, playing in 2.6 s, no server ffmpeg. 12.1 muxed: 2.0 s. - **10.11:** the server's own direct play (`notDeclined`), 3.8 s. Unchanged. - **Remote-source policy on:** a remote source takes the server's stream (`remoteSourcePolicy`); a LAN source still plays upstream. - **Auto-loop on:** the server's stream (`autoLoop`) on 12.1 and on 10.11. It did not start within 70 s on either, so that is 10.11's behaviour, not a new one. - **A playlist that answers while its stream 404s on the device:** direct play fails, the retry logs `forcedTranscode` and plays the server's stream at 32–37 s, with the stream limit at 0 and at 1. - **Only the bitrate reason is a "no" from before 12.0:** with the same request and an HEVC transcode target, 10.11 allowed direct play with no reasons while 12.1 reported `VideoCodecNotSupported`. A 1 Mbps cap added `ContainerBitrateExceedsLimit` on both. - **Tests:** unit 4116/4116 on the device. Disabling the five new checks together fails exactly their six specs; deleting the bitrate check fails its two, and widening it to any reason fails the codec one. Verified 2026-09-21 on a Roku Ultra, with the first version of the override: - **Upstream the TV cannot reach:** falls back to the server's stream and plays at 34 s. Before the preflight it buffered forever. - **Bad DNS or HTTP 404 upstream:** the error dialog in ~4 s. - **A 12.1 server's real channels (two commercial CDNs):** direct play with audio in ~2.1 s; the preflight took 57–129 ms. - **`gzip`** is verified on the device only: on a channel whose CDN sends it, it turned "not a playlist" and a 31.8 s server stream into direct play in 2.1 s. ## Follow-ups - #989: DASH (`.mpd`) channels. 12.x declines them too; play them from their own URL. - #988: a Live TV play that fails into the error dialog never closes the server's live stream. This predates this PR. - [A live TV channel whose stream ends restarts forever](https://github.com/jellyrock/jellyrock/blob/main/docs/progress.md#components): pre-existing in `PlayerHostView`; this PR makes it reachable on 12.x for on-demand playlists listed as channels. - [A live channel the server allows to direct play can buffer forever when the TV cannot reach its URL](https://github.com/jellyrock/jellyrock/blob/main/docs/progress.md#components): the preflight runs only on the override path. Pre-existing. - [A slow server probe fails a live channel's first start and leaks its live stream](https://github.com/jellyrock/jellyrock/blob/main/docs/progress.md#api): pre-existing on every server version. - [Playback Info shows no Audio section for a Live TV stream](https://github.com/jellyrock/jellyrock/blob/main/docs/progress.md#components): a display gap on 12.x. - [Show the playback URL in Playback Info, admin-only](https://github.com/jellyrock/jellyrock/blob/main/docs/progress.md#components) ## Issues Ref #988 Ref #989 ## Docs / context updates - [x] **Architecture doc** updated if a system's *shape* or *why* changed → `docs/architecture/<topic>.md` - [ ] **`docs/dev/` how-to** updated if a workflow / recipe changed (adding a setting, writing a migration, etc.) - [ ] **Subdir `CLAUDE.md`** updated if a per-area rule / convention changed - [x] **`docs/adr/`** ADR added if an architectural / hard-to-reverse / cross-component decision was made (sub-architectural choice → **`docs/decisions.md`** note) - [ ] **`docs/architecture/tech-debt.md`** entry removed if this PR fixes a listed item, or added if this PR introduces new debt or defers a follow-up - [ ] None — this PR doesn't change any of the above <!-- /pr render: sha=e5d70c03610b6df2f5fa8de1772816fcc57fc4d4 ts=2026-09-22T01:33:56Z -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
InferAudioCodecno longer echoes an unrecognised container back as a codecM3UTunerHoststops advertising direct play for channel URLs that point at an HLS/DASH manifest, reading the extension from the URI path so a query string doesn't hide itStreamBuilderrules out direct play for any source probed as hls/applehttp/dash, covering manifest URLs with no extension and other tuner hosts/Videos/{id}/Code assistance
Claude Opus 5 for reproduction help and tests
Issues
Fixes #17765