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 .