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:
@@ -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`:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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!(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: >k::Box) -> Result<VideoSurface, SurfaceError> {
|
pub fn attach(vbox: >k::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: >k::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() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 (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();
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
@@ -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 =
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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). */
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user