diff --git a/docs/traceability.md b/docs/traceability.md index 595d5e48..5e4ea104 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,15 +1,15 @@ # Code Traceability Matrix -**Generated:** 8/22/2026, 11:26:46 AM +**Generated:** 8/22/2026, 11:51:14 AM ## Summary -- **Total Files Scanned:** 394 -- **Total TRACES Found:** 1092 +- **Total Files Scanned:** 395 +- **Total TRACES Found:** 1098 - **Requirements Covered:** - User Requirements (UR): 79 - Integration Requirements (IR): 26 - - Development Requirements (DR): 198 + - Development Requirements (DR): 199 - Jellyfin API Requirements (JA): 35 ## Requirements by Type @@ -26,7 +26,7 @@ IR-003, IR-004, IR-006, IR-008, IR-009, IR-010, IR-011, IR-012, IR-013, IR-014, ### Development Requirements (DR) ``` -DR-001, DR-002, DR-003, DR-004, DR-005, DR-006, DR-007, DR-009, DR-010, DR-011, DR-012, DR-013, DR-014, DR-015, DR-016, DR-017, DR-018, DR-020, DR-021, DR-022, DR-023, DR-024, DR-025, DR-026, DR-027, DR-028, DR-029, DR-030, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-042, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086, DR-087, DR-088, DR-089, DR-090, DR-091, DR-092, DR-093, DR-095, DR-096, DR-097, DR-098, DR-099, DR-100, DR-101, DR-102, DR-103, DR-104, DR-105, DR-106, DR-107, DR-108, DR-109, DR-110, DR-111, DR-112, DR-113, DR-114, DR-115, DR-116, DR-117, DR-118, DR-119, DR-120, DR-121, DR-123, DR-126, DR-127, DR-128, DR-129, DR-130, DR-131, DR-132, DR-133, DR-134, DR-135, DR-136, DR-137, DR-140, DR-141, DR-142, DR-143, DR-147, DR-148, DR-149, DR-150, DR-151, DR-152, DR-153, DR-154, DR-155, DR-156, DR-157, DR-158, DR-159, DR-160, DR-162, DR-167, DR-168, DR-169, DR-170, DR-171, DR-173, DR-174, DR-175, DR-176, DR-177, DR-178, DR-179, DR-180, DR-181, DR-182, DR-183, DR-185, DR-186, DR-188, DR-189, DR-193, DR-195, DR-196, DR-197, DR-198, DR-201, DR-203, DR-204, DR-205, DR-206, DR-207, DR-209, DR-210, DR-211, DR-212, DR-213, DR-215, DR-216, DR-217, DR-218, DR-219, DR-220, DR-221, DR-223, DR-224, DR-225, DR-226, DR-227, DR-228, DR-230, DR-231, DR-232, DR-234 +DR-001, DR-002, DR-003, DR-004, DR-005, DR-006, DR-007, DR-009, DR-010, DR-011, DR-012, DR-013, DR-014, DR-015, DR-016, DR-017, DR-018, DR-020, DR-021, DR-022, DR-023, DR-024, DR-025, DR-026, DR-027, DR-028, DR-029, DR-030, DR-032, DR-033, DR-034, DR-035, DR-036, DR-037, DR-038, DR-039, DR-040, DR-041, DR-042, DR-043, DR-044, DR-045, DR-047, DR-048, DR-049, DR-050, DR-051, DR-052, DR-053, DR-054, DR-055, DR-056, DR-057, DR-058, DR-059, DR-060, DR-061, DR-062, DR-063, DR-064, DR-065, DR-066, DR-067, DR-068, DR-069, DR-070, DR-074, DR-075, DR-076, DR-077, DR-078, DR-079, DR-080, DR-081, DR-082, DR-083, DR-084, DR-085, DR-086, DR-087, DR-088, DR-089, DR-090, DR-091, DR-092, DR-093, DR-095, DR-096, DR-097, DR-098, DR-099, DR-100, DR-101, DR-102, DR-103, DR-104, DR-105, DR-106, DR-107, DR-108, DR-109, DR-110, DR-111, DR-112, DR-113, DR-114, DR-115, DR-116, DR-117, DR-118, DR-119, DR-120, DR-121, DR-123, DR-126, DR-127, DR-128, DR-129, DR-130, DR-131, DR-132, DR-133, DR-134, DR-135, DR-136, DR-137, DR-140, DR-141, DR-142, DR-143, DR-147, DR-148, DR-149, DR-150, DR-151, DR-152, DR-153, DR-154, DR-155, DR-156, DR-157, DR-158, DR-159, DR-160, DR-162, DR-167, DR-168, DR-169, DR-170, DR-171, DR-173, DR-174, DR-175, DR-176, DR-177, DR-178, DR-179, DR-180, DR-181, DR-182, DR-183, DR-185, DR-186, DR-188, DR-189, DR-193, DR-195, DR-196, DR-197, DR-198, DR-201, DR-203, DR-204, DR-205, DR-206, DR-207, DR-209, DR-210, DR-211, DR-212, DR-213, DR-215, DR-216, DR-217, DR-218, DR-219, DR-220, DR-221, DR-223, DR-224, DR-225, DR-226, DR-227, DR-228, DR-230, DR-231, DR-232, DR-233, DR-234 ``` ### Jellyfin API Requirements (JA) @@ -65,11 +65,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/android/mod.rs`](../src-tauri/src/player/android/mod.rs#L1425) - **Line:** 1425 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L153) - - **Line:** 153 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L160) + - **Line:** 160 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L175) - - **Line:** 175 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L182) + - **Line:** 182 - **Context:** `Unknown` ### IR-008 @@ -293,20 +293,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/player/settings.rs`](../src-tauri/src/commands/player/settings.rs#L3) - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L658) - - **Line:** 658 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L691) + - **Line:** 691 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L678) - - **Line:** 678 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L711) + - **Line:** 711 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L747) - - **Line:** 747 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L780) + - **Line:** 780 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L759) - - **Line:** 759 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L792) + - **Line:** 792 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L833) - - **Line:** 833 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L866) + - **Line:** 866 - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L1) - **Line:** 1 @@ -422,8 +422,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/catalog.rs`](../src-tauri/src/commands/catalog.rs#L327) - **Line:** 327 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1618) - - **Line:** 1618 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1619) + - **Line:** 1619 - **Context:** `Unknown` ### IR-031 @@ -439,10 +439,19 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, ### IR-033 -**Locations:** 1 file(s) +**Locations:** 4 file(s) -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L13) - - **Line:** 13 +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L11) + - **Line:** 11 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L60) + - **Line:** 60 + - **Context:** `/ holding a lazily-resolving function pointer. So the address `dlsym` ...` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L160) + - **Line:** 160 + - **Context:** `impl MpvRenderContext {` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L29) + - **Line:** 29 - **Context:** `Unknown` ### DR-001 @@ -1167,8 +1176,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/sleep_timer.rs`](../src-tauri/src/player/sleep_timer.rs#L81) - **Line:** 81 - **Context:** `pub fn cancel(&mut self) {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L435) - - **Line:** 435 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L442) + - **Line:** 442 - **Context:** `Unknown` ### DR-030 @@ -1299,20 +1308,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/android/mod.rs`](../src-tauri/src/player/android/mod.rs#L617) - **Line:** 617 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L658) - - **Line:** 658 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L691) + - **Line:** 691 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L712) - - **Line:** 712 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L745) + - **Line:** 745 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L778) - - **Line:** 778 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L811) + - **Line:** 811 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L788) - - **Line:** 788 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L821) + - **Line:** 821 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L833) - - **Line:** 833 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L866) + - **Line:** 866 - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L1) - **Line:** 1 @@ -1647,32 +1656,32 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L929) - **Line:** 929 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L268) - - **Line:** 268 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L275) + - **Line:** 275 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L279) - - **Line:** 279 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L286) + - **Line:** 286 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1580) - - **Line:** 1580 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1587) + - **Line:** 1587 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1594) - - **Line:** 1594 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1601) + - **Line:** 1601 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1610) - - **Line:** 1610 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1617) + - **Line:** 1617 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1619) - - **Line:** 1619 - - **Context:** `pub fn exit_background_audio(&self) -> f64 {` - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1626) - **Line:** 1626 + - **Context:** `pub fn exit_background_audio(&self) -> f64 {` +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1633) + - **Line:** 1633 - **Context:** `pub fn is_background_audio_active(&self) -> bool {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1641) - - **Line:** 1641 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1648) + - **Line:** 1648 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1883) - - **Line:** 1883 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1890) + - **Line:** 1890 - **Context:** `Unknown` ### DR-053 @@ -1755,8 +1764,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L289) - - **Line:** 289 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L278) + - **Line:** 278 - **Context:** `Unknown` ### DR-062 @@ -1766,11 +1775,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L289) - - **Line:** 289 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L278) + - **Line:** 278 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L394) - - **Line:** 394 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L383) + - **Line:** 383 - **Context:** `Unknown` - **File:** [`src/lib/components/library/EpisodeFocusView.test.ts`](../src/lib/components/library/EpisodeFocusView.test.ts#L7) - **Line:** 7 @@ -2404,23 +2413,23 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/services/playerEvents.ts`](../src/lib/services/playerEvents.ts#L8) - **Line:** 8 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L279) - - **Line:** 279 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L286) + - **Line:** 286 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L660) - - **Line:** 660 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L667) + - **Line:** 667 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L668) - - **Line:** 668 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L675) + - **Line:** 675 - **Context:** `pub fn is_html5_active(&self) -> bool {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L712) - - **Line:** 712 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L719) + - **Line:** 719 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1594) - - **Line:** 1594 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1601) + - **Line:** 1601 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1610) - - **Line:** 1610 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1617) + - **Line:** 1617 - **Context:** `Unknown` ### DR-098 @@ -2478,8 +2487,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#L1569) - **Line:** 1569 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L205) - - **Line:** 205 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L206) + - **Line:** 206 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/repository.rs`](../src-tauri/src/commands/repository.rs#L356) - **Line:** 356 @@ -2510,11 +2519,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L205) - - **Line:** 205 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L206) + - **Line:** 206 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L326) - - **Line:** 326 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L315) + - **Line:** 315 - **Context:** `async function handlePlayAll() {` ### DR-103 @@ -2533,8 +2542,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L124) - - **Line:** 124 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L125) + - **Line:** 125 - **Context:** `Unknown` - **File:** [`src/routes/library/tv/+page.svelte`](../src/routes/library/tv/+page.svelte#L8) - **Line:** 8 @@ -2659,8 +2668,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/repository.rs`](../src-tauri/src/commands/repository.rs#L51) - **Line:** 51 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1618) - - **Line:** 1618 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1619) + - **Line:** 1619 - **Context:** `Unknown` ### DR-110 @@ -2913,8 +2922,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/ArtistDetailView.svelte`](../src/lib/components/library/ArtistDetailView.svelte#L138) - **Line:** 138 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L250) - - **Line:** 250 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L239) + - **Line:** 239 - **Context:** `Unknown` - **File:** [`src/lib/components/library/MediaCard.svelte`](../src/lib/components/library/MediaCard.svelte#L1) - **Line:** 1 @@ -2949,8 +2958,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/stores/favorites.ts`](../src/lib/stores/favorites.ts#L79) - **Line:** 79 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L619) - - **Line:** 619 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L608) + - **Line:** 608 - **Context:** `Unknown` ### DR-120 @@ -3011,8 +3020,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L195) - **Line:** 195 - **Context:** `impl OnlineRepository {` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1625) - - **Line:** 1625 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1626) + - **Line:** 1626 - **Context:** `Unknown` ### DR-121 @@ -3122,20 +3131,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/stream_end.rs`](../src-tauri/src/player/stream_end.rs#L54) - **Line:** 54 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L288) - - **Line:** 288 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L295) + - **Line:** 295 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1698) - - **Line:** 1698 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1705) + - **Line:** 1705 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1731) - - **Line:** 1731 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1738) + - **Line:** 1738 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1788) - - **Line:** 1788 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1795) + - **Line:** 1795 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1819) - - **Line:** 1819 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1826) + - **Line:** 1826 - **Context:** `Unknown` ### DR-130 @@ -3154,14 +3163,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/player/timers.rs`](../src-tauri/src/commands/player/timers.rs#L299) - **Line:** 299 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L564) - - **Line:** 564 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L597) + - **Line:** 597 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L573) - - **Line:** 573 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L606) + - **Line:** 606 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1788) - - **Line:** 1788 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1795) + - **Line:** 1795 - **Context:** `Unknown` ### DR-131 @@ -3213,8 +3222,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L2690) - **Line:** 2690 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1632) - - **Line:** 1632 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1633) + - **Line:** 1633 - **Context:** `Unknown` ### DR-132 @@ -3369,8 +3378,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1126) - **Line:** 1126 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1535) - - **Line:** 1535 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1536) + - **Line:** 1536 - **Context:** `Unknown` ### DR-140 @@ -3405,11 +3414,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L250) - - **Line:** 250 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L239) + - **Line:** 239 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L394) - - **Line:** 394 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L383) + - **Line:** 383 - **Context:** `Unknown` - **File:** [`src/lib/components/library/EpisodeFocusView.test.ts`](../src/lib/components/library/EpisodeFocusView.test.ts#L7) - **Line:** 7 @@ -3423,14 +3432,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L139) - - **Line:** 139 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L140) + - **Line:** 140 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L233) - - **Line:** 233 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L234) + - **Line:** 234 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L467) - - **Line:** 467 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L456) + - **Line:** 456 - **Context:** `Unknown` - **File:** [`src/routes/search/+page.svelte`](../src/routes/search/+page.svelte#L97) - **Line:** 97 @@ -3458,8 +3467,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/services/offlineCatalog.ts`](../src/lib/services/offlineCatalog.ts#L46) - **Line:** 46 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L100) - - **Line:** 100 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L101) + - **Line:** 101 - **Context:** `Unknown` - **File:** [`src/routes/library/favorites/+page.svelte`](../src/routes/library/favorites/+page.svelte#L88) - **Line:** 88 @@ -3655,14 +3664,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, **Locations:** 3 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L881) - - **Line:** 881 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L888) + - **Line:** 888 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4099) - - **Line:** 4099 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4106) + - **Line:** 4106 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4142) - - **Line:** 4142 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4149) + - **Line:** 4149 - **Context:** `Unknown` ### DR-160 @@ -3791,8 +3800,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L716) - **Line:** 716 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1492) - - **Line:** 1492 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1493) + - **Line:** 1493 - **Context:** `Unknown` ### DR-167 @@ -4119,41 +4128,41 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L2108) - **Line:** 2108 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L312) - - **Line:** 312 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L319) + - **Line:** 319 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L508) - - **Line:** 508 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L515) + - **Line:** 515 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L953) - - **Line:** 953 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L960) + - **Line:** 960 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L968) - - **Line:** 968 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L975) + - **Line:** 975 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L979) - - **Line:** 979 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L986) + - **Line:** 986 - **Context:** `pub fn observed_duration(&self) -> Option {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1017) - - **Line:** 1017 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1024) + - **Line:** 1024 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4179) - - **Line:** 4179 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4186) + - **Line:** 4186 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4198) - - **Line:** 4198 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4205) + - **Line:** 4205 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4215) - - **Line:** 4215 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4222) + - **Line:** 4222 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4230) - - **Line:** 4230 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4237) + - **Line:** 4237 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4244) - - **Line:** 4244 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4251) + - **Line:** 4251 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4293) - - **Line:** 4293 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4300) + - **Line:** 4300 - **Context:** `Unknown` ### DR-179 @@ -4166,44 +4175,44 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/services/playbackReporting.ts`](../src/lib/services/playbackReporting.ts#L76) - **Line:** 76 - **Context:** `* This function is therefore the *local* half only, which is what its ...` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L76) - - **Line:** 76 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L83) + - **Line:** 83 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L128) - - **Line:** 128 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L135) + - **Line:** 135 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L772) - - **Line:** 772 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L779) + - **Line:** 779 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L987) - - **Line:** 987 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L994) + - **Line:** 994 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L996) - - **Line:** 996 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1003) + - **Line:** 1003 - **Context:** `pub fn set_report_sink(&self, sink: Arc) {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1315) - - **Line:** 1315 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1322) + - **Line:** 1322 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1559) - - **Line:** 1559 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1566) + - **Line:** 1566 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4266) - - **Line:** 4266 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4273) + - **Line:** 4273 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4293) - - **Line:** 4293 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4300) + - **Line:** 4300 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4331) - - **Line:** 4331 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4338) + - **Line:** 4338 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4359) - - **Line:** 4359 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4366) + - **Line:** 4366 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4388) - - **Line:** 4388 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4395) + - **Line:** 4395 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4406) - - **Line:** 4406 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4413) + - **Line:** 4413 - **Context:** `Unknown` ### DR-180 @@ -4222,11 +4231,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L3184) - **Line:** 3184 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L886) - - **Line:** 886 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L893) + - **Line:** 893 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4036) - - **Line:** 4036 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4043) + - **Line:** 4043 - **Context:** `Unknown` ### DR-181 @@ -4343,11 +4352,11 @@ 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-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L545) - - **Line:** 545 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L552) + - **Line:** 552 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L3264) - - **Line:** 3264 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L3271) + - **Line:** 3271 - **Context:** `Unknown` ### DR-195 @@ -4629,8 +4638,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L357) - **Line:** 357 - **Context:** `impl LibrarySettings {` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1504) - - **Line:** 1504 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1505) + - **Line:** 1505 - **Context:** `Unknown` ### DR-210 @@ -4769,8 +4778,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/settings/+page.svelte`](../src/routes/settings/+page.svelte#L1299) - **Line:** 1299 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1286) - - **Line:** 1286 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1287) + - **Line:** 1287 - **Context:** `Unknown` ### DR-218 @@ -4822,8 +4831,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1186) - **Line:** 1186 - **Context:** `pub fn run() {` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1278) - - **Line:** 1278 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1279) + - **Line:** 1279 - **Context:** `Unknown` ### DR-219 @@ -4872,8 +4881,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L625) - **Line:** 625 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1348) - - **Line:** 1348 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1349) + - **Line:** 1349 - **Context:** `Unknown` ### DR-224 @@ -5313,25 +5322,28 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, ### DR-231 -**Locations:** 7 file(s) +**Locations:** 8 file(s) -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L13) - - **Line:** 13 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L104) + - **Line:** 104 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L87) - - **Line:** 87 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L110) + - **Line:** 110 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L138) - - **Line:** 138 +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L11) + - **Line:** 11 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L169) - - **Line:** 169 +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L160) + - **Line:** 160 + - **Context:** `impl MpvRenderContext {` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L229) + - **Line:** 229 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L198) - - **Line:** 198 +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L29) + - **Line:** 29 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L222) - - **Line:** 222 +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L124) + - **Line:** 124 - **Context:** `Unknown` - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1239) - **Line:** 1239 @@ -5339,15 +5351,41 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, ### DR-232 -**Locations:** 2 file(s) +**Locations:** 4 file(s) -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L138) - - **Line:** 138 +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L11) + - **Line:** 11 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L29) + - **Line:** 29 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L124) + - **Line:** 124 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L198) - **Line:** 198 - **Context:** `Unknown` +### DR-233 + +**Locations:** 5 file(s) + +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L217) + - **Line:** 217 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L273) + - **Line:** 273 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L29) + - **Line:** 29 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L98) + - **Line:** 98 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L124) + - **Line:** 124 + - **Context:** `Unknown` + ### DR-234 **Locations:** 7 file(s) @@ -5931,8 +5969,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/storage/schema.rs`](../src-tauri/src/storage/schema.rs#L3) - **Line:** 3 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1632) - - **Line:** 1632 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1633) + - **Line:** 1633 - **Context:** `Unknown` ### UR-003 @@ -6083,11 +6121,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L545) - - **Line:** 545 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L552) + - **Line:** 552 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L3264) - - **Line:** 3264 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L3271) + - **Line:** 3271 - **Context:** `Unknown` ### UR-004 @@ -6241,14 +6279,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1698) - - **Line:** 1698 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1705) + - **Line:** 1705 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1788) - - **Line:** 1788 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1795) + - **Line:** 1795 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1819) - - **Line:** 1819 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1826) + - **Line:** 1826 - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L29) - **Line:** 29 @@ -6546,11 +6584,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/events.rs`](../src-tauri/src/player/events.rs#L23) - **Line:** 23 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L564) - - **Line:** 564 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L597) + - **Line:** 597 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L573) - - **Line:** 573 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L606) + - **Line:** 606 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/webview_audio_backend.rs`](../src-tauri/src/player/webview_audio_backend.rs#L25) - **Line:** 25 @@ -6591,71 +6629,71 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L312) - - **Line:** 312 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L319) + - **Line:** 319 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L508) - - **Line:** 508 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L515) + - **Line:** 515 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L545) - - **Line:** 545 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L552) + - **Line:** 552 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L660) - - **Line:** 660 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L667) + - **Line:** 667 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L668) - - **Line:** 668 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L675) + - **Line:** 675 - **Context:** `pub fn is_html5_active(&self) -> bool {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L712) - - **Line:** 712 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L719) + - **Line:** 719 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L772) - - **Line:** 772 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L779) + - **Line:** 779 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L881) - - **Line:** 881 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L888) + - **Line:** 888 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L953) - - **Line:** 953 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L960) + - **Line:** 960 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L968) - - **Line:** 968 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L975) + - **Line:** 975 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L979) - - **Line:** 979 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L986) + - **Line:** 986 - **Context:** `pub fn observed_duration(&self) -> Option {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1017) - - **Line:** 1017 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1024) + - **Line:** 1024 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1315) - - **Line:** 1315 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1322) + - **Line:** 1322 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1594) - - **Line:** 1594 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1601) + - **Line:** 1601 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1610) - - **Line:** 1610 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1617) + - **Line:** 1617 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L3264) - - **Line:** 3264 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L3271) + - **Line:** 3271 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4099) - - **Line:** 4099 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4106) + - **Line:** 4106 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4142) - - **Line:** 4142 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4149) + - **Line:** 4149 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4215) - - **Line:** 4215 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4222) + - **Line:** 4222 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4230) - - **Line:** 4230 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4237) + - **Line:** 4237 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4388) - - **Line:** 4388 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4395) + - **Line:** 4395 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4406) - - **Line:** 4406 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4413) + - **Line:** 4413 - **Context:** `Unknown` ### UR-007 @@ -7407,80 +7445,80 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/stream_end.rs`](../src-tauri/src/player/stream_end.rs#L91) - **Line:** 91 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L128) - - **Line:** 128 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L135) + - **Line:** 135 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L268) - - **Line:** 268 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L275) + - **Line:** 275 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L279) - - **Line:** 279 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L286) + - **Line:** 286 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L288) - - **Line:** 288 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L295) + - **Line:** 295 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L881) - - **Line:** 881 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L888) + - **Line:** 888 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L953) - - **Line:** 953 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L960) + - **Line:** 960 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1559) - - **Line:** 1559 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1566) + - **Line:** 1566 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1580) - - **Line:** 1580 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1587) + - **Line:** 1587 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1594) - - **Line:** 1594 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1601) + - **Line:** 1601 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1610) - - **Line:** 1610 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1617) + - **Line:** 1617 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1619) - - **Line:** 1619 - - **Context:** `pub fn exit_background_audio(&self) -> f64 {` - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1626) - **Line:** 1626 + - **Context:** `pub fn exit_background_audio(&self) -> f64 {` +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1633) + - **Line:** 1633 - **Context:** `pub fn is_background_audio_active(&self) -> bool {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1641) - - **Line:** 1641 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1648) + - **Line:** 1648 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1698) - - **Line:** 1698 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1705) + - **Line:** 1705 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1731) - - **Line:** 1731 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1738) + - **Line:** 1738 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1788) - - **Line:** 1788 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1795) + - **Line:** 1795 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1819) - - **Line:** 1819 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1826) + - **Line:** 1826 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1883) - - **Line:** 1883 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1890) + - **Line:** 1890 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4036) - - **Line:** 4036 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4043) + - **Line:** 4043 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4099) - - **Line:** 4099 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4106) + - **Line:** 4106 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4179) - - **Line:** 4179 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4186) + - **Line:** 4186 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4198) - - **Line:** 4198 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4205) + - **Line:** 4205 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4244) - - **Line:** 4244 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4251) + - **Line:** 4251 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4331) - - **Line:** 4331 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4338) + - **Line:** 4338 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4359) - - **Line:** 4359 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4366) + - **Line:** 4366 - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/hybrid.rs`](../src-tauri/src/repository/hybrid.rs#L121) - **Line:** 121 @@ -7520,6 +7558,14 @@ 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` + ### UT-003 **Locations:** 1 file(s) @@ -7552,14 +7598,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **Line:** 2 - **Context:** `Unknown` -### UT-012 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/sleep_timer.rs`](../src-tauri/src/player/sleep_timer.rs#L81) - - **Line:** 81 - - **Context:** `pub fn cancel(&mut self) {` - ### UT-014 **Locations:** 2 file(s) @@ -7716,15 +7754,7 @@ 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-029 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251) - - **Line:** 251 - - **Context:** `Unknown` - -### UT-030 +### UT-032 **Locations:** 1 file(s) @@ -7897,14 +7927,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L435) - - **Line:** 435 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L442) + - **Line:** 442 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1641) - - **Line:** 1641 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1648) + - **Line:** 1648 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1883) - - **Line:** 1883 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1890) + - **Line:** 1890 - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L1306) - **Line:** 1306 @@ -8055,50 +8085,50 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/repository.rs`](../src-tauri/src/commands/repository.rs#L764) - **Line:** 764 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L76) - - **Line:** 76 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L83) + - **Line:** 83 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L128) - - **Line:** 128 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L135) + - **Line:** 135 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L312) - - **Line:** 312 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L319) + - **Line:** 319 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L772) - - **Line:** 772 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L779) + - **Line:** 779 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L953) - - **Line:** 953 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L960) + - **Line:** 960 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L987) - - **Line:** 987 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L994) + - **Line:** 994 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L996) - - **Line:** 996 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1003) + - **Line:** 1003 - **Context:** `pub fn set_report_sink(&self, sink: Arc) {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1315) - - **Line:** 1315 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1322) + - **Line:** 1322 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1559) - - **Line:** 1559 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1566) + - **Line:** 1566 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4215) - - **Line:** 4215 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4222) + - **Line:** 4222 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4266) - - **Line:** 4266 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4273) + - **Line:** 4273 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4293) - - **Line:** 4293 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4300) + - **Line:** 4300 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4331) - - **Line:** 4331 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4338) + - **Line:** 4338 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4359) - - **Line:** 4359 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4366) + - **Line:** 4366 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4388) - - **Line:** 4388 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4395) + - **Line:** 4395 - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L688) - **Line:** 688 @@ -8124,8 +8154,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/storage/mod.rs`](../src-tauri/src/storage/mod.rs#L157) - **Line:** 157 - **Context:** `pub fn file_size(&self) -> Option {` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1632) - - **Line:** 1632 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1633) + - **Line:** 1633 - **Context:** `Unknown` ### UR-026 @@ -8186,8 +8216,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L2) - **Line:** 2 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L435) - - **Line:** 435 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L442) + - **Line:** 442 - **Context:** `Unknown` ### UR-027 @@ -8215,20 +8245,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/android/mod.rs`](../src-tauri/src/player/android/mod.rs#L617) - **Line:** 617 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L658) - - **Line:** 658 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L691) + - **Line:** 691 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L678) - - **Line:** 678 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L711) + - **Line:** 711 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L747) - - **Line:** 747 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L780) + - **Line:** 780 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L759) - - **Line:** 759 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L792) + - **Line:** 792 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L833) - - **Line:** 833 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L866) + - **Line:** 866 - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L1) - **Line:** 1 @@ -8706,14 +8736,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L124) - - **Line:** 124 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L125) + - **Line:** 125 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L205) - - **Line:** 205 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L206) + - **Line:** 206 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L326) - - **Line:** 326 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L315) + - **Line:** 315 - **Context:** `async function handlePlayAll() {` - **File:** [`src-tauri/src/commands/repository.rs`](../src-tauri/src/commands/repository.rs#L356) - **Line:** 356 @@ -8776,14 +8806,6 @@ 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-067 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3026) - - **Line:** 3026 - - **Context:** `Unknown` - ### UT-072 **Locations:** 1 file(s) @@ -8876,16 +8898,16 @@ 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/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L747) - - **Line:** 747 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L780) + - **Line:** 780 - **Context:** `Unknown` ### UT-084 **Locations:** 1 file(s) -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L759) - - **Line:** 759 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L792) + - **Line:** 792 - **Context:** `Unknown` ### UT-085 @@ -8895,8 +8917,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/player/tapGestures.ts`](../src/lib/components/player/tapGestures.ts#L21) - **Line:** 21 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L778) - - **Line:** 778 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L811) + - **Line:** 811 - **Context:** `Unknown` ### UT-087 @@ -8906,8 +8928,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/player/tapGestures.ts`](../src/lib/components/player/tapGestures.ts#L21) - **Line:** 21 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L833) - - **Line:** 833 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L866) + - **Line:** 866 - **Context:** `Unknown` ### UT-089 @@ -8974,34 +8996,6 @@ 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) @@ -9382,11 +9376,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L289) - - **Line:** 289 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L278) + - **Line:** 278 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L394) - - **Line:** 394 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L383) + - **Line:** 383 - **Context:** `Unknown` - **File:** [`src/lib/components/library/EpisodeFocusView.test.ts`](../src/lib/components/library/EpisodeFocusView.test.ts#L7) - **Line:** 7 @@ -9443,8 +9437,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/stores/connectivity.ts`](../src/lib/stores/connectivity.ts#L250) - **Line:** 250 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L100) - - **Line:** 100 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L101) + - **Line:** 101 - **Context:** `Unknown` - **File:** [`src/routes/library/favorites/+page.svelte`](../src/routes/library/favorites/+page.svelte#L88) - **Line:** 88 @@ -9490,8 +9484,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L250) - - **Line:** 250 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L239) + - **Line:** 239 - **Context:** `Unknown` - **File:** [`src/lib/components/library/EpisodeFocusView.test.ts`](../src/lib/components/library/EpisodeFocusView.test.ts#L7) - **Line:** 7 @@ -9508,14 +9502,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L139) - - **Line:** 139 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L140) + - **Line:** 140 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L233) - - **Line:** 233 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L234) + - **Line:** 234 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L467) - - **Line:** 467 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L456) + - **Line:** 456 - **Context:** `Unknown` - **File:** [`src/routes/search/+page.svelte`](../src/routes/search/+page.svelte#L97) - **Line:** 97 @@ -9868,8 +9862,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1186) - **Line:** 1186 - **Context:** `pub fn run() {` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1278) - - **Line:** 1278 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1279) + - **Line:** 1279 - **Context:** `Unknown` ### UR-079 @@ -10198,8 +10192,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/player/tapGestures.ts`](../src/lib/components/player/tapGestures.ts#L21) - **Line:** 21 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L788) - - **Line:** 788 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L821) + - **Line:** 821 - **Context:** `Unknown` ### UT-088 @@ -10286,51 +10280,20 @@ 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-111 +### UT-109 -**Locations:** 3 file(s) +**Locations:** 1 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 +- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3306) + - **Line:** 3306 - **Context:** `Unknown` -### UT-112 +### UT-110 -**Locations:** 2 file(s) +**Locations:** 1 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 +- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L3362) + - **Line:** 3362 - **Context:** `Unknown` ### UT-116 @@ -10366,17 +10329,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/stream_end.rs`](../src-tauri/src/player/stream_end.rs#L54) - **Line:** 54 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1698) - - **Line:** 1698 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1705) + - **Line:** 1705 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1731) - - **Line:** 1731 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1738) + - **Line:** 1738 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1788) - - **Line:** 1788 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1795) + - **Line:** 1795 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1819) - - **Line:** 1819 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1826) + - **Line:** 1826 - **Context:** `Unknown` ### UT-119 @@ -10405,11 +10368,11 @@ 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-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L564) - - **Line:** 564 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L597) + - **Line:** 597 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L573) - - **Line:** 573 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L606) + - **Line:** 606 - **Context:** `Unknown` ### UT-122 @@ -10818,11 +10781,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L3184) - **Line:** 3184 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L886) - - **Line:** 886 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L893) + - **Line:** 893 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4036) - - **Line:** 4036 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4043) + - **Line:** 4043 - **Context:** `Unknown` ### UT-183 @@ -11124,8 +11087,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/components/library/ArtistDetailView.svelte`](../src/lib/components/library/ArtistDetailView.svelte#L138) - **Line:** 138 - **Context:** `Unknown` -- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L250) - - **Line:** 250 +- **File:** [`src/lib/components/library/EpisodeFocusView.svelte`](../src/lib/components/library/EpisodeFocusView.svelte#L239) + - **Line:** 239 - **Context:** `Unknown` - **File:** [`src/lib/components/library/MediaCard.svelte`](../src/lib/components/library/MediaCard.svelte#L1) - **Line:** 1 @@ -11157,8 +11120,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/lib/stores/favorites.ts`](../src/lib/stores/favorites.ts#L55) - **Line:** 55 - **Context:** `Unknown` -- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L619) - - **Line:** 619 +- **File:** [`src/routes/library/[id]/+page.svelte`](../src/routes/library/[id]/+page.svelte#L608) + - **Line:** 608 - **Context:** `Unknown` ### UR-071 @@ -11309,11 +11272,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/media_server.rs`](../src-tauri/src/media_server.rs#L567) - **Line:** 567 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L886) - - **Line:** 886 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L893) + - **Line:** 893 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4036) - - **Line:** 4036 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4043) + - **Line:** 4043 - **Context:** `Unknown` - **File:** [`src-tauri/src/repository/mod.rs`](../src-tauri/src/repository/mod.rs#L215) - **Line:** 215 @@ -11357,8 +11320,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1126) - **Line:** 1126 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1535) - - **Line:** 1535 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1536) + - **Line:** 1536 - **Context:** `Unknown` - **File:** [`scripts/tauri-security-config.test.ts`](../scripts/tauri-security-config.test.ts#L11) - **Line:** 11 @@ -11586,8 +11549,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L716) - **Line:** 716 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1492) - - **Line:** 1492 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1493) + - **Line:** 1493 - **Context:** `Unknown` ### UT-170 @@ -11752,8 +11715,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L357) - **Line:** 357 - **Context:** `impl LibrarySettings {` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1504) - - **Line:** 1504 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1505) + - **Line:** 1505 - **Context:** `Unknown` ### UT-185 @@ -11917,8 +11880,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1126) - **Line:** 1126 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1348) - - **Line:** 1348 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1349) + - **Line:** 1349 - **Context:** `Unknown` - **File:** [`scripts/tauri-security-config.test.ts`](../scripts/tauri-security-config.test.ts#L11) - **Line:** 11 @@ -12019,11 +11982,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/seek.rs`](../src-tauri/src/player/seek.rs#L135) - **Line:** 135 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L153) - - **Line:** 153 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L160) + - **Line:** 160 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L175) - - **Line:** 175 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L182) + - **Line:** 182 - **Context:** `Unknown` - **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L477) - **Line:** 477 @@ -12095,20 +12058,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/player/android/mod.rs`](../src-tauri/src/player/android/mod.rs#L617) - **Line:** 617 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L658) - - **Line:** 658 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L691) + - **Line:** 691 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L712) - - **Line:** 712 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L745) + - **Line:** 745 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L778) - - **Line:** 778 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L811) + - **Line:** 811 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L788) - - **Line:** 788 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L821) + - **Line:** 821 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L833) - - **Line:** 833 +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L866) + - **Line:** 866 - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](../src-tauri/src/settings.rs#L1) - **Line:** 1 @@ -12276,8 +12239,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/storage/schema.rs`](../src-tauri/src/storage/schema.rs#L769) - **Line:** 769 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1618) - - **Line:** 1618 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1619) + - **Line:** 1619 - **Context:** `Unknown` ### UR-069 @@ -12353,8 +12316,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L4087) - **Line:** 4087 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1625) - - **Line:** 1625 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1626) + - **Line:** 1626 - **Context:** `Unknown` ### UR-072 @@ -12378,32 +12341,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **File:** [`src/routes/settings/+page.svelte`](../src/routes/settings/+page.svelte#L1299) - **Line:** 1299 - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1286) - - **Line:** 1286 - - **Context:** `Unknown` - -### UT-031 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251) - - **Line:** 251 - - **Context:** `Unknown` - -### UT-032 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251) - - **Line:** 251 - - **Context:** `Unknown` - -### UT-033 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251) - - **Line:** 251 +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1287) + - **Line:** 1287 - **Context:** `Unknown` ### UT-115 @@ -12428,6 +12367,234 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **Line:** 1090 - **Context:** `Unknown` +### UT-206 + +**Locations:** 9 file(s) + +- **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L1849) + - **Line:** 1849 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L1868) + - **Line:** 1868 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L2949) + - **Line:** 2949 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L1253) + - **Line:** 1253 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4500) + - **Line:** 4500 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4553) + - **Line:** 4553 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L1210) + - **Line:** 1210 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L3930) + - **Line:** 3930 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L3966) + - **Line:** 3966 + - **Context:** `Unknown` + +### UR-047 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/commands/storage/mod.rs`](../src-tauri/src/commands/storage/mod.rs#L3) + - **Line:** 3 + - **Context:** `Unknown` + +### UT-164 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/download/worker.rs`](../src-tauri/src/download/worker.rs#L313) + - **Line:** 313 + - **Context:** `Unknown` + +### UR-080 + +**Locations:** 20 file(s) + +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L104) + - **Line:** 104 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_backend.rs`](../src-tauri/src/player/mpv_backend.rs#L110) + - **Line:** 110 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L11) + - **Line:** 11 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L60) + - **Line:** 60 + - **Context:** `/ holding a lazily-resolving function pointer. So the address `dlsym` ...` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L160) + - **Line:** 160 + - **Context:** `impl MpvRenderContext {` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L217) + - **Line:** 217 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L229) + - **Line:** 229 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/mpv_render.rs`](../src-tauri/src/player/mpv_render.rs#L273) + - **Line:** 273 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L29) + - **Line:** 29 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L98) + - **Line:** 98 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L124) + - **Line:** 124 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L198) + - **Line:** 198 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L218) + - **Line:** 218 + - **Context:** `pub fn video_audio_codecs(detected: &str) -> String {` +- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L261) + - **Line:** 261 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L307) + - **Line:** 307 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L351) + - **Line:** 351 + - **Context:** `pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> boo...` +- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L630) + - **Line:** 630 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L791) + - **Line:** 791 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L873) + - **Line:** 873 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1239) + - **Line:** 1239 + - **Context:** `Unknown` + +### UT-012 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/sleep_timer.rs`](../src-tauri/src/player/sleep_timer.rs#L81) + - **Line:** 81 + - **Context:** `pub fn cancel(&mut self) {` + +### UT-029 + +**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` + +### UT-031 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251) + - **Line:** 251 + - **Context:** `Unknown` + +### UT-033 + +**Locations:** 1 file(s) + +- **File:** [`src-tauri/src/player/backend.rs`](../src-tauri/src/player/backend.rs#L251) + - **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-130 **Locations:** 3 file(s) @@ -12504,14 +12671,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, **Locations:** 3 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L881) - - **Line:** 881 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L888) + - **Line:** 888 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4099) - - **Line:** 4099 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4106) + - **Line:** 4106 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4142) - - **Line:** 4142 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4149) + - **Line:** 4149 - **Context:** `Unknown` ### UT-156 @@ -12620,82 +12787,82 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, **Locations:** 3 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L953) - - **Line:** 953 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L960) + - **Line:** 960 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4179) - - **Line:** 4179 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4186) + - **Line:** 4186 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4198) - - **Line:** 4198 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4205) + - **Line:** 4205 - **Context:** `Unknown` ### UT-177 **Locations:** 6 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L953) - - **Line:** 953 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L960) + - **Line:** 960 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L968) - - **Line:** 968 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L975) + - **Line:** 975 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L979) - - **Line:** 979 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L986) + - **Line:** 986 - **Context:** `pub fn observed_duration(&self) -> Option {` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4215) - - **Line:** 4215 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4222) + - **Line:** 4222 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4230) - - **Line:** 4230 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4237) + - **Line:** 4237 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4244) - - **Line:** 4244 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4251) + - **Line:** 4251 - **Context:** `Unknown` ### UT-178 **Locations:** 3 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L772) - - **Line:** 772 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L779) + - **Line:** 779 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4266) - - **Line:** 4266 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4273) + - **Line:** 4273 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4293) - - **Line:** 4293 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4300) + - **Line:** 4300 - **Context:** `Unknown` ### UT-179 **Locations:** 4 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L128) - - **Line:** 128 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L135) + - **Line:** 135 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1559) - - **Line:** 1559 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1566) + - **Line:** 1566 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4331) - - **Line:** 4331 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4338) + - **Line:** 4338 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4359) - - **Line:** 4359 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4366) + - **Line:** 4366 - **Context:** `Unknown` ### UT-180 **Locations:** 3 file(s) -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1315) - - **Line:** 1315 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L1322) + - **Line:** 1322 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4388) - - **Line:** 4388 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4395) + - **Line:** 4395 - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4406) - - **Line:** 4406 +- **File:** [`src-tauri/src/player/mod.rs`](../src-tauri/src/player/mod.rs#L4413) + - **Line:** 4413 - **Context:** `Unknown` ### UT-190 @@ -12819,54 +12986,6 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **Line:** 766 - **Context:** `Unknown` -### UT-206 - -**Locations:** 9 file(s) - -- **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L1849) - - **Line:** 1849 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L1868) - - **Line:** 1868 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](../src-tauri/src/commands/player/mod.rs#L2949) - - **Line:** 2949 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L1253) - - **Line:** 1253 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4500) - - **Line:** 4500 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/offline.rs`](../src-tauri/src/repository/offline.rs#L4553) - - **Line:** 4553 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L1210) - - **Line:** 1210 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L3930) - - **Line:** 3930 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L3966) - - **Line:** 3966 - - **Context:** `Unknown` - -### UR-047 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/commands/storage/mod.rs`](../src-tauri/src/commands/storage/mod.rs#L3) - - **Line:** 3 - - **Context:** `Unknown` - -### UT-164 - -**Locations:** 1 file(s) - -- **File:** [`src-tauri/src/download/worker.rs`](../src-tauri/src/download/worker.rs#L313) - - **Line:** 313 - - **Context:** `Unknown` - ### UT-210 **Locations:** 1 file(s) @@ -12897,54 +13016,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-006, JA-007, JA-008, JA-009, JA-010, - **Line:** 257 - **Context:** `Unknown` -### UR-080 - -**Locations:** 14 file(s) - -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L13) - - **Line:** 13 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L87) - - **Line:** 87 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L138) - - **Line:** 138 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L169) - - **Line:** 169 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L198) - - **Line:** 198 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/player/video_surface.rs`](../src-tauri/src/player/video_surface.rs#L222) - - **Line:** 222 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L218) - - **Line:** 218 - - **Context:** `pub fn video_audio_codecs(detected: &str) -> String {` -- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L261) - - **Line:** 261 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L307) - - **Line:** 307 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/device_profile.rs`](../src-tauri/src/repository/device_profile.rs#L351) - - **Line:** 351 - - **Context:** `pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> boo...` -- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L630) - - **Line:** 630 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L791) - - **Line:** 791 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/repository/online.rs`](../src-tauri/src/repository/online.rs#L873) - - **Line:** 873 - - **Context:** `Unknown` -- **File:** [`src-tauri/src/lib.rs`](../src-tauri/src/lib.rs#L1239) - - **Line:** 1239 - - **Context:** `Unknown` - -### IT-016 +### UT-067 **Locations:** 1 file(s) @@ -12952,20 +13024,4 @@ 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-tauri/src/lib.rs b/src-tauri/src/lib.rs index 314a193c..89a8aba7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1242,17 +1242,18 @@ pub fn run() { use tauri::Manager; log::warn!( "[INIT] JELLYTAU_NATIVE_VIDEO=1 — attaching the experimental \ - video surface; the app will abort on the first click until \ - the widget-tree shape is solved (DR-231)" + video surface (mpv drawn behind the webview, no reparenting)" ); if let Some(window) = app.get_webview_window("main") { match window.default_vbox() { - Ok(vbox) => match crate::player::video_surface::attach(&vbox) { - Ok(_surface) => { + Ok(vbox) => { + let handle = crate::player::mpv_backend::registered_handle(); + if crate::player::video_surface::attach(&vbox, handle) { info!("[INIT] Native video surface attached"); + } else { + log::warn!("[INIT] Native video surface unavailable"); } - Err(e) => log::warn!("[INIT] Native video surface unavailable: {e}"), - }, + } Err(e) => { log::warn!("[INIT] No GTK vbox for the main window: {e}") } diff --git a/src-tauri/src/player/mod.rs b/src-tauri/src/player/mod.rs index e0e2806e..f8a60013 100644 --- a/src-tauri/src/player/mod.rs +++ b/src-tauri/src/player/mod.rs @@ -24,11 +24,18 @@ pub mod android; #[cfg(target_os = "linux")] pub mod mpv_backend; +/// mpv's render API into a framebuffer we own (UR-080 / DR-231, IR-033). +/// +/// Deliberately *not* GTK-gated beyond the platform that currently builds it: +/// everything here is the portable half, and Windows reuses it unchanged behind +/// its own surface. +#[cfg(target_os = "linux")] +pub mod mpv_render; + /// The native video surface mpv renders into (UR-080 / DR-231). /// -/// Linux-gated for now because the surface is GTK. Everything *around* it — the -/// render context, its lifetime, frame pacing, the device profile — is -/// deliberately not, so Windows reuses it behind its own surface. +/// Linux-gated because the *surface* is GTK. Everything around it — the render +/// context, its lifetime, frame pacing, the device profile — is not. #[cfg(target_os = "linux")] pub mod video_surface; diff --git a/src-tauri/src/player/mpv_backend.rs b/src-tauri/src/player/mpv_backend.rs index 2137f7ed..873a65ad 100644 --- a/src-tauri/src/player/mpv_backend.rs +++ b/src-tauri/src/player/mpv_backend.rs @@ -89,6 +89,32 @@ fn get_stream_url(media: &MediaItem) -> String { } } +/// The mpv handle of the backend this process created, for the video surface. +/// +/// A `OnceLock` rather than a field reached through `PlayerBackend`, because the +/// trait is cross-platform and a raw mpv pointer is not something every backend +/// should have to pretend to have. Stored as `usize` because a raw pointer is +/// neither `Send` nor `Sync`; the only consumer is the GTK main thread, which is +/// also where mpv was created. +/// +/// Written once at construction and never cleared: the backend outlives the +/// window, so there is no window in which this could dangle while a surface is +/// still using it. +/// +/// TRACES: UR-080 | DR-231 +static MPV_HANDLE: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// The registered handle, or null if no MPV backend was created (initialisation +/// can fail, and the app falls back to a no-op backend rather than dying). +/// +/// TRACES: UR-080 | DR-231 +pub fn registered_handle() -> *mut libmpv_sys::mpv_handle { + MPV_HANDLE + .get() + .map(|p| *p as *mut libmpv_sys::mpv_handle) + .unwrap_or(std::ptr::null_mut()) +} + impl MpvBackend { /// Create a new MPV backend pub fn new( @@ -178,7 +204,14 @@ impl MpvBackend { })); let backend = MpvBackend { - mpv: Arc::new(mpv), + mpv: { + let mpv = Arc::new(mpv); + // Publish the handle for the video surface (DR-231). Ignores a + // second call: only one MPV backend is ever constructed, and a + // failed re-init must not replace a live handle. + let _ = MPV_HANDLE.set(mpv.ctx.as_ptr() as usize); + mpv + }, state, event_emitter, audio_settings: AudioSettings::default(), diff --git a/src-tauri/src/player/mpv_render.rs b/src-tauri/src/player/mpv_render.rs new file mode 100644 index 00000000..9595819b --- /dev/null +++ b/src-tauri/src/player/mpv_render.rs @@ -0,0 +1,374 @@ +//! mpv's render API, driven into an OpenGL framebuffer we own. +//! +//! This is the half of native video that is not GTK: create a render context +//! over the mpv handle the audio backend already drives, render a frame into a +//! texture, and hand that texture id back for the toolkit to composite. +//! +//! Kept apart from `video_surface` deliberately — everything here is portable +//! across the platforms this app targets, while the surface that consumes it is +//! not. Windows reuses this file unchanged (DR-237). +//! +//! TRACES: UR-080 | DR-231, DR-232, IR-033 + +use std::ffi::{c_void, CStr, CString}; +use std::os::raw::{c_char, c_int}; +use std::ptr; + +use log::{error, info, warn}; + +/// GL entry points, resolved once. +/// +/// Only the handful needed to own a framebuffer; mpv resolves everything else +/// it needs through [`get_proc_address`]. +struct Gl { + gen_framebuffers: unsafe extern "C" fn(c_int, *mut u32), + delete_framebuffers: unsafe extern "C" fn(c_int, *const u32), + bind_framebuffer: unsafe extern "C" fn(u32, u32), + framebuffer_texture_2d: unsafe extern "C" fn(u32, u32, u32, u32, c_int), + gen_textures: unsafe extern "C" fn(c_int, *mut u32), + delete_textures: unsafe extern "C" fn(c_int, *const u32), + bind_texture: unsafe extern "C" fn(u32, u32), + tex_image_2d: + unsafe extern "C" fn(u32, c_int, c_int, c_int, c_int, c_int, u32, u32, *const c_void), + tex_parameteri: unsafe extern "C" fn(u32, u32, c_int), + check_framebuffer_status: unsafe extern "C" fn(u32) -> u32, +} + +const GL_TEXTURE_2D: u32 = 0x0DE1; +const GL_FRAMEBUFFER: u32 = 0x8D40; +const GL_COLOR_ATTACHMENT0: u32 = 0x8CE0; +const GL_RGBA: u32 = 0x1908; +const GL_RGBA8: c_int = 0x8058; +const GL_UNSIGNED_BYTE: u32 = 0x1401; +const GL_LINEAR: c_int = 0x2601; +const GL_TEXTURE_MIN_FILTER: u32 = 0x2801; +const GL_TEXTURE_MAG_FILTER: u32 = 0x2800; +const GL_FRAMEBUFFER_COMPLETE: u32 = 0x8CD5; + +/// Resolve a GL symbol the way libepoxy actually exports it. +/// +/// **This is the trap that cost the spike a debugging cycle.** libepoxy does not +/// export `glFoo` as a function. It exports `epoxy_glFoo` as a *data* symbol +/// holding a lazily-resolving function pointer. So the address `dlsym` returns +/// is the address *of the pointer*, not of any code: returning it makes mpv jump +/// into non-executable data and take SIGSEGV/SEGV_ACCERR on the very first GL +/// call. The value must be read *out of* that location. +/// +/// The `epoxy` crate does this correctly and is unusable here — its +/// `gl_generator` dependency pulls a yanked `xml-rs`. +/// +/// TRACES: UR-080 | IR-033 +unsafe fn resolve(name: &str) -> *mut c_void { + let epoxy_name = match CString::new(format!("epoxy_{name}")) { + Ok(n) => n, + Err(_) => return ptr::null_mut(), + }; + let slot = libc::dlsym(libc::RTLD_DEFAULT, epoxy_name.as_ptr()); + if !slot.is_null() { + // The symbol holds the function pointer; return what is stored there. + return *(slot as *mut *mut c_void); + } + + // Fall back to a plain symbol, for a GL stack that is not behind epoxy. + match CString::new(name) { + Ok(n) => libc::dlsym(libc::RTLD_DEFAULT, n.as_ptr()), + Err(_) => ptr::null_mut(), + } +} + +/// What mpv calls to find GL entry points. Same rule as [`resolve`]. +unsafe extern "C" fn get_proc_address(_ctx: *mut c_void, name: *const c_char) -> *mut c_void { + if name.is_null() { + return ptr::null_mut(); + } + match CStr::from_ptr(name).to_str() { + Ok(n) => resolve(n), + Err(_) => ptr::null_mut(), + } +} + +macro_rules! load { + ($name:literal) => {{ + let p = resolve($name); + if p.is_null() { + error!("[MpvRender] GL symbol not found: {}", $name); + return None; + } + std::mem::transmute(p) + }}; +} + +impl Gl { + /// Resolve every entry point, or none — a partially-loaded table would fail + /// later at a call site with no context. + /// + /// The transmutes are unannotated on purpose: each target type is declared + /// once on the struct field above, and repeating it at the call site would + /// be two places to get the same signature wrong. + #[allow(clippy::missing_transmute_annotations)] + unsafe fn load() -> Option { + Some(Gl { + gen_framebuffers: load!("glGenFramebuffers"), + delete_framebuffers: load!("glDeleteFramebuffers"), + bind_framebuffer: load!("glBindFramebuffer"), + framebuffer_texture_2d: load!("glFramebufferTexture2D"), + gen_textures: load!("glGenTextures"), + delete_textures: load!("glDeleteTextures"), + bind_texture: load!("glBindTexture"), + tex_image_2d: load!("glTexImage2D"), + tex_parameteri: load!("glTexParameteri"), + check_framebuffer_status: load!("glCheckFramebufferStatus"), + }) + } +} + +/// A colour-renderable framebuffer mpv draws into, sized to the widget. +struct Target { + fbo: u32, + texture: u32, + width: i32, + height: i32, +} + +/// mpv's render context plus the framebuffer it draws into. +/// +/// # Lifetime (DR-232) +/// +/// The render context must not outlive the GL context it was created against. +/// `Drop` unregisters mpv's update callback *before* freeing the context, so a +/// callback cannot land on a freed pointer, and frees the GL objects while the +/// caller still has the context current. The caller is responsible for making +/// the GL context current around both creation and drop — see `video_surface`. +/// +/// This is DR-184 on Android restated: a surface outliving its player. The spike +/// had no defence at all and saw one unexplained SIGSEGV in a decoder thread. +pub struct MpvRenderContext { + ctx: *mut libmpv_sys::mpv_render_context, + gl: Gl, + target: Option, +} + +// The render context is driven only from the GTK main thread; the update +// callback merely schedules a redraw and touches nothing here. +unsafe impl Send for MpvRenderContext {} + +impl MpvRenderContext { + /// Create a render context over an existing mpv handle. + /// + /// The GL context must already be current on this thread. + /// + /// TRACES: UR-080 | DR-231, IR-033 + pub unsafe fn new(mpv: *mut libmpv_sys::mpv_handle) -> Option { + let gl = Gl::load()?; + + let mut init = libmpv_sys::mpv_opengl_init_params { + get_proc_address: Some(get_proc_address), + get_proc_address_ctx: ptr::null_mut(), + }; + let mut api_type = CString::new("opengl").ok()?; + // `advanced_control` lets mpv tell us when a frame is ready instead of + // us guessing; it is what makes the update callback meaningful (DR-233). + let mut advanced: c_int = 1; + + let mut params = [ + libmpv_sys::mpv_render_param { + type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_API_TYPE, + data: api_type.as_ptr() as *mut c_void, + }, + libmpv_sys::mpv_render_param { + type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, + data: &mut init as *mut _ as *mut c_void, + }, + libmpv_sys::mpv_render_param { + type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_ADVANCED_CONTROL, + data: &mut advanced as *mut _ as *mut c_void, + }, + libmpv_sys::mpv_render_param { + type_: 0, + data: ptr::null_mut(), + }, + ]; + + let mut ctx: *mut libmpv_sys::mpv_render_context = ptr::null_mut(); + let rc = libmpv_sys::mpv_render_context_create(&mut ctx, mpv, params.as_mut_ptr()); + // Keep the CString alive until after the call. + let _ = &mut api_type; + + if rc < 0 || ctx.is_null() { + error!("[MpvRender] mpv_render_context_create failed: {rc}"); + return None; + } + + info!("[MpvRender] render context created"); + Some(MpvRenderContext { + ctx, + gl, + target: None, + }) + } + + /// Ask to be told when a new frame is ready. + /// + /// Paired with [`report_swap`](Self::report_swap): without both, mpv has + /// nothing to time against. The symptom is misleading — playback looks fine + /// in a window and judders at fullscreen, which reads as a compositing or + /// GPU limit and is neither (DR-233). + /// + /// TRACES: UR-080 | DR-233 + pub unsafe fn set_update_callback( + &mut self, + callback: libmpv_sys::mpv_render_update_fn, + ctx: *mut c_void, + ) { + libmpv_sys::mpv_render_context_set_update_callback(self.ctx, callback, ctx); + } + + /// Render the current frame at `width` x `height`, returning the texture id + /// holding it. The GL context must be current. + /// + /// TRACES: UR-080 | DR-231 + pub unsafe fn render(&mut self, width: i32, height: i32) -> Option { + if width <= 0 || height <= 0 { + return None; + } + self.ensure_target(width, height)?; + let target = self.target.as_ref()?; + + let mut fbo = libmpv_sys::mpv_opengl_fbo { + fbo: target.fbo as c_int, + w: width as c_int, + h: height as c_int, + internal_format: 0, + }; + // GTK's cairo surface has its origin at the top left; mpv defaults to + // OpenGL's bottom-left. Without this the picture is drawn upside down — + // which looks like a broken decode rather than a coordinate convention. + let mut flip: c_int = 1; + + let mut params = [ + libmpv_sys::mpv_render_param { + type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_OPENGL_FBO, + data: &mut fbo as *mut _ as *mut c_void, + }, + libmpv_sys::mpv_render_param { + type_: libmpv_sys::mpv_render_param_type_MPV_RENDER_PARAM_FLIP_Y, + data: &mut flip as *mut _ as *mut c_void, + }, + libmpv_sys::mpv_render_param { + type_: 0, + data: ptr::null_mut(), + }, + ]; + + let rc = libmpv_sys::mpv_render_context_render(self.ctx, params.as_mut_ptr()); + if rc < 0 { + warn!("[MpvRender] render failed: {rc}"); + return None; + } + Some(target.texture) + } + + /// Tell mpv the frame reached the screen. See [`set_update_callback`]. + /// + /// TRACES: UR-080 | DR-233 + pub unsafe fn report_swap(&self) { + libmpv_sys::mpv_render_context_report_swap(self.ctx); + } + + /// Create or resize the framebuffer. Reused across frames — reallocating per + /// frame would churn GPU memory at the display rate. + unsafe fn ensure_target(&mut self, width: i32, height: i32) -> Option<()> { + if let Some(t) = &self.target { + if t.width == width && t.height == height { + return Some(()); + } + } + self.drop_target(); + + let gl = &self.gl; + let mut texture: u32 = 0; + (gl.gen_textures)(1, &mut texture); + (gl.bind_texture)(GL_TEXTURE_2D, texture); + (gl.tex_image_2d)( + GL_TEXTURE_2D, + 0, + GL_RGBA8, + width, + height, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + ptr::null(), + ); + (gl.tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + (gl.tex_parameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + (gl.bind_texture)(GL_TEXTURE_2D, 0); + + let mut fbo: u32 = 0; + (gl.gen_framebuffers)(1, &mut fbo); + (gl.bind_framebuffer)(GL_FRAMEBUFFER, fbo); + (gl.framebuffer_texture_2d)( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + texture, + 0, + ); + let status = (gl.check_framebuffer_status)(GL_FRAMEBUFFER); + (gl.bind_framebuffer)(GL_FRAMEBUFFER, 0); + + if status != GL_FRAMEBUFFER_COMPLETE { + error!("[MpvRender] framebuffer incomplete: 0x{status:x}"); + (gl.delete_framebuffers)(1, &fbo); + (gl.delete_textures)(1, &texture); + return None; + } + + self.target = Some(Target { + fbo, + texture, + width, + height, + }); + Some(()) + } + + unsafe fn drop_target(&mut self) { + if let Some(t) = self.target.take() { + (self.gl.delete_framebuffers)(1, &t.fbo); + (self.gl.delete_textures)(1, &t.texture); + } + } + + /// Free everything, with the GL context current. + /// + /// Explicit rather than left to `Drop` because the ordering matters and the + /// caller is the only one that can guarantee the GL context is current. See + /// DR-232. + pub unsafe fn destroy(mut self) { + // Unregister first: a callback arriving after the free would be a use + // after free, and it is scheduled from mpv's own threads. + libmpv_sys::mpv_render_context_set_update_callback(self.ctx, None, ptr::null_mut()); + self.drop_target(); + libmpv_sys::mpv_render_context_free(self.ctx); + self.ctx = ptr::null_mut(); + info!("[MpvRender] render context freed"); + std::mem::forget(self); + } +} + +impl Drop for MpvRenderContext { + fn drop(&mut self) { + if !self.ctx.is_null() { + // Reached only if `destroy` was not called — the GL context may not + // be current, so the GL objects are deliberately leaked rather than + // deleted against whatever context happens to be bound. Freeing the + // render context is still safe and is the part that matters. + warn!("[MpvRender] dropped without destroy(); GL objects leaked deliberately"); + unsafe { + libmpv_sys::mpv_render_context_set_update_callback(self.ctx, None, ptr::null_mut()); + libmpv_sys::mpv_render_context_free(self.ctx); + } + } + } +} diff --git a/src-tauri/src/player/video_surface.rs b/src-tauri/src/player/video_surface.rs index c96c3106..5017884c 100644 --- a/src-tauri/src/player/video_surface.rs +++ b/src-tauri/src/player/video_surface.rs @@ -1,232 +1,283 @@ -//! The native video surface: a GL area beneath Tauri's own webview. +//! The native video surface: mpv drawn *behind* Tauri's webview, without +//! touching the widget tree. //! -//! This is the desktop counterpart of the Android arrangement — a native -//! renderer at the bottom of the stack with a transparent webview drawn over it, -//! so the Svelte controls composite on top of moving video. +//! # Why there is no overlay here //! -//! The spike that authorised this built its *own* `GtkOverlay` and proved mpv -//! renders into it on X11 and Wayland. What it could not prove is the step this -//! module exists for: taking the overlay Tauri already built and reparenting the -//! 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. +//! The obvious arrangement — wrap the webview in a `GtkOverlay` with a +//! `GtkGLArea` beneath — 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: //! -//! TRACES: UR-080 | DR-231, IR-033 +//! ```text +//! 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 because the panic is non-unwinding it takes the app with it. +//! Nothing in configuration avoids it: on Linux the handler is attached +//! *unconditionally* (the Windows path guards it behind `is_decorated()`), and +//! the decoration check that would make it inert runs *after* the unwrap. +//! +//! So the widget tree is left exactly as Tauri built it. GTK draws a container +//! before its children, so rendering into the vbox's own `draw` handler puts the +//! picture underneath the webview for free — the same z-order, no reparenting, +//! one less widget, and nothing a Tauri upgrade can invalidate by assuming its +//! own layout. +//! +//! TRACES: UR-080 | DR-231, DR-232, DR-233, IR-033 + +use std::cell::RefCell; +use std::ffi::c_void; +use std::rc::Rc; use gtk::prelude::*; -use log::{info, warn}; +use gtk::{gdk, glib}; +use log::{error, info, warn}; -/// 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 -/// area realizes and must be freed before it unrealizes — DR-232. +use super::mpv_render::MpvRenderContext; + +/// GL enum for `gdk_cairo_draw_from_gl`'s `source_type`. GDK takes the GL +/// constant itself rather than an enum of its own. +const GL_TEXTURE: i32 = 0x1702; + +/// Everything the draw handler needs, shared with the GTK callbacks. +struct SurfaceState { + gl: Option, + render: Option, + mpv: *mut libmpv_sys::mpv_handle, + /// The widget to repaint when mpv reports a frame. Held here because the + /// update callback arrives on an mpv thread with nothing but this state. + widget: gtk::Box, +} + +impl SurfaceState { + /// Tear down in the order DR-232 requires, with the GL context current. + /// + /// The update callback is unregistered before the context is freed (inside + /// `destroy`), and the GL objects go while their context is still bound. + /// Getting this wrong is DR-184 on Android restated — a surface outliving + /// its player — and is the likeliest cause of the one unexplained SIGSEGV + /// the spike recorded. + fn teardown(&mut self) { + if let Some(render) = self.render.take() { + if let Some(gl) = &self.gl { + gl.make_current(); + } + unsafe { render.destroy() }; + } + self.gl = None; + } +} + +/// A live video surface. Dropping it tears the render context down. pub struct VideoSurface { - /// The GL area mpv renders into. Main child of the overlay, so it sits - /// *under* everything else. - #[allow(dead_code)] - gl_area: gtk::GLArea, - /// The overlay holding the GL area and the webview. - #[allow(dead_code)] - overlay: gtk::Overlay, + state: Rc>, + widget: gtk::Box, + handlers: Vec, } -impl VideoSurface { - // Consumed by the render context, which binds to the GL area on `realize` - // and is freed on `unrealize` (DR-232). Held here from the moment the - // surface exists so that binding has something to attach to. - #[allow(dead_code)] - /// The GL area, for the render context to bind to. - pub fn gl_area(&self) -> >k::GLArea { - &self.gl_area - } - - #[allow(dead_code)] - /// The overlay, for teardown. - pub fn overlay(&self) -> >k::Overlay { - &self.overlay +impl Drop for VideoSurface { + fn drop(&mut self) { + for id in self.handlers.drain(..) { + self.widget.disconnect(id); + } + self.state.borrow_mut().teardown(); + self.widget.queue_draw(); + info!("[VideoSurface] detached"); } } -/// Why a surface could not be attached. +/// mpv's update callback. Runs on an mpv thread, so it does the least possible: +/// flags the state and asks GTK to redraw on the main loop. /// -/// One variant, because there is exactly one way this fails that is not already -/// reported by Tauri itself: the window exists and has a vbox, but the vbox is -/// not shaped the way Tauri has always shaped it. -#[derive(Debug)] -pub enum SurfaceError { - /// The vbox held no webview to reparent — Tauri's layout has changed. - NoWebviewChild, +/// **Nothing here may block or re-enter the player.** The project's deadlock +/// gotcha applies with full force — this is called from mpv's own threads. +/// +/// TRACES: UR-080 | DR-233 +unsafe extern "C" fn on_mpv_update(ctx: *mut c_void) { + if ctx.is_null() { + return; + } + let state = &*(ctx as *const Rc>); + let state = state.clone(); + // Hop to the main loop: GTK is not thread-safe and mpv is not on its thread. + // + // Asking for a repaint is the entire job. Without it a decoded frame simply + // waits — the picture would then only advance when something else happened + // to invalidate the widget, which looks like severe stutter rather than like + // a missing call. + glib::idle_add_local_once(move || { + let Ok(s) = state.try_borrow() else { return }; + s.widget.queue_draw(); + }); } -impl std::fmt::Display for SurfaceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SurfaceError::NoWebviewChild => write!( - f, - "Tauri's default vbox had no child to reparent — its window layout has changed" - ), +/// Start drawing mpv's video underneath the webview. +/// +/// `vbox` is Tauri's `default_vbox()` — the container the webview already lives +/// in. It is not modified; only a `draw` handler is added. +/// +/// Must run on the GTK main thread. +/// +/// TRACES: UR-080 | DR-231, DR-232, DR-233 +pub fn attach(vbox: >k::Box, mpv: *mut libmpv_sys::mpv_handle) -> bool { + if mpv.is_null() { + warn!("[VideoSurface] no mpv handle; native video unavailable"); + return false; + } + + let state = Rc::new(RefCell::new(SurfaceState { + gl: None, + render: None, + mpv, + widget: vbox.clone(), + })); + + let mut handlers = Vec::new(); + + // The GL context can only be created once the widget has a GdkWindow, which + // is what `realize` announces. Creating it earlier leaves nothing to attach + // to — the same ordering constraint the render context has. + let realize_state = state.clone(); + handlers.push(vbox.connect_realize(move |widget| { + if let Err(e) = init_gl(widget, &realize_state) { + error!("[VideoSurface] GL init failed: {e}"); + } + })); + + // A render context outliving its GL context is the defect DR-232 exists to + // prevent, so teardown is bound to `unrealize` rather than left to Drop. + let unrealize_state = state.clone(); + handlers.push(vbox.connect_unrealize(move |_| { + unrealize_state.borrow_mut().teardown(); + })); + + let draw_state = state.clone(); + handlers.push(vbox.connect_draw(move |widget, cr| { + draw(widget, cr, &draw_state); + // Propagate: the webview is a child and must still draw over us. + glib::Propagation::Proceed + })); + + // The window is already up by the time we are called, so run the init the + // `realize` signal would have. + if vbox.is_realized() { + if let Err(e) = init_gl(vbox, &state) { + error!("[VideoSurface] GL init failed: {e}"); } } + + info!("[VideoSurface] attached to Tauri's vbox without reparenting"); + // The surface lives as long as the window. Held in a thread-local rather + // than returned, because it owns `Rc` and GTK types and so is neither `Send` + // nor `Sync` — it cannot go into Tauri's managed state, and leaking it would + // give up the ability to tear it down at all. + // + // Teardown does not depend on this being dropped: it is driven by the + // widget's `unrealize`, which is the signal that actually means "your GL + // context is going away" (DR-232). + LIVE_SURFACE.with(|cell| { + *cell.borrow_mut() = Some(VideoSurface { + state, + widget: vbox.clone(), + handlers, + }); + }); + true } -impl std::error::Error for SurfaceError {} - -/// Build the overlay and move Tauri's webview on top of it. -/// -/// Tauri's Linux window is an `ApplicationWindow` holding a single vertical -/// `gtk::Box` (`default_vbox`), with the webview packed into it. This takes that -/// webview out, puts a `GtkGLArea` in its place inside a `GtkOverlay`, and adds -/// the webview back as the *overlay* child so it draws above. -/// -/// **Must run on the GTK main thread.** Every GTK call here is main-thread-only, -/// and the caller reaches it via `run_on_main_thread`. -/// -/// Ordering matters: the GL area is added as the overlay's main child *before* -/// the webview goes back, because `GtkOverlay` treats its first `add` as the -/// 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. -/// -/// TRACES: UR-080 | DR-231 -pub fn attach(vbox: >k::Box) -> Result { - // 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 - // all this needs is "whatever Tauri put here". - let children = vbox.children(); - let webview = children - .into_iter() - .next() - .ok_or(SurfaceError::NoWebviewChild)?; - - let gl_area = gtk::GLArea::new(); - // No depth buffer: mpv draws a flat picture into an FBO and nothing here is - // 3D. Asking for one costs memory on every resize for nothing. - gl_area.set_has_depth_buffer(false); - gl_area.set_has_stencil_buffer(false); - // Fill the overlay rather than centring at intrinsic size — the same defect - // `videoFitClass` had to fix on the webview side, where `max-w-full` only - // ever shrank and a 480p source rendered as a small box on a black screen. - gl_area.set_hexpand(true); - gl_area.set_vexpand(true); - - let overlay = gtk::Overlay::new(); - - // Reparent. `remove` drops the container's reference, so hold one across the - // move or the widget is destroyed between the two calls. - let webview_ref = webview.clone(); - vbox.remove(&webview); - - overlay.add(&gl_area); // main child — the bottom of the stack - overlay.add_overlay(&webview_ref); // drawn above the video - - // The webview must keep receiving input: it *is* the UI. `GtkOverlay` passes - // events to overlay children by default, so pass-through stays off — setting - // it would send clicks to the GL area, which has no controls on it. - overlay.set_overlay_pass_through(&webview_ref, false); - - vbox.pack_start(&overlay, true, true, 0); - overlay.show_all(); - - info!("[VideoSurface] GL area attached beneath Tauri's webview"); - - Ok(VideoSurface { gl_area, overlay }) +thread_local! { + /// The one live surface, on the GTK main thread. + static LIVE_SURFACE: RefCell> = const { RefCell::new(None) }; } -/// Put Tauri's window back the way it was found. +/// Drop the live surface, if there is one. Idempotent. /// -/// Not merely tidiness: the webview outlives the video surface, so if the -/// surface is torn down without returning the webview to the vbox the UI -/// disappears while the app keeps running. Mirrors [`attach`] exactly. -/// -/// TRACES: UR-080 | DR-231, DR-232 -// 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 -// reparent whose inverse is written wrong. +/// TRACES: UR-080 | DR-232 #[allow(dead_code)] -pub fn detach(vbox: >k::Box, surface: &VideoSurface) { - let children = surface.overlay.children(); - for child in children { - // Everything except the GL area came from the vbox and goes back to it. - if child.downcast_ref::().is_some() { - continue; - } - surface.overlay.remove(&child); - vbox.pack_start(&child, true, true, 0); - } - vbox.remove(&surface.overlay); - vbox.show_all(); - warn!("[VideoSurface] detached; webview returned to Tauri's vbox"); +pub fn detach() { + LIVE_SURFACE.with(|cell| { + cell.borrow_mut().take(); + }); } -#[cfg(test)] -mod tests { - //! These exercise GTK widget wiring, so they need a display and are ignored - //! by default — CI has no X11 or Wayland session. Run locally with - //! `cargo test -- --ignored video_surface`. +/// Create the GL context and the mpv render context over it. +fn init_gl(widget: >k::Box, state: &Rc>) -> Result<(), String> { + if state.borrow().render.is_some() { + return Ok(()); + } + let window = widget.window().ok_or("widget has no GdkWindow")?; - use super::*; + let gl = window + .create_gl_context() + .map_err(|e| format!("create_gl_context: {e}"))?; + gl.realize().map_err(|e| format!("realize: {e}"))?; + gl.make_current(); - /// The stacking order is the whole point, and getting it backwards produces - /// video painted over the controls rather than under them. - /// - /// TRACES: UR-080 | DR-231 - #[test] - #[ignore = "requires a display"] - fn test_gl_area_is_below_the_reparented_webview() { - if gtk::init().is_err() { - return; - } - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - // Stand in for the webview; `attach` deliberately does not care what it is. - let stand_in = gtk::DrawingArea::new(); - vbox.pack_start(&stand_in, true, true, 0); + let mpv = state.borrow().mpv; + let mut render = + unsafe { MpvRenderContext::new(mpv) }.ok_or("mpv render context creation failed")?; - let surface = attach(&vbox).expect("attaches"); - let children = surface.overlay().children(); + // The callback holds a pointer to the shared state, so that Rc must outlive + // it. `destroy` unregisters before freeing, which is what makes it safe. + let ctx_ptr = Rc::into_raw(state.clone()) as *mut c_void; + unsafe { render.set_update_callback(Some(on_mpv_update), ctx_ptr) }; - // GtkOverlay lists its main child first. - assert!( - children[0].downcast_ref::().is_some(), - "the GL area must be the overlay's main child, i.e. underneath" - ); - assert!( - children.len() > 1, - "the reparented widget must still be present" - ); + let mut s = state.borrow_mut(); + s.gl = Some(gl); + s.render = Some(render); + info!("[VideoSurface] GL and render context ready"); + Ok(()) +} + +/// Draw the current frame, if there is one. +/// +/// Runs *before* the children, which is what puts the picture behind the +/// webview. Deliberately forgiving: no frame, no GL, or a borrowed state all +/// mean "draw nothing this pass" rather than an error — the webview then paints +/// over an untouched background, which is exactly the pre-native appearance. +fn draw(widget: >k::Box, cr: >k::cairo::Context, state: &Rc>) { + let Ok(mut s) = state.try_borrow_mut() else { + return; + }; + let Some(gl) = s.gl.clone() else { + return; + }; + let Some(window) = widget.window() else { + return; + }; + + let scale = widget.scale_factor(); + let width = widget.allocated_width() * scale; + let height = widget.allocated_height() * scale; + if width <= 0 || height <= 0 { + return; } - /// A surface that tears down without returning the webview leaves a running - /// app with no UI. - /// - /// TRACES: UR-080 | DR-231, DR-232 - #[test] - #[ignore = "requires a display"] - fn test_detach_returns_the_webview_to_the_vbox() { - if gtk::init().is_err() { - return; - } - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - let stand_in = gtk::DrawingArea::new(); - vbox.pack_start(&stand_in, true, true, 0); + gl.make_current(); - let surface = attach(&vbox).expect("attaches"); - detach(&vbox, &surface); + let Some(render) = s.render.as_mut() else { + return; + }; + let Some(texture) = (unsafe { render.render(width, height) }) else { + return; + }; - let children = vbox.children(); - assert_eq!(children.len(), 1, "exactly the original child comes back"); - assert!( - children[0].downcast_ref::().is_some(), - "and it is the webview stand-in, not the overlay" + unsafe { + cr.draw_from_gl( + &window, + texture as i32, + GL_TEXTURE, + scale, + 0, + 0, + width, + height, ); - } - - /// A vbox Tauri has not populated is a changed assumption, not a panic. - /// - /// TRACES: UR-080 | DR-231 - #[test] - #[ignore = "requires a display"] - fn test_an_empty_vbox_is_an_error_not_a_panic() { - if gtk::init().is_err() { - return; - } - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - assert!(matches!(attach(&vbox), Err(SurfaceError::NoWebviewChild))); + // Tell mpv the frame reached the screen. Without this it has nothing to + // pace against — see DR-233. + render.report_swap(); } } diff --git a/src/lib/components/library/EpisodeFocusView.svelte b/src/lib/components/library/EpisodeFocusView.svelte index 509f97b7..c9ba767e 100644 --- a/src/lib/components/library/EpisodeFocusView.svelte +++ b/src/lib/components/library/EpisodeFocusView.svelte @@ -8,6 +8,7 @@ --> diff --git a/src/lib/components/player/Queue.svelte b/src/lib/components/player/Queue.svelte index f7a44fb5..1be5335d 100644 --- a/src/lib/components/player/Queue.svelte +++ b/src/lib/components/player/Queue.svelte @@ -1,5 +1,6 @@