chore(traceability): shift this branch's ids clear of master's

Master allocated DR-224 and UT-211 while this branch was in flight — the third
collision on this work. Everything here moves up by one: DR-224..236 become
DR-225..237, UT-211..213 become UT-212..214. UR-079, UR-080 and IR-033 were
still free and are unchanged.

Mechanical, and matched on each row's own text rather than on its number, so a
row cannot be shifted twice or the wrong one caught. Master's DR-224 (the
background-audio toggle) and UT-211 are untouched.
This commit is contained in:
2026-08-22 13:45:04 +02:00
parent 156b9e3684
commit fecd6022fe
30 changed files with 5309 additions and 5070 deletions
+5 -5
View File
@@ -675,7 +675,7 @@ source file's own bitrate, and no URL parameter afterwards can reduce it.
#### Two levels of ceiling #### Two levels of ceiling
**Location**: `src-tauri/src/repository/online.rs` (TRACES: UR-074, UR-079 | DR-225) **Location**: `src-tauri/src/repository/online.rs` (TRACES: UR-074, UR-079 | DR-226)
There are two, and they are not the same thing: There are two, and they are not the same thing:
@@ -703,7 +703,7 @@ to constrain, or the reverse.
### Stream selection ### Stream selection
**Location**: `src-tauri/src/repository/stream_selection.rs`, **Location**: `src-tauri/src/repository/stream_selection.rs`,
`OnlineRepository::get_stream_selection` (TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227) `OnlineRepository::get_stream_selection` (TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228)
**Rust decides *what stream*. The player decides *how to deliver it*.** That line **Rust decides *what stream*. The player decides *how to deliver it*.** That line
is the whole design. A backend with genuine adaptive selection (ExoPlayer over a is the whole design. A backend with genuine adaptive selection (ExoPlayer over a
@@ -719,7 +719,7 @@ the bare URL `get_video_stream_url` used to hand out:
| `transport` | `Hls` / `Progressive` / `LocalFile` — how to fetch it | | `transport` | `Hls` / `Progressive` / `LocalFile` — how to fetch it |
| `playback_kind` | `DirectPlay` / `DirectStream` / `Transcode` — what the server is doing to the source | | `playback_kind` | `DirectPlay` / `DirectStream` / `Transcode` — what the server is doing to the source |
| `rendition` | The negotiated ceiling and codecs; `None` for a direct play, which *is* the source | | `rendition` | The negotiated ceiling and codecs; `None` for a direct play, which *is* the source |
| `available` | The quality ladder as it applies to this media source (DR-226) | | `available` | The quality ladder as it applies to this media source (DR-227) |
| `needs_transcoding` | Derived from `playback_kind`, so the rule is answered once | | `needs_transcoding` | Derived from `playback_kind`, so the rule is answered once |
Both enums are serde-tagged (`{"type":"hls"}`) so the frontend matches a Both enums are serde-tagged (`{"type":"hls"}`) so the frontend matches a
@@ -777,7 +777,7 @@ a free passthrough as a server-side re-encode.
> in its `MediaCodecList` — no Dolby licence, which is normal for a tablet — so > in its `MediaCodecList` — no Dolby licence, which is normal for a tablet — so
> eac3 content, about a third of the sampled library, correctly transcodes there. > eac3 content, about a third of the sampled library, correctly transcodes there.
> What any given device achieves depends on its own codec list, and on the > What any given device achieves depends on its own codec list, and on the
> profile being derived from the renderer at all (DR-233), which it was not when > profile being derived from the renderer at all (DR-234), which it was not when
> the figure was taken. > the figure was taken.
> >
> **The payoff is still overwhelmingly Android**, because that is where a real > **The payoff is still overwhelmingly Android**, because that is where a real
@@ -801,7 +801,7 @@ they are.
#### No adaptive ladder to preserve #### No adaptive ladder to preserve
**TRACES: UR-079 | DR-228 (Won't Do)** **TRACES: UR-079 | DR-229 (Won't Do)**
Mid-playback re-negotiation on throughput was scoped and dropped on measurement. Mid-playback re-negotiation on throughput was scoped and dropped on measurement.
A master playlist from this server carries exactly **one** `EXT-X-STREAM-INF`: A master playlist from this server carries exactly **one** `EXT-X-STREAM-INF`:
+2 -2
View File
@@ -805,7 +805,7 @@ can safely be re-sent on resume.
## Stream Transport ## Stream Transport
**Location**: `src/lib/player/streamTransport.ts` **Location**: `src/lib/player/streamTransport.ts`
**TRACES**: UR-079 | DR-224 | UT-213 **TRACES**: UR-079 | DR-225 | UT-214
`videoLoaderFor(selection, capabilities)` picks the loader for the webview `videoLoaderFor(selection, capabilities)` picks the loader for the webview
`<video>` element — `hlsjs`, `nativeHls`, or `direct` — from the backend's tagged `<video>` element — `hlsjs`, `nativeHls`, or `direct` — from the backend's tagged
@@ -835,7 +835,7 @@ drift apart. The background-audio handoff states the transport it is moving to
progressive mp3 out, HLS back — via `selectionAt()`, rather than leaving it to be progressive mp3 out, HLS back — via `selectionAt()`, rather than leaving it to be
inferred. inferred.
The quality picker is filled from `selection.available` (DR-226): rungs the The quality picker is filled from `selection.available` (DR-227): rungs the
backend marked `exceedsSource` are not drawn, because they produce the same bytes backend marked `exceedsSource` are not drawn, because they produce the same bytes
as `Original`. Nothing is optimistically assigned when the viewer picks a rung — as `Original`. Nothing is optimistically assigned when the viewer picks a rung —
what the menu shows comes from the selection the backend hands back, since a what the menu shows comes from the selection the backend hands back, since a
+1 -1
View File
@@ -134,7 +134,7 @@ sequenceDiagram
## Video Stream Selection Flow ## Video Stream Selection Flow
**TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227** **TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228**
Before a video plays, Rust decides *what stream* — direct play, remux or Before a video plays, Rust decides *what stream* — direct play, remux or
transcode, over which transport — and hands the player one self-describing transcode, over which transport — and hands the player one self-describing
+2 -2
View File
@@ -28,7 +28,7 @@ know how something *works*, read
**Next free requirement ids** (always re-check **Next free requirement ids** (always re-check
[requirements.md](../requirements.md) before allocating): **UR-079**, [requirements.md](../requirements.md) before allocating): **UR-079**,
**IR-033**, **DR-231**. Three specs below suggested ids that have since been **IR-033**, **DR-232**. Three specs below suggested ids that have since been
taken by other work; each carries a ⚠️ note at the top. taken by other work; each carries a ⚠️ note at the top.
## Partially implemented ## Partially implemented
@@ -48,7 +48,7 @@ taken by other work; each carries a ⚠️ note at the top.
| [build-provenance.md](build-provenance.md) | `build.rs` is still bare. ⚠️ suggested id DR-093 is taken. | | [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. | | [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. | | [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. | | [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-229). `StreamSelection` (DR-225) is the contract to consume. |
## Design authority ## Design authority
+31 -31
View File
@@ -1,16 +1,16 @@
# Spec: Desktop native video — mpv renders the picture, everywhere # Spec: Desktop native video — mpv renders the picture, everywhere
**Status:** Proposed **Status:** Proposed
**Requirements:** UR-080 (new) → DR-230 … DR-236 (new); IR-033 (new) **Requirements:** UR-080 (new) → DR-231 … DR-237 (new); IR-033 (new)
**UX spec:** n/a — nothing about the player's appearance changes. What changes is **UX spec:** n/a — nothing about the player's appearance changes. What changes is
what is behind the controls. what is behind the controls.
**Supersedes / revises:** consumes and closes **Supersedes / revises:** consumes and closes
[linux-native-video-spike.md](linux-native-video-spike.md), whose gates [linux-native-video-spike.md](linux-native-video-spike.md), whose gates
authorised exactly this spec and nothing more. Settles finding 2 of authorised exactly this spec and nothing more. Settles finding 2 of
[playback-backend-unification.md](playback-backend-unification.md) on the [playback-backend-unification.md](playback-backend-unification.md) on the
desktop; finding 3 was already settled by DR-228. Absorbs the video half of what desktop; finding 3 was already settled by DR-229. Absorbs the video half of what
[windows-native-audio-backend.md](windows-native-audio-backend.md) leaves open. [windows-native-audio-backend.md](windows-native-audio-backend.md) leaves open.
**Depends on:** backend-owned stream selection (DR-224 … DR-229), the branch **Depends on:** backend-owned stream selection (DR-225 … DR-230), the branch
below this one. mpv is a *consumer* of `StreamSelection`, never a second place to below this one. mpv is a *consumer* of `StreamSelection`, never a second place to
decide what to play. decide what to play.
@@ -42,7 +42,7 @@ profile therefore claims `h264` alone. That is not a statement about the machine
— the same machine runs mpv, which decodes essentially everything in the library — the same machine runs mpv, which decodes essentially everything in the library
— it is a statement about which widget is holding the frame. — it is a statement about which widget is holding the frame.
DR-227 made the cost measurable. Over 40 items negotiated against the development DR-228 made the cost measurable. Over 40 items negotiated against the development
server: server:
| Profile | Direct play | | Profile | Direct play |
@@ -60,7 +60,7 @@ can actually do, and that — not the compositing — is the product.
> containing `ac3,eac3`. The Android device later used for verification reports > containing `ac3,eac3`. The Android device later used for verification reports
> neither in its `MediaCodecList` — no Dolby licence, normal for a tablet — so > neither in its `MediaCodecList` — no Dolby licence, normal for a tablet — so
> eac3 content, about a third of the sampled library, correctly transcodes there. > eac3 content, about a third of the sampled library, correctly transcodes there.
> Realising any of this depends on DR-233, deriving the profile from the renderer > Realising any of this depends on DR-234, deriving the profile from the renderer
> rather than from the platform, which is why that requirement is load-bearing > rather than from the platform, which is why that requirement is load-bearing
> and not tidy-up. > and not tidy-up.
@@ -86,10 +86,10 @@ follow-up that never gets written.
1. **Compositing works, including Wayland.** The spike ran all six gates; the 1. **Compositing works, including Wayland.** The spike ran all six gates; the
2024 "not possible on Wayland at all" claim is out of date when the render API 2024 "not possible on Wayland at all" claim is out of date when the render API
is used instead of foreign-window embedding. is used instead of foreign-window embedding.
2. **There is no ABR to lose.** DR-228: the server's master playlist carries one 2. **There is no ABR to lose.** DR-229: the server's master playlist carries one
`EXT-X-STREAM-INF`. hls.js was demuxing, not adapting. `EXT-X-STREAM-INF`. hls.js was demuxing, not adapting.
3. **A direct-play path exists.** It did not when the spike was written. DR-227 3. **A direct-play path exists.** It did not when the spike was written. DR-228
built it; DR-229 proved the contract is player-agnostic. built it; DR-230 proved the contract is player-agnostic.
And on Windows specifically, `tauri-plugin-libmpv` lists Windows as its **fully And on Windows specifically, `tauri-plugin-libmpv` lists Windows as its **fully
tested** platform — the inverse of the Linux situation the spike had to tested** platform — the inverse of the Linux situation the spike had to
@@ -101,9 +101,9 @@ disprove. The embedding difficulty was always WebKitGTK-specific.
|---|---|---| |---|---|---|
| **Which codecs this device can decode** | **Rust** | Domain: it is the input to Jellyfin's `PlaybackInfo` negotiation. It stops being a property of the *platform* and becomes a property of *the renderer in use* — see "The structural change". | | **Which codecs this device can decode** | **Rust** | Domain: it is the input to Jellyfin's `PlaybackInfo` negotiation. It stops being a property of the *platform* and becomes a property of *the renderer in use* — see "The structural change". |
| Which backend renders video | **Rust** | Rust already owns this (`use_html5_element` / `VideoBackend`). It stops being a `cfg!` constant and becomes a runtime fact. | | Which backend renders video | **Rust** | Rust already owns this (`use_html5_element` / `VideoBackend`). It stops being a `cfg!` constant and becomes a runtime fact. |
| What stream to play (direct / remux / transcode, transport, ceiling) | **Rust — already decided** | DR-224. mpv consumes `StreamSelection`. Re-deriving any of it in a new backend would be the defect DR-224 exists to remove, restated. | | What stream to play (direct / remux / transcode, transport, ceiling) | **Rust — already decided** | DR-225. mpv consumes `StreamSelection`. Re-deriving any of it in a new backend would be the defect DR-225 exists to remove, restated. |
| Creating the GL surface, reparenting the webview, owning the render context | **Rust (platform layer)** | Native window and GL-context lifetime. Not presentation, and not expressible above the IPC boundary at all. | | Creating the GL surface, reparenting the webview, owning the render context | **Rust (platform layer)** | Native window and GL-context lifetime. Not presentation, and not expressible above the IPC boundary at all. |
| Render-context ↔ GL-context lifetime binding | **Rust** | A correctness invariant over native resources. DR-231. | | Render-context ↔ GL-context lifetime binding | **Rust** | A correctness invariant over native resources. DR-232. |
| Frame pacing (update callback, `report_swap`) | **Rust** | Timing against the compositor; mpv's own contract. | | Frame pacing (update callback, `report_swap`) | **Rust** | Timing against the compositor; mpv's own contract. |
| Hardware-decode selection | **Rust** | A capability question about the machine, answered from what mpv reports it actually selected. | | Hardware-decode selection | **Rust** | A capability question about the machine, answered from what mpv reports it actually selected. |
| Z-order of controls over video, overlay chrome, letterbox colour | **Frontend / mpv** | Presentation. Controls already draw over a transparent webview on Android; mpv paints its own letterbox bars (better than the Android equivalent, which shipped DR-194 as a defect). | | Z-order of controls over video, overlay chrome, letterbox colour | **Frontend / mpv** | Presentation. Controls already draw over a transparent webview on Android; mpv paints its own letterbox bars (better than the Android equivalent, which shipped DR-194 as a defect). |
@@ -127,7 +127,7 @@ derived from **which renderer will decode this stream**, which is runtime state.
It looks like configuration and is not: it is the input that decides whether the It looks like configuration and is not: it is the input that decides whether the
server re-encodes, it changes when Jellyfin's API or our renderer changes, and server re-encodes, it changes when Jellyfin's API or our renderer changes, and
getting it wrong fails *silently* — a claimed codec the renderer cannot decode is getting it wrong fails *silently* — a claimed codec the renderer cannot decode is
a black picture or silence, which is DR-148 and DR-227's audio override already. a black picture or silence, which is DR-148 and DR-228's audio override already.
**Write this against "the active video renderer", never `cfg!(target_os)`.** It **Write this against "the active video renderer", never `cfg!(target_os)`.** It
is the single piece that must not be Linux-shaped, because phase 2 reuses it is the single piece that must not be Linux-shaped, because phase 2 reuses it
@@ -135,7 +135,7 @@ unchanged.
## Design ## Design
### Backend and compositing (DR-230, IR-033) ### Backend and compositing (DR-231, IR-033)
An `MpvVideoBackend` beside the existing `MpvBackend` (audio). The mpv side — An `MpvVideoBackend` beside the existing `MpvBackend` (audio). The mpv side —
render context, FBO, update callback, hwdec — is **shared**; only the surface render context, FBO, update callback, hwdec — is **shared**; only the surface
@@ -168,9 +168,9 @@ which looks like a platform limitation and is not:
returning the symbol's own address makes mpv jump into non-executable data and returning the symbol's own address makes mpv jump into non-executable data and
take SIGSEGV on the first GL call. The `epoxy` crate does this correctly but is take SIGSEGV on the first GL call. The `epoxy` crate does this correctly but is
unusable — its `gl_generator` dependency pulls a yanked `xml-rs`. unusable — its `gl_generator` dependency pulls a yanked `xml-rs`.
3. **Frame pacing is not optional and its symptom misleads.** See DR-232. 3. **Frame pacing is not optional and its symptom misleads.** See DR-233.
### Render-context lifetime (DR-231) — the crash defence ### Render-context lifetime (DR-232) — the crash defence
The spike's one unexplained SIGSEGV landed in a *decoder* thread with no Tauri, The spike's one unexplained SIGSEGV landed in a *decoder* thread with no Tauri,
GTK or GL frame in the stack, and three plausible causes failed to reproduce it GTK or GL frame in the stack, and three plausible causes failed to reproduce it
@@ -193,7 +193,7 @@ reproduce:
If the crash recurs after this, it is a different bug and the likeliest cause is If the crash recurs after this, it is a different bug and the likeliest cause is
out of the search space. If it does not, we needed this anyway. out of the search space. If it does not, we needed this anyway.
### Frame pacing (DR-232) ### Frame pacing (DR-233)
Register `mpv_render_context_set_update_callback`; redraw only when it reports a Register `mpv_render_context_set_update_callback`; redraw only when it reports a
frame ready; call `mpv_render_context_report_swap` after each render. frame ready; call `mpv_render_context_report_swap` after each render.
@@ -203,7 +203,7 @@ frame clock every tick without reporting the swap leaves mpv nothing to time
against. It looks fine in a window and **judders at fullscreen**, which reads as against. It looks fine in a window and **judders at fullscreen**, which reads as
a compositing or GPU limit and is neither. a compositing or GPU limit and is neither.
### Renderer-dependent device profile (DR-233) ### Renderer-dependent device profile (DR-234)
`build_device_profile` takes the active video renderer and derives the codec `build_device_profile` takes the active video renderer and derives the codec
lists from it: lists from it:
@@ -222,9 +222,9 @@ constraints stay, sourced from the renderer rather than assumed.
**This is what converts the 7% figure upward** (toward, not necessarily to, the 85% ceiling — see the caveat above), and it is also the change most able to break **This is what converts the 7% figure upward** (toward, not necessarily to, the 85% ceiling — see the caveat above), and it is also the change most able to break
playback silently — so it lands after compositing is proven, covered by the playback silently — so it lands after compositing is proven, covered by the
DR-227 override tests. DR-228 override tests.
### Deleting the webview video path (DR-234) ### Deleting the webview video path (DR-235)
`get_player_status` stops reporting `use_html5_element: true` on desktop; `get_player_status` stops reporting `use_html5_element: true` on desktop;
`supports_native_video` becomes true there. `supports_native_video` becomes true there.
@@ -251,7 +251,7 @@ backend, emit `backend-init-failed`, and surface a real error rather than a blac
rectangle. An honest failure beats a hidden downgrade to the transcode we are rectangle. An honest failure beats a hidden downgrade to the transcode we are
trying to stop paying for. trying to stop paying for.
### Hardware decode (DR-235) ### Hardware decode (DR-236)
The spike established the load-bearing fact: **hardware decode works through the The spike established the load-bearing fact: **hardware decode works through the
render API** (`hwdec-current` reported `nvdec-copy` on the discrete GPU), so the render API** (`hwdec-current` reported `nvdec-copy` on the discrete GPU), so the
@@ -270,12 +270,12 @@ asked for:
- Log `hwdec-current` at start-up; knowing what was actually chosen is the whole - Log `hwdec-current` at start-up; knowing what was actually chosen is the whole
diagnostic value. diagnostic value.
### Windows: what phase 2 actually costs (DR-236) ### Windows: what phase 2 actually costs (DR-237)
Not hidden, because it is the part most likely to be underestimated: Not hidden, because it is the part most likely to be underestimated:
- **The surface is different code.** WebView2 in an HWND, not GTK. A transparent - **The surface is different code.** WebView2 in an HWND, not GTK. A transparent
WebView2 over a native child window is a solved arrangement, but DR-230's WebView2 over a native child window is a solved arrangement, but DR-231's
Linux surface does not transfer. Everything else does. Linux surface does not transfer. Everything else does.
- **libmpv is currently a Linux-only dependency**, and Windows is - **libmpv is currently a Linux-only dependency**, and Windows is
**cross-compiled from Linux** via `x86_64-pc-windows-msvc` + `cargo-xwin`. Phase **cross-compiled from Linux** via `x86_64-pc-windows-msvc` + `cargo-xwin`. Phase
@@ -299,7 +299,7 @@ and shrinks to the surface.
- **Audio backends.** mpv already plays audio on Linux; this adds a video - **Audio backends.** mpv already plays audio on Linux; this adds a video
renderer beside it. Windows audio is its own spec. renderer beside it. Windows audio is its own spec.
- **HDR, tone mapping, multi-window.** Not exercised by the spike at all. - **HDR, tone mapping, multi-window.** Not exercised by the spike at all.
- **Re-deciding what stream to play.** DR-224 owns that. If this spec finds - **Re-deciding what stream to play.** DR-225 owns that. If this spec finds
itself choosing a URL, something has gone wrong. itself choosing a URL, something has gone wrong.
## Acceptance criteria ## Acceptance criteria
@@ -353,7 +353,7 @@ and shrinks to the surface.
## Testing ## Testing
- **Rust, pure:** the device profile per renderer — mpv claims hevc, the webview - **Rust, pure:** the device profile per renderer — mpv claims hevc, the webview
does not, the multichannel bound survives both. The DR-233 table as a does not, the multichannel bound survives both. The DR-234 table as a
table-driven test. table-driven test.
- **Rust, pure:** `PlaybackInfo` fixtures that transcode under the webview - **Rust, pure:** `PlaybackInfo` fixtures that transcode under the webview
profile and direct-play under the mpv profile — the direct-play conversion as a unit profile and direct-play under the mpv profile — the direct-play conversion as a unit
@@ -370,13 +370,13 @@ and shrinks to the surface.
| Piece | Tag | | Piece | Tag |
|---|---| |---|---|
| mpv video backend + compositing | `UR-080 \| DR-230, IR-033` | | mpv video backend + compositing | `UR-080 \| DR-231, IR-033` |
| Render-context lifetime binding | `UR-080 \| DR-231` | | Render-context lifetime binding | `UR-080 \| DR-232` |
| Frame pacing | `UR-080 \| DR-232` | | Frame pacing | `UR-080 \| DR-233` |
| Renderer-dependent device profile | `UR-080, UR-070 \| DR-233` | | Renderer-dependent device profile | `UR-080, UR-070 \| DR-234` |
| Webview video path removed | `UR-080 \| DR-234` | | Webview video path removed | `UR-080 \| DR-235` |
| Hardware-decode policy | `UR-080 \| DR-235` | | Hardware-decode policy | `UR-080 \| DR-236` |
| Windows surface + cross-build | `UR-080 \| DR-236` | | Windows surface + cross-build | `UR-080 \| DR-237` |
## Notes for the implementer ## Notes for the implementer
@@ -384,7 +384,7 @@ and shrinks to the surface.
hardware-decode table are the most valuable things in this directory, and each hardware-decode table are the most valuable things in this directory, and each
cost a debugging cycle to find. cost a debugging cycle to find.
- **mpv consumes `StreamSelection`; it does not decide.** The transport is on the - **mpv consumes `StreamSelection`; it does not decide.** The transport is on the
queue item (DR-229). If you are parsing a URL, stop. queue item (DR-230). If you are parsing a URL, stop.
- **Guard nothing on `cfg!(target_os = "linux")` that phase 2 will need.** That is - **Guard nothing on `cfg!(target_os = "linux")` that phase 2 will need.** That is
the one avoidable mistake here. the one avoidable mistake here.
- The Android backend is the reference for the *shape* of this — transparent - The Android backend is the reference for the *shape* of this — transparent
+2 -2
View File
@@ -295,13 +295,13 @@ anything.
closed** — hls.js is serving as an HLS demuxer, exactly as (2) above supposed, closed** — hls.js is serving as an HLS demuxer, exactly as (2) above supposed,
and mpv gives up nothing by replacing it. and mpv gives up nothing by replacing it.
Recorded as DR-228 (Won't Do) rather than deleted, because it is a Recorded as DR-229 (Won't Do) rather than deleted, because it is a
measurement: a server that *does* publish a ladder would change the answer, and measurement: a server that *does* publish a ladder would change the answer, and
the re-negotiation path is the hook that work would build on. the re-negotiation path is the hook that work would build on.
**The direct-play path now exists.** It did not when this spike was written — **The direct-play path now exists.** It did not when this spike was written —
every video play went through the HLS transcode endpoint. Backend-owned stream every video play went through the HLS transcode endpoint. Backend-owned stream
selection (DR-224 … DR-229) built it: Rust negotiates direct play / direct selection (DR-225 … DR-230) built it: Rust negotiates direct play / direct
stream / transcode and hands every backend one `StreamSelection` carrying the stream / transcode and hands every backend one `StreamSelection` carrying the
URL, the transport and the chosen rendition. **That is the contract this URL, the transport and the chosen rendition. **That is the contract this
implementation consumes** — mpv is a consumer of a decision already made, not a implementation consumes** — mpv is a consumer of a decision already made, not a
+2 -2
View File
@@ -9,7 +9,7 @@ DR-124, DR-125.
**DR-121 has shipped and left this spec.** The player quality selector, the **DR-121 has shipped and left this spec.** The player quality selector, the
per-playback bitrate ceiling, and the backend-owned stream decision it needed per-playback bitrate ceiling, and the backend-owned stream decision it needed
were built as *backend-owned stream selection* (DR-224 … DR-227) and are were built as *backend-owned stream selection* (DR-225 … DR-228) and are
described in described in
[01-rust-backend.md](../architecture/01-rust-backend.md#stream-selection) and [01-rust-backend.md](../architecture/01-rust-backend.md#stream-selection) and
[03-data-flow.md](../architecture/03-data-flow.md#video-stream-selection-flow). [03-data-flow.md](../architecture/03-data-flow.md#video-stream-selection-flow).
@@ -77,7 +77,7 @@ frontend stores the user's *choice*; Rust decides what that choice resolves to.
### DR-121 — moved out (shipped) ### DR-121 — moved out (shipped)
Bitrate selection in the player shipped as DR-224 … DR-227; see Bitrate selection in the player shipped as DR-225 … DR-228; see
[01-rust-backend.md](../architecture/01-rust-backend.md#stream-selection). [01-rust-backend.md](../architecture/01-rust-backend.md#stream-selection).
The one constraint here that the capture work still has to respect: a quality The one constraint here that the capture work still has to respect: a quality
+5125 -4886
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -187,7 +187,7 @@ pub struct PlayItemRequest {
/// caller falls back to `needs_transcoding` — every transcode this app /// caller falls back to `needs_transcoding` — every transcode this app
/// requests is HLS (DR-140), so that fallback is exact rather than a guess. /// requests is HLS (DR-140), so that fallback is exact rather than a guess.
/// ///
/// TRACES: UR-003, UR-004, UR-079 | DR-224, DR-229 /// TRACES: UR-003, UR-004, UR-079 | DR-225, DR-230
#[serde(default)] #[serde(default)]
pub transport: Option<crate::repository::Transport>, pub transport: Option<crate::repository::Transport>,
@@ -331,7 +331,7 @@ pub enum VideoSeekResponse {
ReloadStream { ReloadStream {
/// What to open, and how — transport included, so the frontend picks /// What to open, and how — transport included, so the frontend picks
/// its loader from a tagged enum rather than by searching the URL for /// its loader from a tagged enum rather than by searching the URL for
/// `.m3u8`. TRACES: UR-079 | DR-224 /// `.m3u8`. TRACES: UR-079 | DR-225
selection: StreamSelection, selection: StreamSelection,
/// `seek_offset` carries the position to RESUME AT, not a base to add to /// `seek_offset` carries the position to RESUME AT, not a base to add to
/// the element's clock. The reloaded stream starts at the item's zero — /// the element's clock. The reloaded stream starts at the item's zero —
@@ -353,7 +353,7 @@ pub enum AudioTrackSwitchResponse {
}, },
/// HTML5 needs to reload stream with new audio track /// HTML5 needs to reload stream with new audio track
ReloadStream { ReloadStream {
/// What to open, and how. TRACES: UR-079 | DR-224 /// What to open, and how. TRACES: UR-079 | DR-225
selection: StreamSelection, selection: StreamSelection,
/// Current position to resume from /// Current position to resume from
position: f64, position: f64,
@@ -379,7 +379,7 @@ pub enum StreamQualityResponse {
/// that would have used the requested value was never reached. The stream /// that would have used the requested value was never reached. The stream
/// changed and the menu did not. /// changed and the menu did not.
/// ///
/// TRACES: UR-074, UR-079 | DR-225, DR-226 /// TRACES: UR-074, UR-079 | DR-226, DR-227
Native { Native {
/// What the backend actually opened, so the UI reflects it rather than /// What the backend actually opened, so the UI reflects it rather than
/// assuming the request was honoured verbatim. /// assuming the request was honoured verbatim.
@@ -392,7 +392,7 @@ pub enum StreamQualityResponse {
/// What to open, and how — already negotiated against the requested /// What to open, and how — already negotiated against the requested
/// ceiling. Carries `available` too, so a picker opened after a quality /// ceiling. Carries `available` too, so a picker opened after a quality
/// change still describes the source correctly. /// change still describes the source correctly.
/// TRACES: UR-070, UR-079 | DR-224, DR-226 /// TRACES: UR-070, UR-079 | DR-225, DR-227
selection: StreamSelection, selection: StreamSelection,
/// Position to resume from. /// Position to resume from.
position: f64, position: f64,
@@ -449,7 +449,7 @@ pub(super) async fn create_media_item(
source, source,
video_codec: Some(req.video_codec), video_codec: Some(req.video_codec),
needs_transcoding: req.needs_transcoding, needs_transcoding: req.needs_transcoding,
// The caller's negotiated transport, when it had one. TRACES: UR-079 | DR-229 // The caller's negotiated transport, when it had one. TRACES: UR-079 | DR-230
transport: req.transport, transport: req.transport,
video_width: None, // Not available from video-only request video_width: None, // Not available from video-only request
video_height: None, // Not available from video-only request video_height: None, // Not available from video-only request
@@ -702,9 +702,9 @@ pub async fn player_play_item(
// A ceiling chosen from the in-player picker belongs to the playback it was // A ceiling chosen from the in-player picker belongs to the playback it was
// chosen for. Starting a different item returns to the device default — // chosen for. Starting a different item returns to the device default —
// otherwise "2 Mbps, just for this one film" quietly governs the rest of the // otherwise "2 Mbps, just for this one film" quietly governs the rest of the
// session, which is the defect DR-225 exists to close. // session, which is the defect DR-226 exists to close.
// //
// TRACES: UR-074, UR-079 | DR-225 // TRACES: UR-074, UR-079 | DR-226
crate::repository::online::clear_playback_quality_override(); crate::repository::online::clear_playback_quality_override();
// Create media item, checking for local download first // Create media item, checking for local download first
@@ -898,7 +898,7 @@ pub async fn player_enter_background_audio(
/// playing there is nothing to pause, and an error would make the frontend /// playing there is nothing to pause, and an error would make the frontend
/// handle a case that is not a failure. /// handle a case that is not a failure.
/// ///
/// TRACES: UR-040, UR-041 | DR-224 | UT-211 /// TRACES: UR-040, UR-041 | DR-225 | UT-212
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn player_background_action( pub async fn player_background_action(
@@ -984,9 +984,9 @@ pub async fn player_play_queue(
// A ceiling chosen from the in-player picker belongs to the playback it was // A ceiling chosen from the in-player picker belongs to the playback it was
// chosen for. Starting a different item returns to the device default — // chosen for. Starting a different item returns to the device default —
// otherwise "2 Mbps, just for this one film" quietly governs the rest of the // otherwise "2 Mbps, just for this one film" quietly governs the rest of the
// session, which is the defect DR-225 exists to close. // session, which is the defect DR-226 exists to close.
// //
// TRACES: UR-074, UR-079 | DR-225 // TRACES: UR-074, UR-079 | DR-226
crate::repository::online::clear_playback_quality_override(); crate::repository::online::clear_playback_quality_override();
// Handle shuffle first // Handle shuffle first
@@ -1461,7 +1461,7 @@ pub async fn player_seek_video(
// queued without one fall back to `needs_transcoding`, which is exact: // queued without one fall back to `needs_transcoding`, which is exact:
// every transcode this app requests is HLS (DR-140). // every transcode this app requests is HLS (DR-140).
// //
// TRACES: UR-004, UR-079 | DR-224, DR-229 // TRACES: UR-004, UR-079 | DR-225, DR-230
let is_hls = match transport { let is_hls = match transport {
Some(crate::repository::Transport::Hls) => true, Some(crate::repository::Transport::Hls) => true,
Some(crate::repository::Transport::Progressive) Some(crate::repository::Transport::Progressive)
@@ -1676,7 +1676,7 @@ pub async fn player_switch_audio_track(
/// belongs to Settings, and `player_set_video_settings` is the one that writes /// belongs to Settings, and `player_set_video_settings` is the one that writes
/// to the database. /// to the database.
/// ///
/// TRACES: UR-074, UR-079 | DR-162, DR-225 /// TRACES: UR-074, UR-079 | DR-162, DR-226
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
// Two of the eight arguments are Tauri `State<'_, _>` injections, not caller // Two of the eight arguments are Tauri `State<'_, _>` injections, not caller
@@ -1728,7 +1728,7 @@ pub async fn player_set_stream_quality(
// builder then never gets to constrain). // builder then never gets to constrain).
// //
// Deliberately the *override*, not the device default: see the doc above. // Deliberately the *override*, not the device default: see the doc above.
// TRACES: UR-074, UR-079 | DR-225 // TRACES: UR-074, UR-079 | DR-226
crate::repository::online::set_playback_quality_override(quality); crate::repository::online::set_playback_quality_override(quality);
// Where to resume. `current_position` is the *element's* clock, which only // Where to resume. `current_position` is the *element's* clock, which only
@@ -1740,7 +1740,7 @@ pub async fn player_set_stream_quality(
// playback state); asking the DOM for it and falling back to 0 inverted // playback state); asking the DOM for it and falling back to 0 inverted
// that. Fall back to what the controller reports instead. // that. Fall back to what the controller reports instead.
// //
// TRACES: UR-005, UR-074 | DR-225 // TRACES: UR-005, UR-074 | DR-226
// The guard is bound inside the arm's block so it is dropped before the // The guard is bound inside the arm's block so it is dropped before the
// reload below takes the same lock. This codebase has been bitten by a // reload below takes the same lock. This codebase has been bitten by a
// MutexGuard living longer than the expression that produced it. // MutexGuard living longer than the expression that produced it.
@@ -2451,9 +2451,9 @@ pub async fn player_play_tracks(
// A ceiling chosen from the in-player picker belongs to the playback it was // A ceiling chosen from the in-player picker belongs to the playback it was
// chosen for. Starting a different item returns to the device default — // chosen for. Starting a different item returns to the device default —
// otherwise "2 Mbps, just for this one film" quietly governs the rest of the // otherwise "2 Mbps, just for this one film" quietly governs the rest of the
// session, which is the defect DR-225 exists to close. // session, which is the defect DR-226 exists to close.
// //
// TRACES: UR-074, UR-079 | DR-225 // TRACES: UR-074, UR-079 | DR-226
crate::repository::online::clear_playback_quality_override(); crate::repository::online::clear_playback_quality_override();
info!( info!(
+1 -1
View File
@@ -618,7 +618,7 @@ pub async fn repository_get_video_stream_url(
/// position on an HLS playlist is copied onto every segment URI and the server /// position on an HLS playlist is copied onto every segment URI and the server
/// rejects each with `400` (DR-181). Callers resume by seeking after load. /// rejects each with `400` (DR-181). Callers resume by seeking after load.
/// ///
/// TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 | UT-212 /// TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228 | UT-213
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn repository_get_stream_selection( pub async fn repository_get_stream_selection(
+1 -1
View File
@@ -114,7 +114,7 @@ pub fn media_local_url(
/// to consume rather than two, and so no caller has to infer a transport from a /// to consume rather than two, and so no caller has to infer a transport from a
/// loopback URL. /// loopback URL.
/// ///
/// TRACES: UR-071, UR-079 | DR-224 /// TRACES: UR-071, UR-079 | DR-225
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn media_local_selection( pub fn media_local_selection(
+3 -3
View File
@@ -1211,7 +1211,7 @@ pub fn run() {
builder.mount_events(app); builder.mount_events(app);
// Native video surface: put a GL area under Tauri's webview so mpv // Native video surface: put a GL area under Tauri's webview so mpv
// can draw beneath the controls (UR-080 / DR-230). // can draw beneath the controls (UR-080 / DR-231).
// //
// 🔴 OFF BY DEFAULT — the naive reparent crashes the app on the // 🔴 OFF BY DEFAULT — the naive reparent crashes the app on the
// first click. `tauri-runtime-wry`'s undecorated-resizing handler // first click. `tauri-runtime-wry`'s undecorated-resizing handler
@@ -1236,14 +1236,14 @@ pub fn run() {
// Kept behind an env var rather than deleted so the next attempt has // Kept behind an env var rather than deleted so the next attempt has
// something to iterate on: JELLYTAU_NATIVE_VIDEO=1 bun run tauri dev // something to iterate on: JELLYTAU_NATIVE_VIDEO=1 bun run tauri dev
// //
// TRACES: UR-080 | DR-230 // TRACES: UR-080 | DR-231
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if std::env::var("JELLYTAU_NATIVE_VIDEO").as_deref() == Ok("1") { if std::env::var("JELLYTAU_NATIVE_VIDEO").as_deref() == Ok("1") {
use tauri::Manager; use tauri::Manager;
log::warn!( log::warn!(
"[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \ "[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \
video surface; the app will abort on the first click until \ video surface; the app will abort on the first click until \
the widget-tree shape is solved (DR-230)" the widget-tree shape is solved (DR-231)"
); );
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
match window.default_vbox() { match window.default_vbox() {
+1 -1
View File
@@ -123,7 +123,7 @@ pub struct MediaItem {
/// caller falls back to `needs_transcoding` — every transcode this app /// caller falls back to `needs_transcoding` — every transcode this app
/// requests is HLS (DR-140), so that fallback is exact rather than a guess. /// requests is HLS (DR-140), so that fallback is exact rather than a guess.
/// ///
/// TRACES: UR-003, UR-004, UR-079 | DR-224, DR-229 /// TRACES: UR-003, UR-004, UR-079 | DR-225, DR-230
#[serde(default)] #[serde(default)]
pub transport: Option<crate::repository::Transport>, pub transport: Option<crate::repository::Transport>,
+1 -1
View File
@@ -24,7 +24,7 @@ pub mod android;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub mod mpv_backend; pub mod mpv_backend;
/// The native video surface mpv renders into (UR-080 / DR-230). /// The native video surface mpv renders into (UR-080 / DR-231).
/// ///
/// Linux-gated for now because the surface is GTK. Everything *around* it — the /// Linux-gated for now because the surface is GTK. Everything *around* it — the
/// render context, its lifetime, frame pacing, the device profile — is /// render context, its lifetime, frame pacing, the device profile — is
+9 -9
View File
@@ -10,14 +10,14 @@
//! real webview into it. Same widgets, one extra move, and the only place //! real webview into it. Same widgets, one extra move, and the only place
//! Tauri-specific behaviour can still bite — which is why it is gate one. //! Tauri-specific behaviour can still bite — which is why it is gate one.
//! //!
//! TRACES: UR-080 | DR-230, IR-033 //! TRACES: UR-080 | DR-231, IR-033
use gtk::prelude::*; use gtk::prelude::*;
use log::{info, warn}; use log::{info, warn};
/// The widgets that make up the video surface, kept together because their /// The widgets that make up the video surface, kept together because their
/// lifetimes are bound: the render context (added next) is created when the GL /// lifetimes are bound: the render context (added next) is created when the GL
/// area realizes and must be freed before it unrealizes — DR-231. /// area realizes and must be freed before it unrealizes — DR-232.
pub struct VideoSurface { pub struct VideoSurface {
/// The GL area mpv renders into. Main child of the overlay, so it sits /// The GL area mpv renders into. Main child of the overlay, so it sits
/// *under* everything else. /// *under* everything else.
@@ -30,7 +30,7 @@ pub struct VideoSurface {
impl VideoSurface { impl VideoSurface {
// Consumed by the render context, which binds to the GL area on `realize` // Consumed by the render context, which binds to the GL area on `realize`
// and is freed on `unrealize` (DR-231). Held here from the moment the // and is freed on `unrealize` (DR-232). Held here from the moment the
// surface exists so that binding has something to attach to. // surface exists so that binding has something to attach to.
#[allow(dead_code)] #[allow(dead_code)]
/// The GL area, for the render context to bind to. /// The GL area, for the render context to bind to.
@@ -84,7 +84,7 @@ impl std::error::Error for SurfaceError {}
/// bottom of the stack. Adding them the other way round yields a webview with /// bottom of the stack. Adding them the other way round yields a webview with
/// video painted over it — an easy mistake with an obvious symptom. /// video painted over it — an easy mistake with an obvious symptom.
/// ///
/// TRACES: UR-080 | DR-230 /// TRACES: UR-080 | DR-231
pub fn attach(vbox: &gtk::Box) -> Result<VideoSurface, SurfaceError> { pub fn attach(vbox: &gtk::Box) -> Result<VideoSurface, SurfaceError> {
// Tauri packs exactly one child (the webview) into the default vbox. Take it // Tauri packs exactly one child (the webview) into the default vbox. Take it
// rather than assume its type: wry's widget is an implementation detail, and // rather than assume its type: wry's widget is an implementation detail, and
@@ -135,8 +135,8 @@ pub fn attach(vbox: &gtk::Box) -> Result<VideoSurface, SurfaceError> {
/// surface is torn down without returning the webview to the vbox the UI /// surface is torn down without returning the webview to the vbox the UI
/// disappears while the app keeps running. Mirrors [`attach`] exactly. /// disappears while the app keeps running. Mirrors [`attach`] exactly.
/// ///
/// TRACES: UR-080 | DR-230, DR-231 /// TRACES: UR-080 | DR-231, DR-232
// Called by the render-context teardown, which lands with DR-231. Written now, // Called by the render-context teardown, which lands with DR-232. Written now,
// beside `attach`, because a reparent whose inverse is written later is a // beside `attach`, because a reparent whose inverse is written later is a
// reparent whose inverse is written wrong. // reparent whose inverse is written wrong.
#[allow(dead_code)] #[allow(dead_code)]
@@ -166,7 +166,7 @@ mod tests {
/// The stacking order is the whole point, and getting it backwards produces /// The stacking order is the whole point, and getting it backwards produces
/// video painted over the controls rather than under them. /// video painted over the controls rather than under them.
/// ///
/// TRACES: UR-080 | DR-230 /// TRACES: UR-080 | DR-231
#[test] #[test]
#[ignore = "requires a display"] #[ignore = "requires a display"]
fn test_gl_area_is_below_the_reparented_webview() { fn test_gl_area_is_below_the_reparented_webview() {
@@ -195,7 +195,7 @@ mod tests {
/// A surface that tears down without returning the webview leaves a running /// A surface that tears down without returning the webview leaves a running
/// app with no UI. /// app with no UI.
/// ///
/// TRACES: UR-080 | DR-230, DR-231 /// TRACES: UR-080 | DR-231, DR-232
#[test] #[test]
#[ignore = "requires a display"] #[ignore = "requires a display"]
fn test_detach_returns_the_webview_to_the_vbox() { fn test_detach_returns_the_webview_to_the_vbox() {
@@ -219,7 +219,7 @@ mod tests {
/// A vbox Tauri has not populated is a changed assumption, not a panic. /// A vbox Tauri has not populated is a changed assumption, not a panic.
/// ///
/// TRACES: UR-080 | DR-230 /// TRACES: UR-080 | DR-231
#[test] #[test]
#[ignore = "requires a display"] #[ignore = "requires a display"]
fn test_an_empty_vbox_is_an_error_not_a_panic() { fn test_an_empty_vbox_is_an_error_not_a_panic() {
+4 -4
View File
@@ -215,7 +215,7 @@ pub fn video_audio_codecs(detected: &str) -> String {
// Where the video renderer decodes the audio itself (ExoPlayer), the // Where the video renderer decodes the audio itself (ExoPlayer), the
// platform list *is* the answer and narrowing it to the webview's throws // platform list *is* the answer and narrowing it to the webview's throws
// away codecs the device genuinely plays — dts, on the tablet this was // away codecs the device genuinely plays — dts, on the tablet this was
// found on. TRACES: UR-004, UR-080 | DR-233 // found on. TRACES: UR-004, UR-080 | DR-234
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
return detected.to_string(); return detected.to_string();
@@ -258,7 +258,7 @@ pub fn video_audio_codecs(detected: &str) -> String {
/// ///
/// One source, so the copies cannot disagree again. /// One source, so the copies cannot disagree again.
/// ///
/// TRACES: UR-004, UR-080 | DR-233 /// TRACES: UR-004, UR-080 | DR-234
pub fn renderer_codecs() -> (String, String) { pub fn renderer_codecs() -> (String, String) {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
{ {
@@ -304,7 +304,7 @@ pub fn renderer_codecs() -> (String, String) {
/// the audio, so judging it against the webview's capabilities transcodes files /// the audio, so judging it against the webview's capabilities transcodes files
/// that would have played. /// that would have played.
/// ///
/// TRACES: UR-004, UR-080 | DR-233 /// TRACES: UR-004, UR-080 | DR-234
pub fn renderer_can_decode_audio(codec: &str) -> bool { pub fn renderer_can_decode_audio(codec: &str) -> bool {
let codec = codec.trim(); let codec = codec.trim();
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
@@ -348,7 +348,7 @@ pub fn webview_can_decode_audio(codec: &str) -> bool {
pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> bool { pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> bool {
match served_audio_codec(streams) { match served_audio_codec(streams) {
// The renderer that will decode it, not always the webview — see // The renderer that will decode it, not always the webview — see
// `renderer_can_decode_audio`. TRACES: UR-004, UR-080 | DR-233 // `renderer_can_decode_audio`. TRACES: UR-004, UR-080 | DR-234
Some(codec) => !renderer_can_decode_audio(codec), Some(codec) => !renderer_can_decode_audio(codec),
// No audio at all, or a codec the server did not name: leave it alone. // No audio at all, or a codec the server did not name: leave it alone.
None => false, None => false,
+2 -2
View File
@@ -97,13 +97,13 @@ impl HybridRepository {
.await .await
} }
/// Decide what stream to play and describe it fully — the DR-224 contract. /// Decide what stream to play and describe it fully — the DR-225 contract.
/// ///
/// Online-only for the same reason as `get_video_stream_url`: an offline /// Online-only for the same reason as `get_video_stream_url`: an offline
/// item is a file on disk, and the caller builds /// item is a file on disk, and the caller builds
/// [`StreamSelection::local_file`] for it rather than negotiating anything. /// [`StreamSelection::local_file`] for it rather than negotiating anything.
/// ///
/// TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 /// TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228
pub async fn get_stream_selection( pub async fn get_stream_selection(
&self, &self,
item_id: &str, item_id: &str,
+1 -1
View File
@@ -5,7 +5,7 @@ pub mod hybrid;
pub mod offline; pub mod offline;
pub mod online; pub mod online;
pub mod series_progress; pub mod series_progress;
/// Backend-owned stream selection (UR-079 / DR-224). /// Backend-owned stream selection (UR-079 / DR-225).
pub mod stream_selection; pub mod stream_selection;
pub mod types; pub mod types;
+28 -28
View File
@@ -43,7 +43,7 @@ static STREAMING_QUALITY: RwLock<StreamingQuality> = RwLock::new(StreamingQualit
/// Cleared when a new item starts playing, so the override cannot outlive the /// Cleared when a new item starts playing, so the override cannot outlive the
/// playback it was chosen for. The device default is never touched by it. /// playback it was chosen for. The device default is never touched by it.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 /// TRACES: UR-074, UR-079 | DR-226
static PLAYBACK_QUALITY_OVERRIDE: RwLock<Option<StreamingQuality>> = RwLock::new(None); static PLAYBACK_QUALITY_OVERRIDE: RwLock<Option<StreamingQuality>> = RwLock::new(None);
/// Set the durable device default. Applies to every stream opened afterwards /// Set the durable device default. Applies to every stream opened afterwards
@@ -71,7 +71,7 @@ pub fn streaming_quality() -> StreamingQuality {
/// Move *this playback* to a different ceiling without disturbing the default. /// Move *this playback* to a different ceiling without disturbing the default.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 /// TRACES: UR-074, UR-079 | DR-226
pub fn set_playback_quality_override(quality: StreamingQuality) { pub fn set_playback_quality_override(quality: StreamingQuality) {
*PLAYBACK_QUALITY_OVERRIDE.write_safe() = Some(quality); *PLAYBACK_QUALITY_OVERRIDE.write_safe() = Some(quality);
} }
@@ -82,14 +82,14 @@ pub fn set_playback_quality_override(quality: StreamingQuality) {
/// not silently govern the next one. Autoplaying the next episode is the case /// not silently govern the next one. Autoplaying the next episode is the case
/// that matters — nobody re-opens the picker between episodes. /// that matters — nobody re-opens the picker between episodes.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 /// TRACES: UR-074, UR-079 | DR-226
pub fn clear_playback_quality_override() { pub fn clear_playback_quality_override() {
*PLAYBACK_QUALITY_OVERRIDE.write_safe() = None; *PLAYBACK_QUALITY_OVERRIDE.write_safe() = None;
} }
/// The per-playback override, if one is in force. /// The per-playback override, if one is in force.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 /// TRACES: UR-074, UR-079 | DR-226
pub fn playback_quality_override() -> Option<StreamingQuality> { pub fn playback_quality_override() -> Option<StreamingQuality> {
*PLAYBACK_QUALITY_OVERRIDE.read_safe() *PLAYBACK_QUALITY_OVERRIDE.read_safe()
} }
@@ -102,7 +102,7 @@ pub fn playback_quality_override() -> Option<StreamingQuality> {
/// the old static: a negotiation that authorises a direct play the URL builder /// the old static: a negotiation that authorises a direct play the URL builder
/// then constrains (or vice versa) leaks the cap. /// then constrains (or vice versa) leaks the cap.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 /// TRACES: UR-074, UR-079 | DR-226
pub fn effective_streaming_quality() -> StreamingQuality { pub fn effective_streaming_quality() -> StreamingQuality {
playback_quality_override().unwrap_or_else(streaming_quality) playback_quality_override().unwrap_or_else(streaming_quality)
} }
@@ -627,7 +627,7 @@ impl OnlineRepository {
// On the webview path this still resolves to "h264" alone, so nothing // On the webview path this still resolves to "h264" alone, so nothing
// changes there. // changes there.
// //
// TRACES: UR-004, UR-080 | DR-233 // TRACES: UR-004, UR-080 | DR-234
let (renderer_video_codecs, _) = super::device_profile::renderer_codecs(); let (renderer_video_codecs, _) = super::device_profile::renderer_codecs();
let mut params = vec![ let mut params = vec![
("api_key", self.access_token.clone()), ("api_key", self.access_token.clone()),
@@ -775,10 +775,10 @@ impl OnlineRepository {
/// ///
/// The single place the device profile is built and POSTed. Both /// The single place the device profile is built and POSTed. Both
/// [`get_playback_info`](Self::get_playback_info) (the legacy shape) and /// [`get_playback_info`](Self::get_playback_info) (the legacy shape) and
/// `get_stream_selection` (the DR-224 contract) go through it, so the /// `get_stream_selection` (the DR-225 contract) go through it, so the
/// profile they negotiate under cannot drift apart. /// profile they negotiate under cannot drift apart.
/// ///
/// TRACES: UR-004, UR-074, UR-079 | DR-224, DR-227 /// TRACES: UR-004, UR-074, UR-079 | DR-225, DR-228
async fn negotiate_playback( async fn negotiate_playback(
&self, &self,
item_id: &str, item_id: &str,
@@ -788,7 +788,7 @@ impl OnlineRepository {
// What the renderer that will decode this can play. One source, shared // What the renderer that will decode this can play. One source, shared
// with the transcode URL builder and the client-side audio override, so // with the transcode URL builder and the client-side audio override, so
// the profile we advertise and the stream we then ask for cannot // the profile we advertise and the stream we then ask for cannot
// disagree. TRACES: UR-004, UR-080 | DR-233 // disagree. TRACES: UR-004, UR-080 | DR-234
let (video_codecs, audio_codecs) = super::device_profile::renderer_codecs(); let (video_codecs, audio_codecs) = super::device_profile::renderer_codecs();
// Video plays in a webview <video> element on every platform, which // Video plays in a webview <video> element on every platform, which
@@ -870,7 +870,7 @@ impl OnlineRepository {
// //
// Capped at the two codecs a Jellyfin server actually // Capped at the two codecs a Jellyfin server actually
// encodes, so a wider decode list never asks it for an av1 // encodes, so a wider decode list never asks it for an av1
// encode. TRACES: UR-004, UR-080 | DR-233 // encode. TRACES: UR-004, UR-080 | DR-234
video_codec: Some( video_codec: Some(
if video_codecs.contains("hevc") { if video_codecs.contains("hevc") {
"h264,hevc" "h264,hevc"
@@ -948,7 +948,7 @@ impl OnlineRepository {
/// frontend stops testing the URL for `.m3u8`, and the quality ladder for /// frontend stops testing the URL for `.m3u8`, and the quality ladder for
/// *this* source so the picker stops offering rungs that mean nothing for it. /// *this* source so the picker stops offering rungs that mean nothing for it.
/// ///
/// TRACES: UR-070, UR-079 | DR-224, DR-225, DR-226, DR-227 | UT-212 /// TRACES: UR-070, UR-079 | DR-225, DR-226, DR-227, DR-228 | UT-213
pub async fn get_stream_selection( pub async fn get_stream_selection(
&self, &self,
item_id: &str, item_id: &str,
@@ -1512,7 +1512,7 @@ impl JellyfinItem {
// exactly how the cap used to leak (a negotiation authorising a direct play the // exactly how the cap used to leak (a negotiation authorising a direct play the
// URL builder then never got to constrain). // URL builder then never got to constrain).
// //
// TRACES: UR-079 | DR-224, DR-227 // TRACES: UR-079 | DR-225, DR-228
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -1605,7 +1605,7 @@ pub struct NegotiatedSource {
/// The source's own bitrate, when the server reports one. /// The source's own bitrate, when the server reports one.
/// ///
/// Fills the quality picker's "this rung is the same as Original" judgement /// Fills the quality picker's "this rung is the same as Original" judgement
/// (DR-226). Absent for some containers — the sampled library has `avi` /// (DR-227). Absent for some containers — the sampled library has `avi`
/// files with no bitrate at all — in which case nothing is judged redundant /// files with no bitrate at all — in which case nothing is judged redundant
/// and every rung stays offered. /// and every rung stays offered.
#[serde(default)] #[serde(default)]
@@ -1640,7 +1640,7 @@ pub struct NegotiatedStream {
/// separately, or that the viewer has pinned a track the file does not default /// separately, or that the viewer has pinned a track the file does not default
/// to. /// to.
/// ///
/// TRACES: UR-079 | DR-227 | UT-212 /// TRACES: UR-079 | DR-228 | UT-213
pub fn decide_playback_kind( pub fn decide_playback_kind(
source: &NegotiatedSource, source: &NegotiatedSource,
audio_forces_transcode: bool, audio_forces_transcode: bool,
@@ -3157,7 +3157,7 @@ mod tests {
set_streaming_quality(StreamingQuality::Original); set_streaming_quality(StreamingQuality::Original);
// A leaked per-playback override would cap every later test's // A leaked per-playback override would cap every later test's
// expectations without appearing anywhere in its setup. // expectations without appearing anywhere in its setup.
// TRACES: UR-074, UR-079 | DR-225 // TRACES: UR-074, UR-079 | DR-226
clear_playback_quality_override(); clear_playback_quality_override();
} }
} }
@@ -4217,7 +4217,7 @@ mod tests {
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Direct-play negotiation (DR-227) // Direct-play negotiation (DR-228)
// //
// Fixtures rather than a live server, but the *shapes* are real: every one // Fixtures rather than a live server, but the *shapes* are real: every one
// below was observed in a `PlaybackInfo` response from the development // below was observed in a `PlaybackInfo` response from the development
@@ -4242,11 +4242,11 @@ mod tests {
} }
} }
/// The whole point of DR-227: a source the server will serve untouched is /// The whole point of DR-228: a source the server will serve untouched is
/// served untouched. Before this, every video play built an HLS transcode /// served untouched. Before this, every video play built an HLS transcode
/// URL regardless. /// URL regardless.
/// ///
/// TRACES: UR-079 | DR-227 | UT-212 /// TRACES: UR-079 | DR-228 | UT-213
#[test] #[test]
fn test_a_supported_source_direct_plays() { fn test_a_supported_source_direct_plays() {
let source = source_fixture(); let source = source_fixture();
@@ -4259,7 +4259,7 @@ mod tests {
/// The server can remux without re-encoding. That is not a transcode and /// The server can remux without re-encoding. That is not a transcode and
/// must not be reported as one — the difference is a whole CPU core. /// must not be reported as one — the difference is a whole CPU core.
/// ///
/// TRACES: UR-079 | DR-227 | UT-212 /// TRACES: UR-079 | DR-228 | UT-213
#[test] #[test]
fn test_a_remuxable_source_direct_streams() { fn test_a_remuxable_source_direct_streams() {
let source = NegotiatedSource { let source = NegotiatedSource {
@@ -4278,7 +4278,7 @@ mod tests {
/// An unsupported codec — the hevc that is ~80% of the sampled library, /// An unsupported codec — the hevc that is ~80% of the sampled library,
/// under the Linux h264-only profile — transcodes. /// under the Linux h264-only profile — transcodes.
/// ///
/// TRACES: UR-079 | DR-227 | UT-212 /// TRACES: UR-079 | DR-228 | UT-213
#[test] #[test]
fn test_an_unsupported_source_transcodes() { fn test_an_unsupported_source_transcodes() {
let source = NegotiatedSource { let source = NegotiatedSource {
@@ -4297,7 +4297,7 @@ mod tests {
/// track the webview cannot decode, which renders as picture with no sound. /// track the webview cannot decode, which renders as picture with no sound.
/// The client's verdict has to win over the server's. /// The client's verdict has to win over the server's.
/// ///
/// TRACES: UR-079 | DR-227, DR-148 | UT-212 /// TRACES: UR-079 | DR-228, DR-148 | UT-213
#[test] #[test]
fn test_undecodable_audio_overrides_the_servers_direct_play_offer() { fn test_undecodable_audio_overrides_the_servers_direct_play_offer() {
let source = source_fixture(); let source = source_fixture();
@@ -4313,7 +4313,7 @@ mod tests {
/// different one. Honouring the viewer's choice means asking the server to /// different one. Honouring the viewer's choice means asking the server to
/// build a stream around it. /// build a stream around it.
/// ///
/// TRACES: UR-021, UR-079 | DR-227 | UT-212 /// TRACES: UR-021, UR-079 | DR-228 | UT-213
#[test] #[test]
fn test_pinning_an_audio_track_forces_a_transcode() { fn test_pinning_an_audio_track_forces_a_transcode() {
let source = source_fixture(); let source = source_fixture();
@@ -4329,7 +4329,7 @@ mod tests {
/// arrives here as `supports_direct_play: false`; this pins the mapping so a /// arrives here as `supports_direct_play: false`; this pins the mapping so a
/// future refactor cannot quietly direct-play past a cap. /// future refactor cannot quietly direct-play past a cap.
/// ///
/// TRACES: UR-074, UR-079 | DR-225, DR-227 | UT-212 /// TRACES: UR-074, UR-079 | DR-226, DR-228 | UT-213
#[test] #[test]
fn test_a_ceiling_below_the_source_bitrate_transcodes() { fn test_a_ceiling_below_the_source_bitrate_transcodes() {
// 6.65 Mbps source, 2 Mbps ceiling — the server refuses direct play. // 6.65 Mbps source, 2 Mbps ceiling — the server refuses direct play.
@@ -4357,7 +4357,7 @@ mod tests {
/// Direct play wins over direct stream when both are on offer: copying the /// Direct play wins over direct stream when both are on offer: copying the
/// file is strictly cheaper than repackaging it. /// file is strictly cheaper than repackaging it.
/// ///
/// TRACES: UR-079 | DR-227 | UT-212 /// TRACES: UR-079 | DR-228 | UT-213
#[test] #[test]
fn test_direct_play_is_preferred_over_direct_stream() { fn test_direct_play_is_preferred_over_direct_stream() {
let source = source_fixture(); let source = source_fixture();
@@ -4369,15 +4369,15 @@ mod tests {
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Per-playback quality ceiling (DR-225) // Per-playback quality ceiling (DR-226)
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
/// The defect DR-225 exists to fix: the in-player picker documented itself /// The defect DR-226 exists to fix: the in-player picker documented itself
/// as a "this film, this connection" control but was implemented by writing /// as a "this film, this connection" control but was implemented by writing
/// the device default, so one awkward film silently capped everything played /// the device default, so one awkward film silently capped everything played
/// afterwards. The override must not touch the default. /// afterwards. The override must not touch the default.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 | UT-212 /// TRACES: UR-074, UR-079 | DR-226 | UT-213
#[test] #[test]
fn test_a_playback_override_does_not_disturb_the_device_default() { fn test_a_playback_override_does_not_disturb_the_device_default() {
let _guard = QUALITY_LOCK.lock_safe(); let _guard = QUALITY_LOCK.lock_safe();
@@ -4410,7 +4410,7 @@ mod tests {
/// autoplayed next episode is the case that matters, since nobody reopens /// autoplayed next episode is the case that matters, since nobody reopens
/// the picker between episodes. /// the picker between episodes.
/// ///
/// TRACES: UR-074, UR-079 | DR-225 | UT-212 /// TRACES: UR-074, UR-079 | DR-226 | UT-213
#[test] #[test]
fn test_the_override_is_droppable_so_it_cannot_outlive_its_playback() { fn test_the_override_is_droppable_so_it_cannot_outlive_its_playback() {
let _guard = QUALITY_LOCK.lock_safe(); let _guard = QUALITY_LOCK.lock_safe();
+21 -21
View File
@@ -12,7 +12,7 @@
//! decides *how to deliver it*** — is the point. A multi-variant playlist handed //! decides *how to deliver it*** — is the point. A multi-variant playlist handed
//! to ExoPlayer is still ExoPlayer's to adapt over; Rust never paces bytes. //! to ExoPlayer is still ExoPlayer's to adapt over; Rust never paces bytes.
//! //!
//! TRACES: UR-079 | DR-224 //! TRACES: UR-079 | DR-225
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -30,7 +30,7 @@ use crate::settings::StreamingQuality;
/// Tagged (`{"type":"hls"}`) rather than a bare string so the frontend matches a /// Tagged (`{"type":"hls"}`) rather than a bare string so the frontend matches a
/// discriminant instead of comparing text. /// discriminant instead of comparing text.
/// ///
/// TRACES: UR-079 | DR-224 /// TRACES: UR-079 | DR-225
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum Transport { pub enum Transport {
@@ -52,7 +52,7 @@ pub enum Transport {
/// lets the UI say "this is not costing the server anything" without inferring /// lets the UI say "this is not costing the server anything" without inferring
/// it from a URL shape. /// it from a URL shape.
/// ///
/// TRACES: UR-079 | DR-227 /// TRACES: UR-079 | DR-228
#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")] #[serde(tag = "type", rename_all = "camelCase")]
pub enum PlaybackKind { pub enum PlaybackKind {
@@ -73,7 +73,7 @@ impl PlaybackKind {
/// that several seek/reload paths still branch on; this keeps the two from /// that several seek/reload paths still branch on; this keeps the two from
/// drifting by making one derive from the other. /// drifting by making one derive from the other.
/// ///
/// TRACES: UR-079 | DR-227 /// TRACES: UR-079 | DR-228
pub fn needs_transcoding(&self) -> bool { pub fn needs_transcoding(&self) -> bool {
matches!(self, PlaybackKind::Transcode) matches!(self, PlaybackKind::Transcode)
} }
@@ -85,7 +85,7 @@ impl PlaybackKind {
/// there is no *chosen* rendition in that case, only the file itself, and /// there is no *chosen* rendition in that case, only the file itself, and
/// reporting the ceiling that happened to be set would misdescribe it. /// reporting the ceiling that happened to be set would misdescribe it.
/// ///
/// TRACES: UR-079 | DR-224, DR-225 /// TRACES: UR-079 | DR-225, DR-226
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Rendition { pub struct Rendition {
@@ -108,7 +108,7 @@ pub struct Rendition {
/// them indistinguishable from Original. `exceeds_source` is what lets the /// them indistinguishable from Original. `exceeds_source` is what lets the
/// frontend render that honestly without knowing anything about bitrates. /// frontend render that honestly without knowing anything about bitrates.
/// ///
/// TRACES: UR-070, UR-079 | DR-226, DR-121 /// TRACES: UR-070, UR-079 | DR-227, DR-121
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct QualityOption { pub struct QualityOption {
@@ -133,7 +133,7 @@ pub struct QualityOption {
/// ///
/// Replaces the bare `String` URL that `get_video_stream_url` used to return. /// Replaces the bare `String` URL that `get_video_stream_url` used to return.
/// ///
/// TRACES: UR-079 | DR-224, DR-226, DR-227 /// TRACES: UR-079 | DR-225, DR-227, DR-228
#[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(specta::Type, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct StreamSelection { pub struct StreamSelection {
@@ -145,7 +145,7 @@ pub struct StreamSelection {
pub playback_kind: PlaybackKind, pub playback_kind: PlaybackKind,
/// The negotiated rendition; `None` when direct-playing the source as-is. /// The negotiated rendition; `None` when direct-playing the source as-is.
pub rendition: Option<Rendition>, pub rendition: Option<Rendition>,
/// What this media source can offer, for the quality picker (DR-226). /// What this media source can offer, for the quality picker (DR-227).
pub available: Vec<QualityOption>, pub available: Vec<QualityOption>,
/// The media source this selection is for, so a later re-open (quality /// The media source this selection is for, so a later re-open (quality
/// change, audio-track switch, transcoded seek) targets the same one. /// change, audio-track switch, transcoded seek) targets the same one.
@@ -160,7 +160,7 @@ pub struct StreamSelection {
/// queue's long-standing `needs_transcoding` flag and the seek strategy both /// queue's long-standing `needs_transcoding` flag and the seek strategy both
/// read this, so there is one answer rather than three. /// read this, so there is one answer rather than three.
/// ///
/// TRACES: UR-079 | DR-224, DR-227 /// TRACES: UR-079 | DR-225, DR-228
pub needs_transcoding: bool, pub needs_transcoding: bool,
} }
@@ -171,7 +171,7 @@ impl StreamSelection {
/// source's — and offering a quality ladder over it would be a lie, since /// source's — and offering a quality ladder over it would be a lie, since
/// nothing about a local file can be re-negotiated. /// nothing about a local file can be re-negotiated.
/// ///
/// TRACES: UR-071, UR-079 | DR-224 /// TRACES: UR-071, UR-079 | DR-225
pub fn local_file(url: impl Into<String>) -> Self { pub fn local_file(url: impl Into<String>) -> Self {
Self { Self {
url: url.into(), url: url.into(),
@@ -199,7 +199,7 @@ impl StreamSelection {
/// all). In that case nothing can be judged redundant and every rung is offered, /// all). In that case nothing can be judged redundant and every rung is offered,
/// which is the safe direction: the viewer keeps every choice they had before. /// which is the safe direction: the viewer keeps every choice they had before.
/// ///
/// TRACES: UR-070, UR-079 | DR-226, DR-121 | UT-211 /// TRACES: UR-070, UR-079 | DR-227, DR-121 | UT-212
pub fn quality_options_for_source(source_bitrate: Option<u64>) -> Vec<QualityOption> { pub fn quality_options_for_source(source_bitrate: Option<u64>) -> Vec<QualityOption> {
StreamingQuality::ALL StreamingQuality::ALL
.iter() .iter()
@@ -226,7 +226,7 @@ mod tests {
/// The tag the frontend matches on has to be exactly what it expects, and /// The tag the frontend matches on has to be exactly what it expects, and
/// it is a *string in TypeScript* — nothing but a test keeps the two in step. /// it is a *string in TypeScript* — nothing but a test keeps the two in step.
/// ///
/// TRACES: UR-079 | DR-224 | UT-211 /// TRACES: UR-079 | DR-225 | UT-212
#[test] #[test]
fn test_transport_serialises_with_the_tag_the_frontend_matches() { fn test_transport_serialises_with_the_tag_the_frontend_matches() {
let cases = [ let cases = [
@@ -242,7 +242,7 @@ mod tests {
} }
} }
/// TRACES: UR-079 | DR-227 | UT-211 /// TRACES: UR-079 | DR-228 | UT-212
#[test] #[test]
fn test_playback_kind_serialises_with_the_tag_the_frontend_matches() { fn test_playback_kind_serialises_with_the_tag_the_frontend_matches() {
let cases = [ let cases = [
@@ -261,7 +261,7 @@ mod tests {
/// Only a transcode costs the server encoder time. A direct *stream* is a /// Only a transcode costs the server encoder time. A direct *stream* is a
/// remux — cheap, and not what `needs_transcoding` has ever meant. /// remux — cheap, and not what `needs_transcoding` has ever meant.
/// ///
/// TRACES: UR-079 | DR-227 | UT-211 /// TRACES: UR-079 | DR-228 | UT-212
#[test] #[test]
fn test_only_transcode_counts_as_transcoding() { fn test_only_transcode_counts_as_transcoding() {
assert!(PlaybackKind::Transcode.needs_transcoding()); assert!(PlaybackKind::Transcode.needs_transcoding());
@@ -272,7 +272,7 @@ mod tests {
/// A local file is a direct play over a local transport, with no ladder: /// A local file is a direct play over a local transport, with no ladder:
/// nothing about a file on disk can be re-negotiated. /// nothing about a file on disk can be re-negotiated.
/// ///
/// TRACES: UR-071, UR-079 | DR-224 | UT-211 /// TRACES: UR-071, UR-079 | DR-225 | UT-212
#[test] #[test]
fn test_local_file_selection_offers_no_ladder() { fn test_local_file_selection_offers_no_ladder() {
let selection = StreamSelection::local_file("http://127.0.0.1:9000/media/x.mkv"); let selection = StreamSelection::local_file("http://127.0.0.1:9000/media/x.mkv");
@@ -286,7 +286,7 @@ mod tests {
/// The camelCase rule applies to nested struct fields too, and /// The camelCase rule applies to nested struct fields too, and
/// `playbackKind` is the one the frontend branches on. /// `playbackKind` is the one the frontend branches on.
/// ///
/// TRACES: UR-079 | DR-224 | UT-211 /// TRACES: UR-079 | DR-225 | UT-212
#[test] #[test]
fn test_stream_selection_fields_are_camel_case_on_the_wire() { fn test_stream_selection_fields_are_camel_case_on_the_wire() {
let selection = StreamSelection { let selection = StreamSelection {
@@ -321,7 +321,7 @@ mod tests {
/// The measured library has 1.1 Mbps sources in it. Offering those a choice /// The measured library has 1.1 Mbps sources in it. Offering those a choice
/// of 20, 10, 8, 4 and 2 Mbps is offering five ways to spell "Original". /// of 20, 10, 8, 4 and 2 Mbps is offering five ways to spell "Original".
/// ///
/// TRACES: UR-070, UR-079 | DR-226, DR-121 | UT-211 /// TRACES: UR-070, UR-079 | DR-227, DR-121 | UT-212
#[test] #[test]
fn test_rungs_above_the_source_bitrate_are_marked_redundant() { fn test_rungs_above_the_source_bitrate_are_marked_redundant() {
let options = quality_options_for_source(Some(1_122_137)); let options = quality_options_for_source(Some(1_122_137));
@@ -361,7 +361,7 @@ mod tests {
/// `Original` is the source, so it is never "above" it — not even for a /// `Original` is the source, so it is never "above" it — not even for a
/// source whose bitrate is unknown or zero. /// source whose bitrate is unknown or zero.
/// ///
/// TRACES: UR-070, UR-079 | DR-226 | UT-211 /// TRACES: UR-070, UR-079 | DR-227 | UT-212
#[test] #[test]
fn test_original_is_never_marked_as_exceeding_the_source() { fn test_original_is_never_marked_as_exceeding_the_source() {
for bitrate in [None, Some(0), Some(1), Some(50_000_000)] { for bitrate in [None, Some(0), Some(1), Some(50_000_000)] {
@@ -377,7 +377,7 @@ mod tests {
/// An `avi` with no reported bitrate must not lose the picker. Judging /// An `avi` with no reported bitrate must not lose the picker. Judging
/// nothing redundant is the safe direction — the viewer keeps every choice. /// nothing redundant is the safe direction — the viewer keeps every choice.
/// ///
/// TRACES: UR-070, UR-079 | DR-226 | UT-211 /// TRACES: UR-070, UR-079 | DR-227 | UT-212
#[test] #[test]
fn test_an_unknown_source_bitrate_keeps_every_rung_offered() { fn test_an_unknown_source_bitrate_keeps_every_rung_offered() {
let options = quality_options_for_source(None); let options = quality_options_for_source(None);
@@ -391,7 +391,7 @@ mod tests {
/// A 4K remux constrains at every rung — the ladder is fully meaningful. /// A 4K remux constrains at every rung — the ladder is fully meaningful.
/// ///
/// TRACES: UR-070, UR-079 | DR-226 | UT-211 /// TRACES: UR-070, UR-079 | DR-227 | UT-212
#[test] #[test]
fn test_a_source_above_the_ladder_marks_nothing_redundant() { fn test_a_source_above_the_ladder_marks_nothing_redundant() {
let options = quality_options_for_source(Some(40_000_000)); let options = quality_options_for_source(Some(40_000_000));
@@ -401,7 +401,7 @@ mod tests {
/// The picker's text comes from Rust, beside the numbers it describes, so a /// The picker's text comes from Rust, beside the numbers it describes, so a
/// relabelled rung cannot drift out of step with what it does. /// relabelled rung cannot drift out of step with what it does.
/// ///
/// TRACES: UR-070, UR-079 | DR-226 | UT-211 /// TRACES: UR-070, UR-079 | DR-227 | UT-212
#[test] #[test]
fn test_options_carry_the_ladder_labels() { fn test_options_carry_the_ladder_labels() {
let options = quality_options_for_source(Some(6_652_961)); let options = quality_options_for_source(Some(6_652_961));
+2 -2
View File
@@ -474,9 +474,9 @@ pub struct LiveStreamInfo {
/// A live channel is always an HLS transcode — the server has to repackage a /// A live channel is always an HLS transcode — the server has to repackage a
/// broadcast mux into something a browser can play, and there is no static /// broadcast mux into something a browser can play, and there is no static
/// file to direct-play. Saying so here means the player page never has to /// file to direct-play. Saying so here means the player page never has to
/// work it out from the URL, which is the whole of DR-224. /// work it out from the URL, which is the whole of DR-225.
/// ///
/// TRACES: UR-079 | DR-224 /// TRACES: UR-079 | DR-225
pub transport: super::stream_selection::Transport, pub transport: super::stream_selection::Transport,
} }
+16 -16
View File
@@ -39,7 +39,7 @@ async playerPlayItem(item: PlayItemRequest) : Promise<PlayerStatus> {
* playing there is nothing to pause, and an error would make the frontend * playing there is nothing to pause, and an error would make the frontend
* handle a case that is not a failure. * handle a case that is not a failure.
* *
* TRACES: UR-040, UR-041 | DR-224 | UT-211 * TRACES: UR-040, UR-041 | DR-225 | UT-212
*/ */
async playerBackgroundAction(backgroundAudioArmed: boolean, inPictureInPicture: boolean) : Promise<BackgroundAction> { async playerBackgroundAction(backgroundAudioArmed: boolean, inPictureInPicture: boolean) : Promise<BackgroundAction> {
return await TAURI_INVOKE("player_background_action", { backgroundAudioArmed, inPictureInPicture }); return await TAURI_INVOKE("player_background_action", { backgroundAudioArmed, inPictureInPicture });
@@ -273,7 +273,7 @@ async playerGetStreamingQualities() : Promise<([StreamingQuality, string, string
* belongs to Settings, and `player_set_video_settings` is the one that writes * belongs to Settings, and `player_set_video_settings` is the one that writes
* to the database. * to the database.
* *
* TRACES: UR-074, UR-079 | DR-162, DR-225 * TRACES: UR-074, UR-079 | DR-162, DR-226
*/ */
async playerSetStreamQuality(repositoryHandle: string, quality: StreamingQuality, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamQualityResponse> { async playerSetStreamQuality(repositoryHandle: string, quality: StreamingQuality, useHtml5: boolean, currentPosition: number | null, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamQualityResponse> {
return await TAURI_INVOKE("player_set_stream_quality", { repositoryHandle, quality, useHtml5, currentPosition, mediaSourceId, audioStreamIndex }); return await TAURI_INVOKE("player_set_stream_quality", { repositoryHandle, quality, useHtml5, currentPosition, mediaSourceId, audioStreamIndex });
@@ -1048,7 +1048,7 @@ async mediaLocalUrl(path: string) : Promise<string> {
* to consume rather than two, and so no caller has to infer a transport from a * to consume rather than two, and so no caller has to infer a transport from a
* loopback URL. * loopback URL.
* *
* TRACES: UR-071, UR-079 | DR-224 * TRACES: UR-071, UR-079 | DR-225
*/ */
async mediaLocalSelection(path: string) : Promise<StreamSelection> { async mediaLocalSelection(path: string) : Promise<StreamSelection> {
return await TAURI_INVOKE("media_local_selection", { path }); return await TAURI_INVOKE("media_local_selection", { path });
@@ -1651,7 +1651,7 @@ async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId:
* position on an HLS playlist is copied onto every segment URI and the server * position on an HLS playlist is copied onto every segment URI and the server
* rejects each with `400` (DR-181). Callers resume by seeking after load. * rejects each with `400` (DR-181). Callers resume by seeking after load.
* *
* TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 | UT-212 * TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228 | UT-213
*/ */
async repositoryGetStreamSelection(handle: string, itemId: string, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamSelection> { async repositoryGetStreamSelection(handle: string, itemId: string, mediaSourceId: string | null, audioStreamIndex: number | null) : Promise<StreamSelection> {
return await TAURI_INVOKE("repository_get_stream_selection", { handle, itemId, mediaSourceId, audioStreamIndex }); return await TAURI_INVOKE("repository_get_stream_selection", { handle, itemId, mediaSourceId, audioStreamIndex });
@@ -2374,9 +2374,9 @@ export type LiveStreamInfo = { streamUrl: string; playSessionId: string | null;
* A live channel is always an HLS transcode the server has to repackage a * A live channel is always an HLS transcode the server has to repackage a
* broadcast mux into something a browser can play, and there is no static * broadcast mux into something a browser can play, and there is no static
* file to direct-play. Saying so here means the player page never has to * file to direct-play. Saying so here means the player page never has to
* work it out from the URL, which is the whole of DR-224. * work it out from the URL, which is the whole of DR-225.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
transport: Transport } transport: Transport }
/** /**
@@ -2626,7 +2626,7 @@ needsTranscoding: boolean;
* caller falls back to `needs_transcoding` every transcode this app * caller falls back to `needs_transcoding` every transcode this app
* requests is HLS (DR-140), so that fallback is exact rather than a guess. * requests is HLS (DR-140), so that fallback is exact rather than a guess.
* *
* TRACES: UR-003, UR-004, UR-079 | DR-224, DR-229 * TRACES: UR-003, UR-004, UR-079 | DR-225, DR-230
*/ */
transport?: Transport | null; transport?: Transport | null;
/** /**
@@ -2750,7 +2750,7 @@ export type PlaybackInfo = { mediaSourceId: string; playSessionId: string; strea
* lets the UI say "this is not costing the server anything" without inferring * lets the UI say "this is not costing the server anything" without inferring
* it from a URL shape. * it from a URL shape.
* *
* TRACES: UR-079 | DR-227 * TRACES: UR-079 | DR-228
*/ */
export type PlaybackKind = export type PlaybackKind =
/** /**
@@ -2875,7 +2875,7 @@ needsTranscoding?: boolean;
* caller falls back to `needs_transcoding` every transcode this app * caller falls back to `needs_transcoding` every transcode this app
* requests is HLS (DR-140), so that fallback is exact rather than a guess. * requests is HLS (DR-140), so that fallback is exact rather than a guess.
* *
* TRACES: UR-003, UR-004, UR-079 | DR-224, DR-229 * TRACES: UR-003, UR-004, UR-079 | DR-225, DR-230
*/ */
transport?: Transport | null; transport?: Transport | null;
/** /**
@@ -3168,7 +3168,7 @@ skipped: number }
* them indistinguishable from Original. `exceeds_source` is what lets the * them indistinguishable from Original. `exceeds_source` is what lets the
* frontend render that honestly without knowing anything about bitrates. * frontend render that honestly without knowing anything about bitrates.
* *
* TRACES: UR-070, UR-079 | DR-226, DR-121 * TRACES: UR-070, UR-079 | DR-227, DR-121
*/ */
export type QualityOption = { quality: StreamingQuality; export type QualityOption = { quality: StreamingQuality;
/** /**
@@ -3207,7 +3207,7 @@ export type RemoteSessionStatus = { position: number; duration: number | null; i
* there is no *chosen* rendition in that case, only the file itself, and * there is no *chosen* rendition in that case, only the file itself, and
* reporting the ceiling that happened to be set would misdescribe it. * reporting the ceiling that happened to be set would misdescribe it.
* *
* TRACES: UR-079 | DR-224, DR-225 * TRACES: UR-079 | DR-225, DR-226
*/ */
export type Rendition = { export type Rendition = {
/** /**
@@ -3353,7 +3353,7 @@ export type StreamQualityResponse =
* that would have used the requested value was never reached. The stream * that would have used the requested value was never reached. The stream
* changed and the menu did not. * changed and the menu did not.
* *
* TRACES: UR-074, UR-079 | DR-225, DR-226 * TRACES: UR-074, UR-079 | DR-226, DR-227
*/ */
{ strategy: "native"; selection: StreamSelection; position: number } | { strategy: "native"; selection: StreamSelection; position: number } |
/** /**
@@ -3366,7 +3366,7 @@ export type StreamQualityResponse =
* *
* Replaces the bare `String` URL that `get_video_stream_url` used to return. * Replaces the bare `String` URL that `get_video_stream_url` used to return.
* *
* TRACES: UR-079 | DR-224, DR-226, DR-227 * TRACES: UR-079 | DR-225, DR-227, DR-228
*/ */
export type StreamSelection = { export type StreamSelection = {
/** /**
@@ -3386,7 +3386,7 @@ playbackKind: PlaybackKind;
*/ */
rendition: Rendition | null; rendition: Rendition | null;
/** /**
* What this media source can offer, for the quality picker (DR-226). * What this media source can offer, for the quality picker (DR-227).
*/ */
available: QualityOption[]; available: QualityOption[];
/** /**
@@ -3407,7 +3407,7 @@ playSessionId: string | null;
* queue's long-standing `needs_transcoding` flag and the seek strategy both * queue's long-standing `needs_transcoding` flag and the seek strategy both
* read this, so there is one answer rather than three. * read this, so there is one answer rather than three.
* *
* TRACES: UR-079 | DR-224, DR-227 * TRACES: UR-079 | DR-225, DR-228
*/ */
needsTranscoding: boolean } needsTranscoding: boolean }
/** /**
@@ -3503,7 +3503,7 @@ export type ThumbnailCacheStats = { totalSizeBytes: number; itemCount: number; l
* Tagged (`{"type":"hls"}`) rather than a bare string so the frontend matches a * Tagged (`{"type":"hls"}`) rather than a bare string so the frontend matches a
* discriminant instead of comparing text. * discriminant instead of comparing text.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
export type Transport = export type Transport =
/** /**
+1 -1
View File
@@ -260,7 +260,7 @@ export class RepositoryClient {
* start position on an HLS playlist makes Jellyfin reject every segment behind * start position on an HLS playlist makes Jellyfin reject every segment behind
* it with `400` (DR-181). Resume by seeking once loaded. * it with `400` (DR-181). Resume by seeking once loaded.
* *
* TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 | UT-212 * TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228 | UT-213
*/ */
async getStreamSelection( async getStreamSelection(
itemId: string, itemId: string,
+17 -17
View File
@@ -94,7 +94,7 @@
* which forced this component to re-derive the transport by searching for * which forced this component to re-derive the transport by searching for
* `.m3u8`. * `.m3u8`.
* *
* TRACES: UR-079 | DR-224, DR-226 * TRACES: UR-079 | DR-225, DR-227
*/ */
selection: StreamSelection; selection: StreamSelection;
mediaSourceId?: string; // Media source ID for subtitle URLs mediaSourceId?: string; // Media source ID for subtitle URLs
@@ -196,7 +196,7 @@
// The selection currently loaded. Starts from the prop and is replaced // The selection currently loaded. Starts from the prop and is replaced
// wholesale by a reload (quality change, audio-track switch, transcoded seek) // wholesale by a reload (quality change, audio-track switch, transcoded seek)
// so transport and URL can never disagree. // so transport and URL can never disagree.
// TRACES: UR-079 | DR-224 // TRACES: UR-079 | DR-225
let currentSelection = $state<StreamSelection>(untrack(() => selection)); let currentSelection = $state<StreamSelection>(untrack(() => selection));
const currentStreamUrl = $derived(currentSelection.url); const currentStreamUrl = $derived(currentSelection.url);
/** /**
@@ -281,11 +281,11 @@
* Used by the paths that swap the stream without re-negotiating — the * Used by the paths that swap the stream without re-negotiating — the
* background-audio handoff and its return. Each states the transport it is * background-audio handoff and its return. Each states the transport it is
* moving to rather than letting it be inferred, which is the whole point of * moving to rather than letting it be inferred, which is the whole point of
* DR-224: the audio handoff really is a progressive mp3, and the rebuilt * DR-225: the audio handoff really is a progressive mp3, and the rebuilt
* video stream really is an HLS transcode, and neither is knowable from the * video stream really is an HLS transcode, and neither is knowable from the
* URL text. * URL text.
* *
* TRACES: UR-040, UR-079 | DR-224 * TRACES: UR-040, UR-079 | DR-225
*/ */
function selectionAt(url: string, transport: StreamSelection["transport"]): StreamSelection { function selectionAt(url: string, transport: StreamSelection["transport"]): StreamSelection {
// A re-opened stream is a new transcode job; the old session id is stale. // A re-opened stream is a new transcode job; the old session id is stale.
@@ -326,14 +326,14 @@
/** /**
* The rungs to offer for the stream that is playing, straight from the * The rungs to offer for the stream that is playing, straight from the
* backend (DR-226). Rungs whose ceiling is at or above the source bitrate are * backend (DR-227). Rungs whose ceiling is at or above the source bitrate are
* dropped: they produce the same bytes as Original, so listing five of them is * dropped: they produce the same bytes as Original, so listing five of them is
* five ways to spell one choice. Rust decides which those are — this only * five ways to spell one choice. Rust decides which those are — this only
* decides not to draw them. * decides not to draw them.
* *
* `Original` is always kept; it is the source, never redundant with it. * `Original` is always kept; it is the source, never redundant with it.
* *
* TRACES: UR-070, UR-079 | DR-226, DR-121 * TRACES: UR-070, UR-079 | DR-227, DR-121
*/ */
const qualityOptions = $derived( const qualityOptions = $derived(
currentSelection.available.filter((o) => !o.exceedsSource || o.quality === "original"), currentSelection.available.filter((o) => !o.exceedsSource || o.quality === "original"),
@@ -654,7 +654,7 @@
// teardown for an unchanged stream and left the element showing nothing // teardown for an unchanged stream and left the element showing nothing
// until a seek forced another cycle. // until a seek forced another cycle.
// //
// TRACES: UR-079 | DR-224 | UT-213 // TRACES: UR-079 | DR-225 | UT-214
const loader = loaderForTransport(transportKind, { const loader = loaderForTransport(transportKind, {
hlsJsSupported: Hls.isSupported(), hlsJsSupported: Hls.isSupported(),
nativeHlsSupported: !!videoElement.canPlayType("application/vnd.apple.mpegurl"), nativeHlsSupported: !!videoElement.canPlayType("application/vnd.apple.mpegurl"),
@@ -892,7 +892,7 @@
}); });
}); });
// The quality *ladder* now arrives with the stream selection (DR-226), so all // The quality *ladder* now arrives with the stream selection (DR-227), so all
// this still needs is the device default, for the case where the stream is a // this still needs is the device default, for the case where the stream is a
// direct play and has no rendition of its own. // direct play and has no rendition of its own.
// //
@@ -901,7 +901,7 @@
// HTML5 mode and breaks native seeking, and nothing about playback waits on // HTML5 mode and breaks native seeking, and nothing about playback waits on
// this value. // this value.
// //
// TRACES: UR-074, UR-079 | DR-162, DR-226 // TRACES: UR-074, UR-079 | DR-162, DR-227
onMount(() => { onMount(() => {
commands commands
.playerGetVideoSettings() .playerGetVideoSettings()
@@ -974,7 +974,7 @@
videoCodec: needsTranscoding ? "hevc" : "h264", videoCodec: needsTranscoding ? "hevc" : "h264",
needsTranscoding: needsTranscoding, needsTranscoding: needsTranscoding,
// Carry the negotiated transport onto the queue item so a later seek // Carry the negotiated transport onto the queue item so a later seek
// reads it instead of falling back. TRACES: UR-079 | DR-229 // reads it instead of falling back. TRACES: UR-079 | DR-230
transport: currentSelection.transport, transport: currentSelection.transport,
// Order matters: player_set_subtitle_track(n) is a position in this // Order matters: player_set_subtitle_track(n) is a position in this
// array. Previously this array was built and then dropped, so // array. Previously this array was built and then dropped, so
@@ -1846,7 +1846,7 @@
// whether the item has a picture to lose, which is Rust's to know. This used // whether the item has a picture to lose, which is Rust's to know. This used
// to be decided implicitly by Kotlin gating the event on the toggle, which // to be decided implicitly by Kotlin gating the event on the toggle, which
// is why the native path -- whose media service keeps playing regardless -- // is why the native path -- whose media service keeps playing regardless --
// ignored the toggle entirely (DR-224). // ignored the toggle entirely (DR-225).
let action: BackgroundAction; let action: BackgroundAction;
try { try {
action = await commands.playerBackgroundAction( action = await commands.playerBackgroundAction(
@@ -2034,7 +2034,7 @@
id: media.id, id: media.id,
videoCodec: needsTranscoding ? "hevc" : "h264", videoCodec: needsTranscoding ? "hevc" : "h264",
needsTranscoding, needsTranscoding,
// TRACES: UR-079 | DR-229 // TRACES: UR-079 | DR-230
transport: targetSelection.transport, transport: targetSelection.transport,
subtitles: nativeSubtitleTracks(sentSubtitleTracks), subtitles: nativeSubtitleTracks(sentSubtitleTracks),
}); });
@@ -2395,12 +2395,12 @@
* only supplies the position to resume at. * only supplies the position to resume at.
* *
* The change applies to this playback alone; the durable Settings default is * The change applies to this playback alone; the durable Settings default is
* untouched (DR-225). Nothing is optimistically assigned here: what the picker * untouched (DR-226). Nothing is optimistically assigned here: what the picker
* shows comes from the selection the backend hands back, because what you get * shows comes from the selection the backend hands back, because what you get
* is not always what you asked for — a ceiling above the source bitrate is the * is not always what you asked for — a ceiling above the source bitrate is the
* source, and claiming otherwise is the kind of lie the old picker told. * source, and claiming otherwise is the kind of lie the old picker told.
* *
* TRACES: UR-074, UR-079 | DR-162, DR-225, DR-226 * TRACES: UR-074, UR-079 | DR-162, DR-226, DR-227
*/ */
async function selectQuality(quality: StreamingQuality) { async function selectQuality(quality: StreamingQuality) {
showQualityMenu = false; showQualityMenu = false;
@@ -2424,7 +2424,7 @@
// this the menu stayed on the first stream's rung while the stream itself // this the menu stayed on the first stream's rung while the stream itself
// changed underneath. // changed underneath.
// //
// TRACES: UR-074, UR-079 | DR-225, DR-226 // TRACES: UR-074, UR-079 | DR-226, DR-227
if (negotiated) { if (negotiated) {
currentSelection = negotiated; currentSelection = negotiated;
} }
@@ -2883,7 +2883,7 @@
<!-- <!--
Streaming quality (bandwidth ceiling), populated from what this media Streaming quality (bandwidth ceiling), populated from what this media
source can actually offer. TRACES: UR-070, UR-074 | DR-162, DR-226 source can actually offer. TRACES: UR-070, UR-074 | DR-162, DR-227
--> -->
{#if qualityOptions.length > 1} {#if qualityOptions.length > 1}
<div class="relative"> <div class="relative">
@@ -2910,7 +2910,7 @@
<div class="text-white text-sm font-semibold">Quality</div> <div class="text-white text-sm font-semibold">Quality</div>
<!-- <!--
What the server is actually doing. Only knowable now that What the server is actually doing. Only knowable now that
the backend reports it. TRACES: UR-079 | DR-227 the backend reports it. TRACES: UR-079 | DR-228
--> -->
<div class="text-xs text-gray-400 mt-0.5">{playbackKindLabel}</div> <div class="text-xs text-gray-400 mt-0.5">{playbackKindLabel}</div>
</div> </div>
+3 -3
View File
@@ -37,7 +37,7 @@ const log = createLogger("Html5PlayerAdapter");
* This is the one place a fallback is tolerable, and it is explicitly a * This is the one place a fallback is tolerable, and it is explicitly a
* fallback: the negotiated path never reaches it. * fallback: the negotiated path never reaches it.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
function selectionForLoad(streamUrl: string, options: PlayerLoadOptions): StreamSelection { function selectionForLoad(streamUrl: string, options: PlayerLoadOptions): StreamSelection {
if (options.selection) return options.selection; if (options.selection) return options.selection;
@@ -75,9 +75,9 @@ export interface Html5ElementBridge {
* *
* Carries the whole [`StreamSelection`], not just the URL: the component's * Carries the whole [`StreamSelection`], not just the URL: the component's
* effect has to know the transport to choose a loader, and deriving that from * effect has to know the transport to choose a loader, and deriving that from
* the URL is the substring check DR-224 removes. * the URL is the substring check DR-225 removes.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
setStreamSelection(selection: StreamSelection): void; setStreamSelection(selection: StreamSelection): void;
/** Tear down the component-owned hls.js instance (dual-audio prevention). */ /** Tear down the component-owned hls.js instance (dual-audio prevention). */
+2 -2
View File
@@ -51,7 +51,7 @@ export interface PlayerLoadOptions {
* plugin's direct URL where the adapter falls back to what the other * plugin's direct URL where the adapter falls back to what the other
* options already say rather than to reading the URL. * options already say rather than to reading the URL.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
selection?: StreamSelection | null; selection?: StreamSelection | null;
/** The source is a file on disk (or the loopback server in front of one). */ /** The source is a file on disk (or the loopback server in front of one). */
@@ -127,7 +127,7 @@ export interface PlayerAdapter {
* already decided to reload, and `selection.transport` says how to open it, so * already decided to reload, and `selection.transport` says how to open it, so
* no adapter has to infer that from the URL. * no adapter has to infer that from the URL.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
reloadSource(selection: StreamSelection, offset: number): Promise<void>; reloadSource(selection: StreamSelection, offset: number): Promise<void>;
+1 -1
View File
@@ -199,7 +199,7 @@ async function switchAudioTrack(
* actually got, which is not always what was asked for: a ceiling above the * actually got, which is not always what was asked for: a ceiling above the
* source bitrate is the source. * source bitrate is the source.
* *
* TRACES: UR-074, UR-079 | DR-162, DR-225 * TRACES: UR-074, UR-079 | DR-162, DR-226
*/ */
async function setStreamQuality( async function setStreamQuality(
quality: StreamingQuality, quality: StreamingQuality,
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* The loader is chosen from the backend's `transport` tag, never from the URL. * The loader is chosen from the backend's `transport` tag, never from the URL.
* *
* TRACES: UR-079 | DR-224 | UT-213 * TRACES: UR-079 | DR-225 | UT-214
*/ */
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
+3 -3
View File
@@ -4,7 +4,7 @@
* Extracted from `VideoPlayer.svelte` so the decision can be unit-tested the * Extracted from `VideoPlayer.svelte` so the decision can be unit-tested the
* same pattern as `episodeStrip.ts` and `TrackList.logic.test.ts`. * same pattern as `episodeStrip.ts` and `TrackList.logic.test.ts`.
* *
* TRACES: UR-079 | DR-224 | UT-213 * TRACES: UR-079 | DR-225 | UT-214
*/ */
import type { StreamSelection, Transport } from "$lib/api/bindings"; import type { StreamSelection, Transport } from "$lib/api/bindings";
@@ -54,11 +54,11 @@ export function videoLoaderFor(
* whenever the selection **object** is replaced even with an identical URL and * whenever the selection **object** is replaced even with an identical URL and
* transport and the HLS effect's teardown/rebuild is not idempotent: it * transport and the HLS effect's teardown/rebuild is not idempotent: it
* destroys the hls.js instance and reattaches, which leaves the element with no * destroys the hls.js instance and reattaches, which leaves the element with no
* video until something forces another cycle. The pre-DR-224 code read a plain * video until something forces another cycle. The pre-DR-225 code read a plain
* URL *string*, so re-assigning the same value was a no-op and the effect stayed * URL *string*, so re-assigning the same value was a no-op and the effect stayed
* put. Passing primitives restores that. * put. Passing primitives restores that.
* *
* TRACES: UR-079 | DR-224 | UT-213 * TRACES: UR-079 | DR-225 | UT-214
*/ */
export function loaderForTransport( export function loaderForTransport(
transport: Transport["type"], transport: Transport["type"],
+4 -4
View File
@@ -82,7 +82,7 @@
* Replaces a bare URL string: the transport travels with it, so neither this * Replaces a bare URL string: the transport travels with it, so neither this
* page nor VideoPlayer has to work out whether the URL is a playlist. * page nor VideoPlayer has to work out whether the URL is a playlist.
* *
* TRACES: UR-079 | DR-224 * TRACES: UR-079 | DR-225
*/ */
let selection = $state<StreamSelection | null>(null); let selection = $state<StreamSelection | null>(null);
let mediaSourceId = $state<string | null>(null); let mediaSourceId = $state<string | null>(null);
@@ -325,7 +325,7 @@
// //
// A downloaded file is a direct play over a local transport, and Rust // A downloaded file is a direct play over a local transport, and Rust
// says so rather than this page assuming it. // says so rather than this page assuming it.
// TRACES: UR-071 | DR-137, DR-224 // TRACES: UR-071 | DR-137, DR-225
selection = await commands.mediaLocalSelection(fullPath); selection = await commands.mediaLocalSelection(fullPath);
videoNeedsTranscoding = false; videoNeedsTranscoding = false;
// Use explicit startPosition, or fall back to retrieved progress from database // Use explicit startPosition, or fall back to retrieved progress from database
@@ -397,7 +397,7 @@
// to stop it a moment later. Observed in the log as a pair of // to stop it a moment later. Observed in the log as a pair of
// `[StreamSelection]` lines for one play. // `[StreamSelection]` lines for one play.
// //
// TRACES: UR-071 | DR-123, DR-137, DR-224 // TRACES: UR-071 | DR-123, DR-137, DR-225
const localPath = await commands.playerLocalMediaPath(id); const localPath = await commands.playerLocalMediaPath(id);
if (localPath) { if (localPath) {
// A downloaded file is a direct play over a local transport, served // A downloaded file is a direct play over a local transport, served
@@ -413,7 +413,7 @@
// the device profile and the ceiling in force, and returns the // the device profile and the ceiling in force, and returns the
// transport and the media-source id with it. This page no longer // transport and the media-source id with it. This page no longer
// decides — or separately asks for — any of that. // decides — or separately asks for — any of that.
// TRACES: UR-070, UR-079 | DR-224, DR-226, DR-227 // TRACES: UR-070, UR-079 | DR-225, DR-227, DR-228
selection = await repo.getStreamSelection(id, null, null); selection = await repo.getStreamSelection(id, null, null);
mediaSourceId = selection.mediaSourceId; mediaSourceId = selection.mediaSourceId;
// Rust's own verdict — "which kinds count as transcoding" is a // Rust's own verdict — "which kinds count as transcoding" is a