Playing a video meant asking the server to re-encode it, always. That
decision was made nowhere and written down nowhere, so whoever needed it
re-derived it downstream — the player worked out whether it had been handed
a playlist by looking for ".m3u8" in the URL, in two places. A viewer paid
for a transcode of a file their device could have played untouched, and the
app could not tell them which it was.
One negotiation now produces one self-describing StreamSelection — direct
play, remux or transcode; over a playlist, a plain HTTP file, or a local one
— and every renderer consumes that same answer.
Measured against the development server (Jellyfin 10.11.5), 400 items
sampled for codec mix and 40 put through a real PlaybackInfo negotiation
per profile:
Linux / WebKitGTK (h264 only, 2ch) 3/40 — 7% direct play
Android / ExoPlayer (hevc, ac3/eac3, 6ch) 34/40 — 85% direct play
The library is ~80% hevc, which is why the two diverge so hard. The payoff
is overwhelmingly Android, where 85% of plays were starting a transcode
nobody needed. Linux stays near 7% until libmpv decodes the picture — the
h264-only profile is a WebKitGTK constraint, not a JellyTau choice.
DR-219 StreamSelection: url + tagged Transport (hls/progressive/localFile)
+ PlaybackKind (directPlay/directStream/transcode) + the negotiated
rendition + this source's ladder + a needs_transcoding flag derived
in Rust so the rule is answered once. Both enums are serde-tagged
so the frontend matches a discriminant, not a substring. The paths
that never negotiate get the same shape from Rust rather than
assembling one — media_local_selection for a downloaded file,
LiveStreamInfo.transport for a live channel — so there is no second
place where a transport is decided.
DR-220 The ceiling becomes two levels: a durable device default (Settings,
persisted) and a per-playback override the in-player picker sets.
The picker had called itself a "this film, this connection" control
since it was written but wrote the process-wide default, so dropping
one awkward film to 2 Mbps silently capped every video played
afterwards for the rest of the process, with Settings still showing
the old value. The override is cleared whenever playback moves to a
new item, which stops it surviving into an autoplayed next episode.
effective_streaming_quality() is the single resolution point.
DR-221 The quality picker is filled from what this media source can offer.
Rust marks a rung exceeds_source when its ceiling is at or above the
source's own bitrate — such a rung is another way to spell Original
— and the frontend does not draw those. Original is never marked; a
source whose bitrate the server does not report marks nothing, which
keeps every rung offered.
DR-222 Direct play and direct stream are negotiated, with two client-side
overrides on top because the server's answer is right about the file
and wrong about what this app will do with it: undecodable audio
(Jellyfin 10.11.5 honours a DirectPlayProfile's container and video
codec but ignores its audio codec, so it offers direct play for an
E-AC-3 track the webview renders in silence) and a viewer-pinned
audio track the file does not default to. A direct stream is a remux
and is deliberately not counted as transcoding.
DR-223 Dropped on measurement, not deferred. A master playlist from this
server carries exactly one EXT-X-STREAM-INF: Jellyfin builds it from
the single rendition the request asked for rather than publishing a
ladder. So there is no adaptation for hls.js to be preserving and
none mpv would lose — the claim that there was, in
playback-backend-unification.md, does not hold. Recorded rather than
deleted because it is a measurement: a server that does publish a
ladder would change the answer.
DR-224 Every backend consumes the same selection. The queue item carries
the transport, so player_seek_video picks its seek strategy from the
backend's decision instead of the last stream_url.contains(".m3u8")
in the codebase. Items queued by a path that never negotiated carry
None and fall back to needs_transcoding, which is exact rather than
a guess because every transcode this app requests is HLS (DR-140).
The frontend loader decision moves to streamTransport.ts so it can be
tested: the two cases that pin it are the ones that failed against the old
implementation — a progressive stream whose URL contains ".m3u8" must not
get an HLS loader, and an HLS stream whose URL contains none must.
Also verified the URL the direct-play branch builds actually serves playable
bytes: 206, video/mp4, valid ISO-BMFF, and a mid-file range works, so
seeking a direct play works.
The spec is folded into docs/architecture/{01,02,03} and deleted, per the
rule that docs/specs holds only work that has not shipped. DR-121 leaves
read-through-media-cache.md with a pointer; that spec keeps its capture half.
Not verified: real playback on a device. Direct play changes what actually
gets played, and neither fixtures nor curl prove the WebKitGTK and ExoPlayer
paths render it.
81 lines
5.5 KiB
Markdown
81 lines
5.5 KiB
Markdown
# Specs index
|
|
|
|
Feature specs for JellyTau. Start a new one from
|
|
[SPEC-TEMPLATE.md](SPEC-TEMPLATE.md) and run it past
|
|
[SPEC-REVIEW-CHECKLIST.md](SPEC-REVIEW-CHECKLIST.md) before accepting it.
|
|
|
|
## What lives here
|
|
|
|
**Only work that has not shipped.** Once a spec is fully implemented its design
|
|
is folded into the architecture docs — which are the maintained description of
|
|
the build — and the spec file is deleted. Git history keeps the original,
|
|
including its rejected alternatives and acceptance criteria; the architecture
|
|
docs keep the reasoning that a future change still needs.
|
|
|
|
So: a file in this directory is a **promise, not a description**. If you want to
|
|
know how something *works*, read
|
|
[docs/architecture/](../architecture/README.md). If you want to know what is
|
|
*planned*, read here.
|
|
|
|
**Status vocabulary**
|
|
|
|
| Status | Meaning |
|
|
|---|---|
|
|
| Proposed | Written, not accepted. Nothing built. |
|
|
| Accepted | Agreed as the design; implementation not started or not finished. |
|
|
| Partially implemented | Some parts shipped; the spec names what is left. |
|
|
| Design authority | No code of its own — it records a decision later specs act on. |
|
|
|
|
**Next free requirement ids** (always re-check
|
|
[requirements.md](../requirements.md) before allocating): **UR-079**,
|
|
**IR-033**, **DR-231**. Three specs below suggested ids that have since been
|
|
taken by other work; each carries a ⚠️ note at the top.
|
|
|
|
## Partially implemented
|
|
|
|
| Spec | What landed | What is left |
|
|
|---|---|---|
|
|
| [frontend-domain-model.md](frontend-domain-model.md) | Catalog surface: `MediaKind`, `from_jellyfin` isolated, ticks → ms | `primaryImageTag` → `imageId` (~30 sites); player/session/reporting tick math; `stream.type` |
|
|
| [libmpv2-migration.md](libmpv2-migration.md) | `LICENSE` | The `libmpv` → `libmpv2` crate swap |
|
|
| [read-through-media-cache.md](read-through-media-cache.md) | DR-126…128, DR-133…138 — cache entries *are* download rows; local playback of downloads | DR-122/124/125 — the read-through capture. DR-121 shipped as backend-owned stream selection and left this spec |
|
|
| [scoped-search-boundary-implementation.md](scoped-search-boundary-implementation.md) | Stage 1: `SearchScope` owned by Rust (DR-063…067) | Stage 2: result-side grouping (`GROUP_ITEM_TYPES` still in `searchScope.ts`) |
|
|
|
|
## Not started
|
|
|
|
| Spec | Blocked on / note |
|
|
|---|---|
|
|
| [build-provenance.md](build-provenance.md) | `build.rs` is still bare. ⚠️ suggested id DR-093 is taken. |
|
|
| [player-facade-enforcement.md](player-facade-enforcement.md) | ~60 `commands.player*` sites still outside the facade; no lint rule. ⚠️ suggested id DR-095 is taken. |
|
|
| [windows-native-audio-backend.md](windows-native-audio-backend.md) | Blocked on the libmpv2 swap. ⚠️ suggested id IR-030 is taken. |
|
|
| [linux-native-video-spike.md](linux-native-video-spike.md) | **Spike run 2026-08-21: compositing works on Linux, X11 and Wayland.** G1-G6 green bar the Tauri `default_vbox()` half of G1. The adaptive-bitrate question it was waiting on is **answered**: the server publishes one `EXT-X-STREAM-INF`, so there is no ladder for mpv to lose (DR-228). `StreamSelection` (DR-224) is the contract to consume. |
|
|
|
|
## Design authority
|
|
|
|
| Spec | Role |
|
|
|---|---|
|
|
| [playback-backend-unification.md](playback-backend-unification.md) | Why video cannot unify onto one native engine and audio can. The audio half has since shipped on Android; Windows has not. |
|
|
| [scoped-search-boundary.md](scoped-search-boundary.md) | The boundary design the `check:boundary` rule came from. Stage 1 built. |
|
|
| [scoped-search.md](scoped-search.md) | Superseded in part — its "frontend only, no Rust changes" decision is the leak the boundary spec reversed. UX still current. |
|
|
|
|
## Where the shipped specs went
|
|
|
|
Sixteen specs were folded into the architecture docs and deleted (2026-08-21).
|
|
Where to look for each:
|
|
|
|
| Shipped work | Now documented in |
|
|
|---|---|
|
|
| Account menu & global chrome | [02-svelte-frontend.md](../architecture/02-svelte-frontend.md) — App Shell and Chrome |
|
|
| Library mosaic | [02-svelte-frontend.md](../architecture/02-svelte-frontend.md) — Library Mosaic |
|
|
| Series current-episode navigation | [02-svelte-frontend.md](../architecture/02-svelte-frontend.md) — Series and Episode Navigation |
|
|
| Downloads as an offline library | [02-svelte-frontend.md](../architecture/02-svelte-frontend.md) — Downloaded Browse |
|
|
| Favourites browsing | [01-rust-backend.md](../architecture/01-rust-backend.md) — Favorites System |
|
|
| Streaming bitrate cap | [01-rust-backend.md](../architecture/01-rust-backend.md) — Streaming quality ladder |
|
|
| Locally-indexed search | [03-data-flow.md](../architecture/03-data-flow.md) — Search Flow; [01-rust-backend.md](../architecture/01-rust-backend.md) — Background workers |
|
|
| Offline downloaded-only filter | [06-downloads-and-offline.md](../architecture/06-downloads-and-offline.md) — Offline Catalog Visibility |
|
|
| Audio equalizer · Android audio settings parity | [05-platform-backends.md](../architecture/05-platform-backends.md) — Audio settings on ExoPlayer |
|
|
| Android native video spike | [05-platform-backends.md](../architecture/05-platform-backends.md) — Native Video Compositing |
|
|
| Video background audio | [05-platform-backends.md](../architecture/05-platform-backends.md) — Background Audio Handoff |
|
|
| Traceability gate repair | [traceability-ci.md](../traceability-ci.md) |
|
|
| Boundary tripwire hardening | `scripts/check-frontend-boundary.sh` (its header is the spec) |
|
|
| Playback docs corrections · req-coverage script removal | Nothing to document — both were corrections that have been applied |
|