refactor: delete two orphans, and record why the reparent design changed
`resolveVideoSource` chose between a local file and a remote URL for video playback. Backend-owned stream selection took that decision into Rust — `media_local_selection` for a downloaded file, `get_stream_selection` for a streamed one — and its last caller went with it. What remained was the function plus sixty lines of tests exercising nothing that ships. `fittedVideoSize` computed the rendered size of a video letterboxed into its container. Nothing has ever called it: it arrived with the fix that made the video fill its viewport and was superseded by `object-fit: contain` in the same change. There is some irony in a helper that models letterboxing sitting unused beside a container that was not letterboxing at all — the bug fixed in the previous commit was CSS, and this function would not have helped. A survey for exported symbols referenced only by their own tests finds 22 more. Most are legitimate — test mocks, deliberate reset hooks, public utility APIs — and the rest are unrelated to this work, so they are left for a cleanup that can be reviewed on its own terms rather than smuggled into a playback branch. Also records in the spec why DR-231's design changed. Reparenting Tauri's webview into a GtkOverlay aborts the process on the first click: Linux calls `attach_resize_handler` unconditionally (the Windows path guards it with `is_decorated()`), and its handler walks webview -> GtkBox -> GtkWindow with an unwrap that an overlay breaks. So the webview is not moved at all — mpv draws into the default vbox's own `draw` handler via `gdk_cairo_draw_from_gl()`, and GTK's container-before-children order puts the webview on top for free. No reparent, one less widget, and nothing a Tauri upgrade can invalidate by assuming its own layout.
This commit is contained in:
@@ -143,17 +143,41 @@ differs per platform:
|
||||
|
||||
| Platform | Surface | Status |
|
||||
|---|---|---|
|
||||
| Linux (X11 + Wayland) | `GtkOverlay`: `GtkGLArea` main child, Tauri's webview reparented as overlay child, via `WebviewWindow::gtk_window()` / `default_vbox()` | Proven by the spike, bar the reparent |
|
||||
| Linux (X11 + Wayland) | `gdk_cairo_draw_from_gl()` in the default vbox's `draw` handler, over a `GdkGLContext` on its `GdkWindow`. No reparenting — see below | Render path proven by the spike; the *overlay* approach it used is rejected |
|
||||
| Windows | Native HWND child beneath a transparent WebView2 | Phase 2 |
|
||||
|
||||
`vo=libmpv` plus `mpv_render_context_create` with `MPV_RENDER_PARAM_OPENGL_FBO`.
|
||||
Webview transparency via `with_transparent(true)` — no window-level transparency;
|
||||
the spike showed it is neither used nor needed.
|
||||
|
||||
**The untested half of G1 is the reparent.** The spike built its own overlay; the
|
||||
app must adopt Tauri's. Same widgets, one extra step, but it is the only place
|
||||
Tauri-specific behaviour can still bite — so it is the first gate, proven before
|
||||
anything is built on it.
|
||||
**G1's untested half failed, and the design changed because of it.**
|
||||
|
||||
Reparenting Tauri's webview into a `GtkOverlay` attaches cleanly and then aborts
|
||||
the process on the first click. `tauri-runtime-wry` connects a
|
||||
button-press handler to the webview that walks a hard-coded path:
|
||||
|
||||
```rust
|
||||
webview.parent() // "This one should be GtkBox"
|
||||
.parent() // ...and this one the GtkWindow
|
||||
.downcast::<gtk::Window>().unwrap()
|
||||
```
|
||||
|
||||
An overlay makes that chain `webview → GtkOverlay → GtkBox`, the downcast fails,
|
||||
and the panic is non-unwinding so it kills the app. Nothing in configuration
|
||||
avoids it: on Linux `attach_resize_handler` is called **unconditionally** (the
|
||||
Windows equivalent is guarded by `is_decorated()`), and the decoration check that
|
||||
would make the handler inert runs *after* the unwrap.
|
||||
|
||||
**So the webview is not moved at all.** mpv draws into the *default vbox's own
|
||||
`draw` handler* instead, via `gdk_cairo_draw_from_gl()` over a `GdkGLContext`
|
||||
created on that widget's `GdkWindow`. GTK3 draws a container before its children,
|
||||
so the webview composites on top for free — the same z-order the overlay was for,
|
||||
without touching the widget tree Tauri walks.
|
||||
|
||||
That is strictly better than the overlay it replaces: no reparent, no extra
|
||||
widget, and the arrangement cannot be broken by a Tauri upgrade that assumes its
|
||||
own layout. It is also why "the surface attached successfully" is not the gate —
|
||||
a click is.
|
||||
|
||||
Three traps from the spike, each of which cost a debugging cycle and each of
|
||||
which looks like a platform limitation and is not:
|
||||
|
||||
+180
-194
@@ -1,11 +1,11 @@
|
||||
# Code Traceability Matrix
|
||||
|
||||
**Generated:** 8/22/2026, 10:39:30 AM
|
||||
**Generated:** 8/22/2026, 11:26:46 AM
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total Files Scanned:** 394
|
||||
- **Total TRACES Found:** 1093
|
||||
- **Total TRACES Found:** 1092
|
||||
- **Requirements Covered:**
|
||||
- User Requirements (UR): 79
|
||||
- Integration Requirements (IR): 26
|
||||
@@ -3037,14 +3037,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
|
||||
### DR-123
|
||||
|
||||
**Locations:** 7 file(s)
|
||||
**Locations:** 6 file(s)
|
||||
|
||||
- **File:** [`src/lib/api/bindings.ts`](../src/lib/api/bindings.ts#L389)
|
||||
- **Line:** 389
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L12)
|
||||
- **Line:** 12
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src/routes/player/[id]/+page.svelte`](../src/routes/player/[id]/+page.svelte#L405)
|
||||
- **Line:** 405
|
||||
- **Context:** `Unknown`
|
||||
@@ -3265,8 +3262,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L55)
|
||||
- **Line:** 55
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src/routes/player/[id]/+page.svelte`](../src/routes/player/[id]/+page.svelte#L314)
|
||||
- **Line:** 314
|
||||
@@ -7523,22 +7520,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 8
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### IT-016
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3026)
|
||||
- **Line:** 3026
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### IT-017
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L878)
|
||||
- **Line:** 878
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-003
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
@@ -7739,6 +7720,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251)
|
||||
- **Line:** 251
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-030
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251)
|
||||
- **Line:** 251
|
||||
- **Context:** `Unknown`
|
||||
@@ -8779,14 +8768,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 929
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-062
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/backgroundAudio.test.ts`](../src/lib/utils/backgroundAudio.test.ts#L7)
|
||||
- **Line:** 7
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-066
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
@@ -8795,12 +8776,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 4
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-071
|
||||
### UT-067
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/formatBytes.test.ts`](../src/lib/utils/formatBytes.test.ts#L4)
|
||||
- **Line:** 4
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3026)
|
||||
- **Line:** 3026
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-072
|
||||
@@ -8956,46 +8937,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 15
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-094
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-095
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-096
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-097
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-098
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/layoutShell.test.ts`](../src/lib/utils/layoutShell.test.ts#L158)
|
||||
- **Line:** 158
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-099
|
||||
|
||||
**Locations:** 3 file(s)
|
||||
@@ -9033,6 +8974,34 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 3869
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-101
|
||||
|
||||
**Locations:** 4 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L2014)
|
||||
- **Line:** 2014
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4370)
|
||||
- **Line:** 4370
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4405)
|
||||
- **Line:** 4405
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/mod.rs`](../src-tauri/src/repository/mod.rs#L238)
|
||||
- **Line:** 238
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-102
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L688)
|
||||
- **Line:** 688
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4605)
|
||||
- **Line:** 4605
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-103
|
||||
|
||||
**Locations:** 6 file(s)
|
||||
@@ -9133,14 +9102,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 1106
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-118
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L12)
|
||||
- **Line:** 12
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-123
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
@@ -9156,8 +9117,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L55)
|
||||
- **Line:** 55
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-131
|
||||
@@ -10189,6 +10150,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 4413
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### IT-017
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L878)
|
||||
- **Line:** 878
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-060
|
||||
|
||||
**Locations:** 4 file(s)
|
||||
@@ -10206,6 +10175,22 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 110
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-062
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/backgroundAudio.test.ts`](../src/lib/utils/backgroundAudio.test.ts#L7)
|
||||
- **Line:** 7
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-071
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/formatBytes.test.ts`](../src/lib/utils/formatBytes.test.ts#L4)
|
||||
- **Line:** 4
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-086
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
@@ -10250,6 +10235,46 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 8
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-094
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-095
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-096
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-097
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/safeArea.test.ts`](../src/lib/utils/safeArea.test.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-098
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src/lib/utils/layoutShell.test.ts`](../src/lib/utils/layoutShell.test.ts#L158)
|
||||
- **Line:** 158
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-108
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
@@ -10261,20 +10286,51 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 550
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-109
|
||||
### UT-111
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
**Locations:** 3 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3306)
|
||||
- **Line:** 3306
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L62)
|
||||
- **Line:** 62
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L2548)
|
||||
- **Line:** 2548
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L2588)
|
||||
- **Line:** 2588
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-110
|
||||
### UT-112
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3362)
|
||||
- **Line:** 3362
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L501)
|
||||
- **Line:** 501
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3253)
|
||||
- **Line:** 3253
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-113
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L276)
|
||||
- **Line:** 276
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3144)
|
||||
- **Line:** 3144
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-114
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L333)
|
||||
- **Line:** 333
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3087)
|
||||
- **Line:** 3087
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-116
|
||||
@@ -10702,6 +10758,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 3394
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-171
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L916)
|
||||
- **Line:** 916
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L957)
|
||||
- **Line:** 957
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-172
|
||||
|
||||
**Locations:** 3 file(s)
|
||||
@@ -11096,7 +11163,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
|
||||
### UR-071
|
||||
|
||||
**Locations:** 67 file(s)
|
||||
**Locations:** 66 file(s)
|
||||
|
||||
- **File:** [`src/lib/api/bindings.ts`](../src/lib/api/bindings.ts#L389)
|
||||
- **Line:** 389
|
||||
@@ -11110,11 +11177,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **File:** [`src/lib/api/bindings.ts`](../src/lib/api/bindings.ts#L2085)
|
||||
- **Line:** 2085
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L12)
|
||||
- **Line:** 12
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L55)
|
||||
- **Line:** 55
|
||||
- **File:** [`src/lib/player/localSource.ts`](../src/lib/player/localSource.ts#L17)
|
||||
- **Line:** 17
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src/routes/player/[id]/+page.svelte`](../src/routes/player/[id]/+page.svelte#L314)
|
||||
- **Line:** 314
|
||||
@@ -12318,14 +12382,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 1286
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-030
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251)
|
||||
- **Line:** 251
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-031
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
@@ -12350,81 +12406,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 251
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-101
|
||||
|
||||
**Locations:** 4 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L2014)
|
||||
- **Line:** 2014
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4370)
|
||||
- **Line:** 4370
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4405)
|
||||
- **Line:** 4405
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/mod.rs`](../src-tauri/src/repository/mod.rs#L238)
|
||||
- **Line:** 238
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-102
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L688)
|
||||
- **Line:** 688
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4605)
|
||||
- **Line:** 4605
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-111
|
||||
|
||||
**Locations:** 3 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L62)
|
||||
- **Line:** 62
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L2548)
|
||||
- **Line:** 2548
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L2588)
|
||||
- **Line:** 2588
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-112
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L501)
|
||||
- **Line:** 501
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3253)
|
||||
- **Line:** 3253
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-113
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L276)
|
||||
- **Line:** 276
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3144)
|
||||
- **Line:** 3144
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-114
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L333)
|
||||
- **Line:** 333
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3087)
|
||||
- **Line:** 3087
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-115
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
@@ -12615,17 +12596,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 3788
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-171
|
||||
|
||||
**Locations:** 2 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L916)
|
||||
- **Line:** 916
|
||||
- **Context:** `Unknown`
|
||||
- **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L957)
|
||||
- **Line:** 957
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-173
|
||||
|
||||
**Locations:** 5 file(s)
|
||||
@@ -12974,7 +12944,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 1239
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-067
|
||||
### IT-016
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
@@ -12982,4 +12952,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010,
|
||||
- **Line:** 3026
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-109
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3306)
|
||||
- **Line:** 3306
|
||||
- **Context:** `Unknown`
|
||||
|
||||
### UT-110
|
||||
|
||||
**Locations:** 1 file(s)
|
||||
|
||||
- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3362)
|
||||
- **Line:** 3362
|
||||
- **Context:** `Unknown`
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { videoFitClass, fittedVideoSize } from "./videoFit";
|
||||
import { videoFitClass } from "./videoFit";
|
||||
|
||||
describe("videoFitClass", () => {
|
||||
it("fills the container instead of capping at the source's intrinsic size", () => {
|
||||
@@ -19,39 +19,3 @@ describe("videoFitClass", () => {
|
||||
expect(cls).not.toContain("object-fill");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fittedVideoSize", () => {
|
||||
it("scales a 480p source up to fill a larger window (the reported bug)", () => {
|
||||
// Exact 16:9 480p in a 1920x1080 window -> scales up to fill, rather than
|
||||
// staying a 854x480 box in the middle.
|
||||
const size = fittedVideoSize(853.33, 480, 1920, 1080);
|
||||
expect(size.width).toBeCloseTo(1920, 0);
|
||||
expect(size.height).toBeCloseTo(1080, 0);
|
||||
});
|
||||
|
||||
it("fits to the constraining dimension when aspect ratios differ", () => {
|
||||
// 4:3 source in a 16:9 window -> height-constrained, pillarboxed.
|
||||
const size = fittedVideoSize(640, 480, 1920, 1080);
|
||||
expect(size.height).toBeCloseTo(1080, 0);
|
||||
expect(size.width).toBeCloseTo(1440, 0);
|
||||
expect(size.width).toBeLessThan(1920);
|
||||
});
|
||||
|
||||
it("fits to width when the source is wider than the window", () => {
|
||||
// 21:9 source in a 16:9 window -> width-constrained, letterboxed.
|
||||
const size = fittedVideoSize(2560, 1080, 1920, 1080);
|
||||
expect(size.width).toBeCloseTo(1920, 0);
|
||||
expect(size.height).toBeCloseTo(810, 0);
|
||||
expect(size.height).toBeLessThan(1080);
|
||||
});
|
||||
|
||||
it("shrinks oversized media to fit rather than overflowing", () => {
|
||||
const size = fittedVideoSize(3840, 2160, 1280, 720);
|
||||
expect(size.width).toBeCloseTo(1280, 0);
|
||||
expect(size.height).toBeCloseTo(720, 0);
|
||||
});
|
||||
|
||||
it("returns a zero size for unknown intrinsic dimensions", () => {
|
||||
expect(fittedVideoSize(0, 0, 1920, 1080)).toEqual({ width: 0, height: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,32 +15,3 @@
|
||||
export function videoFitClass(): string {
|
||||
return "w-full h-full object-contain";
|
||||
}
|
||||
|
||||
export interface FittedSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The rendered size of a video of the given intrinsic dimensions once it has
|
||||
* been fitted into the container - i.e. scaled (up or down) so that it touches
|
||||
* the container on its constraining axis, with the other axis letter/pillar
|
||||
* boxed. Mirrors what `object-fit: contain` on a full-size element does.
|
||||
*/
|
||||
export function fittedVideoSize(
|
||||
intrinsicWidth: number,
|
||||
intrinsicHeight: number,
|
||||
containerWidth: number,
|
||||
containerHeight: number,
|
||||
): FittedSize {
|
||||
if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
const scale = Math.min(containerWidth / intrinsicWidth, containerHeight / intrinsicHeight);
|
||||
|
||||
return {
|
||||
width: intrinsicWidth * scale,
|
||||
height: intrinsicHeight * scale,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { downloadedFilePath, resolveVideoSource } from "./localSource";
|
||||
import { downloadedFilePath } from "./localSource";
|
||||
|
||||
describe("downloadedFilePath", () => {
|
||||
// The download worker rewrites `downloads.file_path` to the absolute path it
|
||||
@@ -29,74 +29,3 @@ describe("downloadedFilePath", () => {
|
||||
expect(downloadedFilePath("C:\\Users\\u\\AppData\\jellytau", stored)).toBe(stored);
|
||||
});
|
||||
});
|
||||
|
||||
// A stand-in for Tauri's convertFileSrc, so the module stays pure.
|
||||
const toAssetUrl = (p: string) => `asset://localhost/${encodeURIComponent(p)}`;
|
||||
|
||||
describe("resolveVideoSource", () => {
|
||||
it("plays the downloaded file when one exists", () => {
|
||||
const decision = resolveVideoSource({
|
||||
localPath: "/home/u/.local/share/jellytau/movie.mp4",
|
||||
remoteUrl: "https://server/Videos/abc/master.m3u8",
|
||||
remoteNeedsTranscoding: true,
|
||||
toAssetUrl,
|
||||
});
|
||||
|
||||
expect(decision.isLocal).toBe(true);
|
||||
expect(decision.url).toBe(toAssetUrl("/home/u/.local/share/jellytau/movie.mp4"));
|
||||
});
|
||||
|
||||
it("never marks a local file as needing transcoding, even when the remote did", () => {
|
||||
// The transcoded path re-requests a whole new stream URL on every seek.
|
||||
// A local file seeks natively; sending it down that route would ask the
|
||||
// server for a stream we deliberately avoided.
|
||||
const decision = resolveVideoSource({
|
||||
localPath: "/downloads/film.mkv",
|
||||
remoteUrl: "https://server/Videos/abc/master.m3u8",
|
||||
remoteNeedsTranscoding: true,
|
||||
toAssetUrl,
|
||||
});
|
||||
|
||||
expect(decision.needsTranscoding).toBe(false);
|
||||
});
|
||||
|
||||
it("streams when nothing is downloaded, preserving the transcoding flag", () => {
|
||||
const decision = resolveVideoSource({
|
||||
localPath: null,
|
||||
remoteUrl: "https://server/Videos/abc/master.m3u8",
|
||||
remoteNeedsTranscoding: true,
|
||||
toAssetUrl,
|
||||
});
|
||||
|
||||
expect(decision).toEqual({
|
||||
url: "https://server/Videos/abc/master.m3u8",
|
||||
needsTranscoding: true,
|
||||
isLocal: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("streams a direct-play remote without claiming it transcodes", () => {
|
||||
const decision = resolveVideoSource({
|
||||
localPath: null,
|
||||
remoteUrl: "https://server/Videos/abc/stream.mp4",
|
||||
remoteNeedsTranscoding: false,
|
||||
toAssetUrl,
|
||||
});
|
||||
|
||||
expect(decision.needsTranscoding).toBe(false);
|
||||
expect(decision.isLocal).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to streaming for a blank path rather than building a dead asset URL", () => {
|
||||
for (const localPath of ["", " "]) {
|
||||
const decision = resolveVideoSource({
|
||||
localPath,
|
||||
remoteUrl: "https://server/stream",
|
||||
remoteNeedsTranscoding: false,
|
||||
toAssetUrl,
|
||||
});
|
||||
expect(decision.isLocal).toBe(false);
|
||||
expect(decision.url).toBe("https://server/stream");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,41 +1,3 @@
|
||||
/**
|
||||
* Choosing between a downloaded file and a server stream for video playback.
|
||||
*
|
||||
* Audio has preferred local files since the queue is built (the Rust queue
|
||||
* resolves `MediaSource::Local`), but video asks the repository for a stream URL
|
||||
* and never consults `downloads` — so a downloaded film was streamed anyway,
|
||||
* spending bandwidth that had already been spent and failing outright offline.
|
||||
*
|
||||
* Pure so it can be unit-tested: the component only supplies the two inputs and
|
||||
* the asset-URL converter.
|
||||
*
|
||||
* TRACES: UR-071 | DR-123 | UT-118
|
||||
*/
|
||||
|
||||
export interface VideoSourceInputs {
|
||||
/** Absolute on-disk path of a completed download, or null to stream. */
|
||||
localPath: string | null;
|
||||
/** Stream URL the repository resolved (already transcoded if it had to be). */
|
||||
remoteUrl: string;
|
||||
/** Whether the *remote* stream is a transcode. */
|
||||
remoteNeedsTranscoding: boolean;
|
||||
/** Usually Tauri's `convertFileSrc`; injected so this module stays pure. */
|
||||
toAssetUrl: (path: string) => string;
|
||||
}
|
||||
|
||||
export interface VideoSourceDecision {
|
||||
/** What to hand the `<video>` element. */
|
||||
url: string;
|
||||
/**
|
||||
* Local files are never transcodes, so this is always false for them. It
|
||||
* matters because the transcoded path re-requests a whole new stream URL on
|
||||
* every seek; a local file seeks natively and must not go down that route.
|
||||
*/
|
||||
needsTranscoding: boolean;
|
||||
/** True when playing from disk — for logging and the offline badge. */
|
||||
isLocal: boolean;
|
||||
}
|
||||
|
||||
/** Absolute on POSIX (`/…`), Windows (`C:\…`, `C:/…`) or a UNC share (`\\…`). */
|
||||
function isAbsolute(path: string): boolean {
|
||||
return path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(path);
|
||||
@@ -57,15 +19,3 @@ function isAbsolute(path: string): boolean {
|
||||
export function downloadedFilePath(storageRoot: string, filePath: string): string {
|
||||
return isAbsolute(filePath) ? filePath : `${storageRoot}/${filePath}`;
|
||||
}
|
||||
|
||||
export function resolveVideoSource(inputs: VideoSourceInputs): VideoSourceDecision {
|
||||
const { localPath, remoteUrl, remoteNeedsTranscoding, toAssetUrl } = inputs;
|
||||
|
||||
// Treat blank/whitespace paths as absent — a malformed `downloads` row must
|
||||
// not produce an asset URL pointing at nothing.
|
||||
if (localPath && localPath.trim() !== "") {
|
||||
return { url: toAssetUrl(localPath), needsTranscoding: false, isLocal: true };
|
||||
}
|
||||
|
||||
return { url: remoteUrl, needsTranscoding: remoteNeedsTranscoding, isLocal: false };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user