Four small things landed in main on top of the
setup-wizard / mini-bar / desktop batch
—
all in the streaming path. None of them open a new phase; together
they close the last obvious gap in the Phase 5 ABR story and take a
sharp edge off the new mini-bar player.
The ladder goes to 2160p
The ABR ladder used to top out at 1080p. It now runs from 360p
through 2160p, with H.264 Level 5.1 codec hints on the 4K rung so
clients don’t reject the variant on profile-level grounds. The 4K
tier is wired into the same TranscodeManager path as every other
rung — no special-casing on the encoder side.
…but only when the source can actually feed it
A 4K rung you offer to a 1080p file is a 4K rung you’ll upscale into
existence. That’s a waste of cycles and looks worse than the
original. So tiers that would only upscale the source are now pruned
from the master playlist before ffmpeg ever runs:
// mythos-stream
pub fn prune_for_source(ladder: &[Rendition], src_w: u32, src_h: u32) -> Vec<Rendition> { … }
The comparison is width-aware: it pits
max(source_w, source_h) against max(tier_w, tier_h) rather than
just height-vs-height. Why: cinematic 4K masters are routinely
3840×1606 (≈ 2.39:1 letterbox), and a naive height check would
strike them down to 1080p even though they obviously belong on the
4K rung. With the width-aware rule a 3840-wide source keeps the
2160p tier; a 1920×1080 source drops it.
Pruning happens in three places — /play, /playback, and
hls::resolve_renditions — so a hand-crafted ?v=2160p URL on a
1080p source still can’t steer the encoder into upscaling.
Defense in depth on the filter side: the encoder’s scale_filter is
fit-in-box, not pin-height. Each tier is treated as a box the
output fits inside via
scale=w='min(iw,W)':h='min(ih,H)':force_original_aspect_ratio=decrease
(with the matching scale_vaapi / scale_cuda forms on the
hardware paths). So picking the 2160p tier on a 3840×1606 source
produces 3840×1606, not an upscaled 5165×2160 — and a slip past
prune_for_source still can’t produce an upscale. The HLS master
still advertises the tier’s nominal RESOLUTION attribute, which
is a hint to clients, not a contract on the encoded dimensions.
A manual quality picker
The client used to pass a max_height and let the server cap the
ladder accordingly. That’s been pulled out — max_height is
intentionally not applied to the ladder anymore. Instead the
player chrome grows a quality picker that sits next to Auto and
exposes every source-feasible tier the master advertises. Pin to
1080p on a 4K source; pin to 4K on a 1080p screen; let Auto drive
when you don’t care.
The backend interface, in web/src/lib/player/backend.ts:
setQualityLevel(idx: number): void
onQualityLevels(cb: (levels: QualityLevel[]) => void): Unsubscribe
onQualityState(cb: (state: QualityState) => void): Unsubscribe
-1 means “hand back to ABR auto”; any other index pins to that
level of the master. The web backend mirrors onto hls.currentLevel
and listens for LEVEL_SWITCHED so the UI can show what ABR
actually picked in Auto mode. Subscribers get a replay of the
cached state on subscribe, so a player that mounts late (after
MANIFEST_PARSED has already fired) doesn’t sit empty waiting for
the next event.
The mpv backend stubs setQualityLevel to a no-op against an empty
levels list — mpv’s internal HLS demuxer doesn’t expose ABR
variants over IPC, and the desktop client typically direct-plays
anyway.
Mini-bar: exit fullscreen on minimize
A small but vexing bug in the mini-bar player : if you minimized while the modal was in fullscreen, the controls vanished — the page went black, no mini-bar appeared, and the only escape was Esc again.
The cause: HTML5 fullscreen pins <media-controller> as the
fullscreen element. A pure-CSS layout swap (the data-mode
attribute flip from 'modal' to 'mini') does not dislodge it.
So the mini-bar styles were applied to an element the browser was
still rendering edge-to-edge as a fullscreen page.
The fix is one line on the session-singleton side:
// playbackSession.minimize()
if (document.fullscreenElement) await document.exitFullscreen();
mode = 'mini';
Exit fullscreen before flipping the mode and the mini-bar gets to render where it’s supposed to. Verified across modal-fullscreen, PiP-from-fullscreen, and PiP-from-modal entry points.
What this is not
It’s worth being clear what this batch doesn’t do. There’s still no per-codec bitrate ceiling — the 4K rung uses the same bitrate ladder shape as the 1080p one, just scaled. There’s no per-user remembered quality preference yet — pinning a rung is a one-shot per session. And mpv’s ABR variants still aren’t surfaced; pinning quality from the desktop client is a no-op there. All of that is on the list, none of it is in this batch.
What’s next
Still Phase 3 sub-phases: music, photos, books. Phase 6 (the Jellyfin-API compatibility shim) waits for that to land. The desktop client’s child-native-widget embed surface and proper Wayland support stay on the parallel-follow-up track.
Source on GitLab .