diff --git a/docs/specs/desktop-native-video.md b/docs/specs/desktop-native-video.md index f409918c..b1cb6aae 100644 --- a/docs/specs/desktop-native-video.md +++ b/docs/specs/desktop-native-video.md @@ -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::().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: diff --git a/docs/traceability.md b/docs/traceability.md index 9526e8ec..595d5e48 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -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` + diff --git a/src/lib/components/player/videoFit.test.ts b/src/lib/components/player/videoFit.test.ts index 82f055dd..10fb17a0 100644 --- a/src/lib/components/player/videoFit.test.ts +++ b/src/lib/components/player/videoFit.test.ts @@ -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 }); - }); -}); diff --git a/src/lib/components/player/videoFit.ts b/src/lib/components/player/videoFit.ts index fb93ae7d..2f88566c 100644 --- a/src/lib/components/player/videoFit.ts +++ b/src/lib/components/player/videoFit.ts @@ -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, - }; -} diff --git a/src/lib/player/localSource.test.ts b/src/lib/player/localSource.test.ts index dd542722..4d06f832 100644 --- a/src/lib/player/localSource.test.ts +++ b/src/lib/player/localSource.test.ts @@ -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"); - } - }); -}); diff --git a/src/lib/player/localSource.ts b/src/lib/player/localSource.ts index c84f90d9..ad65a751 100644 --- a/src/lib/player/localSource.ts +++ b/src/lib/player/localSource.ts @@ -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 `