fix(playback): advertise only webview-decodable audio for video (DR-148, 0.4.7)
The audio codec list sent to Jellyfin comes from MediaCodecList, which describes ExoPlayer — but video does not play through ExoPlayer. Android force-renders every video in the webview <video> element (the interim override in VideoPlayer.svelte) and Linux always has, and Chromium/WebKit decode a far narrower set than the platform does. A motorola edge 30 ships /vendor/etc/media_codecs_dolby_audio.xml, so it reported ac3,eac3; the server direct-played an E-AC-3 track with static=true and the webview built a video decoder and no audio decoder at all — full picture, no sound. The defect is triggered by capability rather than the lack of it, which is why a Fairphone and an Honor tablet play the same file on the same build: without the Dolby decoder they never claim the codec, so the server transcodes to AAC. Confirmed by A/B on the failing device — hevc+eac3 silent, hevc+aac audible, same session, same profile, same direct-play path, audio codec the only variable. video_audio_codecs narrows the platform list to the webview-decodable set for the video direct-play profile only. Audio-only playback really is the native player's, so that profile keeps the full list rather than transcoding music that plays perfectly well. A list with nothing decodable still claims aac, since a profile claiming nothing invites the server to give up instead of transcoding. The video codec list is deliberately untouched: HEVC direct-plays through the webview correctly, so the constraint is specific to audio. Test-first: the tests failed against the old behaviour before the filter existed, including the case built from the phone's real codec list. The requirement-count assertion in extract-traces.test.ts moves 280 -> 281 for the added DR, which is the deliberate edit that test exists to force. Not yet verified on device — the 0.4.7 APK was still building.
This commit is contained in:
@@ -6,6 +6,22 @@ Entries are grouped by the capability they change, not by commit. Requirement
|
|||||||
IDs in parentheses point at [docs/requirements.md](docs/requirements.md); the
|
IDs in parentheses point at [docs/requirements.md](docs/requirements.md); the
|
||||||
generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
|
||||||
|
|
||||||
|
## v0.4.7
|
||||||
|
|
||||||
|
### 🐛 Fixes
|
||||||
|
|
||||||
|
- **Video plays with sound on devices that ship a Dolby decoder.** The audio
|
||||||
|
codec list sent to Jellyfin came from `MediaCodecList`, which describes
|
||||||
|
ExoPlayer — but video does not play through ExoPlayer: it renders in the
|
||||||
|
webview `<video>` element, which decodes far less. A phone whose vendor
|
||||||
|
licenses Dolby therefore advertised `ac3`/`eac3`, got a direct play, and
|
||||||
|
showed full picture with no audio, while a leaner device claimed neither
|
||||||
|
codec, received an AAC transcode, and played the same file correctly. The
|
||||||
|
video direct-play profile is now narrowed to what the webview can decode;
|
||||||
|
audio-only playback is genuinely the native player's and keeps the full list,
|
||||||
|
so music is not transcoded needlessly.
|
||||||
|
(UR-004 → DR-148)
|
||||||
|
|
||||||
## v0.4.1
|
## v0.4.1
|
||||||
|
|
||||||
### 🐛 Fixes
|
### 🐛 Fixes
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ Internal architecture, components, and application logic.
|
|||||||
| DR-141 | The device profile states how many channels the audio route can actually voice. `MediaCodecList` answers "can this device *decode* 5.1", which is not the question that decides whether the user hears anything — a phone decodes an AC-3 5.1 track happily and still has two channels to play it out of. With no `MaxAudioChannels` in the profile, Jellyfin was free to direct-play the multichannel track, and the result is device dependent: a failed `AudioSink` configuration (silence) or dialogue folded into surround channels that go nowhere. media3's `AudioCapabilities.maxChannelCount` for the current route is reported over JNI alongside the codec lists, and bounds both the direct-play profile and the transcoding profiles, so the server downmixes rather than shipping channels the sink cannot take. Codecs are never removed from the profile — a device with genuine surround output keeps direct-playing it. A missing or zero reading means "route not yet established", not "no audio", and falls back to stereo, the one capability every sink has | Playback | UR-004 | Done |
|
| DR-141 | The device profile states how many channels the audio route can actually voice. `MediaCodecList` answers "can this device *decode* 5.1", which is not the question that decides whether the user hears anything — a phone decodes an AC-3 5.1 track happily and still has two channels to play it out of. With no `MaxAudioChannels` in the profile, Jellyfin was free to direct-play the multichannel track, and the result is device dependent: a failed `AudioSink` configuration (silence) or dialogue folded into surround channels that go nowhere. media3's `AudioCapabilities.maxChannelCount` for the current route is reported over JNI alongside the codec lists, and bounds both the direct-play profile and the transcoding profiles, so the server downmixes rather than shipping channels the sink cannot take. Codecs are never removed from the profile — a device with genuine surround output keeps direct-playing it. A missing or zero reading means "route not yet established", not "no audio", and falls back to stereo, the one capability every sink has | Playback | UR-004 | Done |
|
||||||
| DR-145 | Video playback starts only once the app actually holds audio focus. Video manages focus by hand (`handleAudioFocus=false`, because ExoPlayer's automatic handling is reserved for the audio path), and the request's three outcomes were all treated as success: `AUDIOFOCUS_REQUEST_DELAYED` — which `setAcceptsDelayedFocusGain(true)` explicitly invites, and which means the system is *withholding our audio* until it calls back — and an outright `REQUEST_FAILED` were logged and then followed by `playWhenReady = true`. The picture rolled with no sound, indistinguishable to the user from a broken stream. Playback is now held when focus is not granted and started from the `AUDIOFOCUS_GAIN` callback; an explicit `play()` re-requests focus rather than resuming into a stream the system is still muting, guarded by a held-focus flag so repeated plays do not leak focus requests. A `LOSS` clears the pending flag, so an unrelated later `GAIN` cannot start playback the user never asked for | Playback | UR-004 | Done |
|
| DR-145 | Video playback starts only once the app actually holds audio focus. Video manages focus by hand (`handleAudioFocus=false`, because ExoPlayer's automatic handling is reserved for the audio path), and the request's three outcomes were all treated as success: `AUDIOFOCUS_REQUEST_DELAYED` — which `setAcceptsDelayedFocusGain(true)` explicitly invites, and which means the system is *withholding our audio* until it calls back — and an outright `REQUEST_FAILED` were logged and then followed by `playWhenReady = true`. The picture rolled with no sound, indistinguishable to the user from a broken stream. Playback is now held when focus is not granted and started from the `AUDIOFOCUS_GAIN` callback; an explicit `play()` re-requests focus rather than resuming into a stream the system is still muting, guarded by a held-focus flag so repeated plays do not leak focus requests. A `LOSS` clears the pending flag, so an unrelated later `GAIN` cannot start playback the user never asked for | Playback | UR-004 | Done |
|
||||||
| DR-146 | The no-audio-track fallback picks a track the renderer can actually play. When ExoPlayer selected no audio track, the recovery forced group 0 / track 0 unconditionally — but the most likely reason nothing was selected is that this very track cannot be decoded on this device, so the override reinstated the silence it was meant to fix. It now scans the groups for the first `isTrackSupported` track and overrides to that, and clears `setTrackTypeDisabled(TRACK_TYPE_AUDIO)` because audio may equally have been off at the type level, which an override alone does not undo. When no group holds a supported track the condition is logged as an error — the server was expected to transcode — rather than leaving a silent video with no explanation in the log | Playback | UR-004 | Done |
|
| DR-146 | The no-audio-track fallback picks a track the renderer can actually play. When ExoPlayer selected no audio track, the recovery forced group 0 / track 0 unconditionally — but the most likely reason nothing was selected is that this very track cannot be decoded on this device, so the override reinstated the silence it was meant to fix. It now scans the groups for the first `isTrackSupported` track and overrides to that, and clears `setTrackTypeDisabled(TRACK_TYPE_AUDIO)` because audio may equally have been off at the type level, which an override alone does not undo. When no group holds a supported track the condition is logged as an error — the server was expected to transcode — rather than leaving a silent video with no explanation in the log | Playback | UR-004 | Done |
|
||||||
|
| DR-148 | The video direct-play profile advertises only what the **webview** can decode. The audio codec list comes from `MediaCodecList`, which describes ExoPlayer — but video does not play through ExoPlayer on either platform: Android force-renders every video in the webview `<video>` element (the interim override in `VideoPlayer.svelte`, because the native SurfaceView sits behind an opaque webview) and Linux always has. Chromium and WebKit decode a far narrower set than the platform does, and the gap is widest on devices whose vendor licenses Dolby: a phone shipping `/vendor/etc/media_codecs_dolby_audio.xml` reports `ac3,eac3`, so Jellyfin direct-played an E-AC-3 track with `static=true` and the webview built a video decoder and no audio decoder at all — full picture, no sound. The defect is triggered by *capability*, not the lack of it, which is why it reproduced on one Motorola while a Fairphone and an Honor tablet played the same file on the same build: a device without the Dolby decoder never claims the codec, so the server transcodes to AAC and it plays. `video_audio_codecs` narrows the platform list to the webview-decodable set (`aac,mp3,opus,vorbis,flac`) for the video direct-play profile *only* — the audio-only profile keeps the full list, since that playback really is the native player's and narrowing it would transcode music that plays perfectly well. A list with nothing decodable still claims `aac` rather than going out empty, because a profile that claims nothing invites the server to give up instead of transcoding. The video codec list is deliberately untouched: HEVC direct-plays through the webview correctly, so the constraint is specific to audio | Playback | UR-004 | Done |
|
||||||
| DR-143 | Flipping the offline downloaded-only gate actually re-queries the listing. The gate (DR-078) is a process-wide flag in Rust consulted only *while a query runs*, but no library surface re-queried when its inputs changed: `useServerReachabilityReload` fires only on the offline → **online** transition, and `GenericMediaListPage`, `GenericGenreBrowser` and the favourites page never even called its `checkServerReachability`. So going offline left the full server catalog on screen under a now-closed gate, and toggling "Show all server media" only greyed cards — `MediaCard.isServerOnly` is a pure frontend derivation that updates instantly — without adding or removing a single row. The filter therefore read as "shows everything until I filter, then greys some of it" while the backend gate was correct and simply never exercised. `catalogFilterVersion` is the refetch signal: `pushCatalogVisibility` now awaits `set_show_server_catalog` and bumps the version only **after** the backend accepts the new flag, since a reload racing the push would re-query under the old gate and undo itself. A failed push clears `lastIncludeCatalog` instead of latching it, so the next identical transition is retried rather than skipped as a no-op and left permanently disagreeing with the backend. `useOfflineFilterReload` subscribes pages to that signal, skipping the value they already loaded under; it is wired into both generic list components and the movies/music/tv/favourites landing pages and the `/library/[id]` detail page | UI | UR-052 | Done |
|
| DR-143 | Flipping the offline downloaded-only gate actually re-queries the listing. The gate (DR-078) is a process-wide flag in Rust consulted only *while a query runs*, but no library surface re-queried when its inputs changed: `useServerReachabilityReload` fires only on the offline → **online** transition, and `GenericMediaListPage`, `GenericGenreBrowser` and the favourites page never even called its `checkServerReachability`. So going offline left the full server catalog on screen under a now-closed gate, and toggling "Show all server media" only greyed cards — `MediaCard.isServerOnly` is a pure frontend derivation that updates instantly — without adding or removing a single row. The filter therefore read as "shows everything until I filter, then greys some of it" while the backend gate was correct and simply never exercised. `catalogFilterVersion` is the refetch signal: `pushCatalogVisibility` now awaits `set_show_server_catalog` and bumps the version only **after** the backend accepts the new flag, since a reload racing the push would re-query under the old gate and undo itself. A failed push clears `lastIncludeCatalog` instead of latching it, so the next identical transition is retried rather than skipped as a no-op and left permanently disagreeing with the backend. `useOfflineFilterReload` subscribes pages to that signal, skipping the value they already loaded under; it is wired into both generic list components and the movies/music/tv/favourites landing pages and the `/library/[id]` detail page | UI | UR-052 | Done |
|
||||||
| DR-135 | A download's media type comes from the item, not a default. `download_item` — the path a media card uses to queue an item while offline — never records `media_type`, and the reconnect resolver read that NULL as `'audio'`, so a **movie** queued from a card had its URL resolved by `get_audio_stream_url`. The file that landed on disk was an audio-only transcode, which is why a "downloaded" film could never play offline no matter how the path or protocol was fixed. The resolver now falls back to the item's own `item_type` (`VIDEO_ITEM_TYPES` in Rust, so the frontend never learns which types are video) and only defaults to audio when the item is not cached locally. An explicit `media_type` on the row still wins | Downloads | UR-071, UR-052 | Done |
|
| DR-135 | A download's media type comes from the item, not a default. `download_item` — the path a media card uses to queue an item while offline — never records `media_type`, and the reconnect resolver read that NULL as `'audio'`, so a **movie** queued from a card had its URL resolved by `get_audio_stream_url`. The file that landed on disk was an audio-only transcode, which is why a "downloaded" film could never play offline no matter how the path or protocol was fixed. The resolver now falls back to the item's own `item_type` (`VIDEO_ITEM_TYPES` in Rust, so the frontend never learns which types are video) and only defaults to audio when the item is not cached locally. An explicit `media_type` on the row still wins | Downloads | UR-071, UR-052 | Done |
|
||||||
| DR-136 | Rows already downloaded under the audio default are repaired, not just prevented. They are identifiable after the fact — no `media_type`, but a video item — so on reconnect they are reset to `pending` with their audio URL cleared and re-resolved by DR-135's corrected logic, overwriting the audio file in place. Without this the fix is invisible to anyone who had already queued a film: the row still reads "downloaded" and still fails to play. Rows carrying an explicit `media_type` and genuine audio downloads are left untouched | Downloads | UR-071 | Done |
|
| DR-136 | Rows already downloaded under the audio default are repaired, not just prevented. They are identifiable after the fact — no `media_type`, but a video item — so on reconnect they are reset to `pending` with their audio URL cleared and re-resolved by DR-135's corrected logic, overwriting the audio file in place. Without this the fix is invisible to anyone who had already queued a film: the row still reads "downloaded" and still fails to play. Rows carrying an explicit `media_type` and genuine audio downloads are left untouched | Downloads | UR-071 | Done |
|
||||||
@@ -540,6 +541,7 @@ Internal architecture, components, and application logic.
|
|||||||
| UT-139 | A failed visibility push is retried on the next identical transition rather than latched | DR-143 | Done |
|
| UT-139 | A failed visibility push is retried on the next identical transition rather than latched | DR-143 | Done |
|
||||||
| UT-140 | `useOfflineFilterReload` skips the value a page already loaded under and reloads on each later change | DR-143 | Done |
|
| UT-140 | `useOfflineFilterReload` skips the value a page already loaded under and reloads on each later change | DR-143 | Done |
|
||||||
| UT-141 | The advertised channel cap: an unknown or zero reading falls back to stereo, a real route keeps its channels, an absurd driver reading is capped at 7.1, and mono is taken at its word | DR-141 | Done |
|
| UT-141 | The advertised channel cap: an unknown or zero reading falls back to stereo, a real route keeps its channels, an absurd driver reading is capped at 7.1, and mono is taken at its word | DR-141 | Done |
|
||||||
|
| UT-142 | The audio codecs offered for video direct play: a Dolby device's real `MediaCodecList` output drops `ac3`/`eac3`, AMR and raw PCM are dropped too, a fully-supported list is passed through untouched, a list with nothing decodable still claims `aac`, and stray spacing or casing does not decide whether the user gets sound | DR-148 | Done |
|
||||||
|
|
||||||
### Integration Tests
|
### Integration Tests
|
||||||
|
|
||||||
|
|||||||
+99
-80
@@ -1,15 +1,15 @@
|
|||||||
# Code Traceability Matrix
|
# Code Traceability Matrix
|
||||||
|
|
||||||
**Generated:** 8/9/2026, 4:36:27 PM
|
**Generated:** 8/11/2026, 7:10:18 PM
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
- **Total Files Scanned:** 339
|
- **Total Files Scanned:** 339
|
||||||
- **Total TRACES Found:** 578
|
- **Total TRACES Found:** 579
|
||||||
- **Requirements Covered:**
|
- **Requirements Covered:**
|
||||||
- User Requirements (UR): 67
|
- User Requirements (UR): 67
|
||||||
- Integration Requirements (IR): 17
|
- Integration Requirements (IR): 17
|
||||||
- Development Requirements (DR): 129
|
- Development Requirements (DR): 130
|
||||||
- Jellyfin API Requirements (JA): 27
|
- Jellyfin API Requirements (JA): 27
|
||||||
|
|
||||||
## Requirements by Type
|
## Requirements by Type
|
||||||
@@ -26,7 +26,7 @@ IR-003, IR-004, IR-009, IR-010, IR-011, IR-012, IR-013, IR-014, IR-015, IR-020,
|
|||||||
|
|
||||||
### Development Requirements (DR)
|
### 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-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-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-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-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-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
|
||||||
```
|
```
|
||||||
|
|
||||||
### Jellyfin API Requirements (JA)
|
### Jellyfin API Requirements (JA)
|
||||||
@@ -2234,8 +2234,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L235)
|
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L235)
|
||||||
- **Line:** 235
|
- **Line:** 235
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1858)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1873)
|
||||||
- **Line:** 1858
|
- **Line:** 1873
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### DR-107
|
### DR-107
|
||||||
@@ -2374,11 +2374,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L808)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L808)
|
||||||
- **Line:** 808
|
- **Line:** 808
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2676)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2691)
|
||||||
- **Line:** 2676
|
- **Line:** 2691
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2707)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2722)
|
||||||
- **Line:** 2707
|
- **Line:** 2722
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### DR-114
|
### DR-114
|
||||||
@@ -2432,17 +2432,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
||||||
- **Line:** 683
|
- **Line:** 683
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1798)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1813)
|
||||||
- **Line:** 1798
|
- **Line:** 1813
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2586)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2601)
|
||||||
- **Line:** 2586
|
- **Line:** 2601
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2608)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2623)
|
||||||
- **Line:** 2608
|
- **Line:** 2623
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2617)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2632)
|
||||||
- **Line:** 2617
|
- **Line:** 2632
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### DR-116
|
### DR-116
|
||||||
@@ -2479,8 +2479,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L661)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L661)
|
||||||
- **Line:** 661
|
- **Line:** 661
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2639)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2654)
|
||||||
- **Line:** 2639
|
- **Line:** 2654
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### DR-117
|
### DR-117
|
||||||
@@ -2816,8 +2816,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L243)
|
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L243)
|
||||||
- **Line:** 243
|
- **Line:** 243
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1899)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1914)
|
||||||
- **Line:** 1899
|
- **Line:** 1914
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs#L1346)
|
- **File:** [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs#L1346)
|
||||||
- **Line:** 1346
|
- **Line:** 1346
|
||||||
@@ -3091,6 +3091,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **Line:** 9
|
- **Line:** 9
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
|
### DR-148
|
||||||
|
|
||||||
|
**Locations:** 1 file(s)
|
||||||
|
|
||||||
|
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L80)
|
||||||
|
- **Line:** 80
|
||||||
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### JA-001
|
### JA-001
|
||||||
|
|
||||||
**Locations:** 1 file(s)
|
**Locations:** 1 file(s)
|
||||||
@@ -3378,11 +3386,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L452)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L452)
|
||||||
- **Line:** 452
|
- **Line:** 452
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2359)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2374)
|
||||||
- **Line:** 2359
|
- **Line:** 2374
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2400)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2415)
|
||||||
- **Line:** 2400
|
- **Line:** 2415
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### JA-033
|
### JA-033
|
||||||
@@ -3401,14 +3409,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
||||||
- **Line:** 683
|
- **Line:** 683
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1798)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1813)
|
||||||
- **Line:** 1798
|
- **Line:** 1813
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1858)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1873)
|
||||||
- **Line:** 1858
|
- **Line:** 1873
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2586)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2601)
|
||||||
- **Line:** 2586
|
- **Line:** 2601
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### JA-034
|
### JA-034
|
||||||
@@ -3421,8 +3429,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L808)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L808)
|
||||||
- **Line:** 808
|
- **Line:** 808
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2676)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2691)
|
||||||
- **Line:** 2676
|
- **Line:** 2691
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### JA-035
|
### JA-035
|
||||||
@@ -3432,8 +3440,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L243)
|
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L243)
|
||||||
- **Line:** 243
|
- **Line:** 243
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1899)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1914)
|
||||||
- **Line:** 1899
|
- **Line:** 1914
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UR-007
|
### UR-007
|
||||||
@@ -3690,8 +3698,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L243)
|
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L243)
|
||||||
- **Line:** 243
|
- **Line:** 243
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1899)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1914)
|
||||||
- **Line:** 1899
|
- **Line:** 1914
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs#L1346)
|
- **File:** [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs#L1346)
|
||||||
- **Line:** 1346
|
- **Line:** 1346
|
||||||
@@ -4040,11 +4048,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L452)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L452)
|
||||||
- **Line:** 452
|
- **Line:** 452
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2359)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2374)
|
||||||
- **Line:** 2359
|
- **Line:** 2374
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2400)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2415)
|
||||||
- **Line:** 2400
|
- **Line:** 2415
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UR-048
|
### UR-048
|
||||||
@@ -4600,20 +4608,20 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
||||||
- **Line:** 683
|
- **Line:** 683
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1798)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1813)
|
||||||
- **Line:** 1798
|
- **Line:** 1813
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2586)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2601)
|
||||||
- **Line:** 2586
|
- **Line:** 2601
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2608)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2623)
|
||||||
- **Line:** 2608
|
- **Line:** 2623
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2617)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2632)
|
||||||
- **Line:** 2617
|
- **Line:** 2632
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2639)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2654)
|
||||||
- **Line:** 2639
|
- **Line:** 2654
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UR-068
|
### UR-068
|
||||||
@@ -5054,11 +5062,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L808)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L808)
|
||||||
- **Line:** 808
|
- **Line:** 808
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2676)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2691)
|
||||||
- **Line:** 2676
|
- **Line:** 2691
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2707)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2722)
|
||||||
- **Line:** 2707
|
- **Line:** 2722
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs#L1339)
|
- **File:** [`src-tauri/src/lib.rs`](src-tauri/src/lib.rs#L1339)
|
||||||
- **Line:** 1339
|
- **Line:** 1339
|
||||||
@@ -5255,7 +5263,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
|
|
||||||
### UR-004
|
### UR-004
|
||||||
|
|
||||||
**Locations:** 25 file(s)
|
**Locations:** 26 file(s)
|
||||||
|
|
||||||
- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L264)
|
- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L264)
|
||||||
- **Line:** 264
|
- **Line:** 264
|
||||||
@@ -5323,15 +5331,18 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2)
|
- **File:** [`src-tauri/src/commands/mod.rs`](src-tauri/src/commands/mod.rs#L2)
|
||||||
- **Line:** 2
|
- **Line:** 2
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L386)
|
|
||||||
- **Line:** 386
|
|
||||||
- **Context:** `Unknown`
|
|
||||||
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L29)
|
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L29)
|
||||||
- **Line:** 29
|
- **Line:** 29
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L40)
|
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L40)
|
||||||
- **Line:** 40
|
- **Line:** 40
|
||||||
- **Context:** `pub fn clamp_max_audio_channels(reported: Option<u32>) -> u32 {`
|
- **Context:** `pub fn clamp_max_audio_channels(reported: Option<u32>) -> u32 {`
|
||||||
|
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L80)
|
||||||
|
- **Line:** 80
|
||||||
|
- **Context:** `Unknown`
|
||||||
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L386)
|
||||||
|
- **Line:** 386
|
||||||
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UT-107
|
### UT-107
|
||||||
|
|
||||||
@@ -6100,8 +6111,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L235)
|
- **File:** [`src-tauri/src/repository/mod.rs`](src-tauri/src/repository/mod.rs#L235)
|
||||||
- **Line:** 235
|
- **Line:** 235
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1858)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1873)
|
||||||
- **Line:** 1858
|
- **Line:** 1873
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UT-085
|
### UT-085
|
||||||
@@ -6192,8 +6203,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L661)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L661)
|
||||||
- **Line:** 661
|
- **Line:** 661
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2639)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2654)
|
||||||
- **Line:** 2639
|
- **Line:** 2654
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UR-036
|
### UR-036
|
||||||
@@ -6998,11 +7009,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L452)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L452)
|
||||||
- **Line:** 452
|
- **Line:** 452
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2359)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2374)
|
||||||
- **Line:** 2359
|
- **Line:** 2374
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2400)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2415)
|
||||||
- **Line:** 2400
|
- **Line:** 2415
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UT-067
|
### UT-067
|
||||||
@@ -7108,11 +7119,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L592)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L592)
|
||||||
- **Line:** 592
|
- **Line:** 592
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2676)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2691)
|
||||||
- **Line:** 2676
|
- **Line:** 2691
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2707)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2722)
|
||||||
- **Line:** 2707
|
- **Line:** 2722
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UT-100
|
### UT-100
|
||||||
@@ -7125,17 +7136,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L683)
|
||||||
- **Line:** 683
|
- **Line:** 683
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1798)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L1813)
|
||||||
- **Line:** 1798
|
- **Line:** 1813
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2586)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2601)
|
||||||
- **Line:** 2586
|
- **Line:** 2601
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2608)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2623)
|
||||||
- **Line:** 2608
|
- **Line:** 2623
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2617)
|
- **File:** [`src-tauri/src/repository/online.rs`](src-tauri/src/repository/online.rs#L2632)
|
||||||
- **Line:** 2617
|
- **Line:** 2632
|
||||||
- **Context:** `Unknown`
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
### UT-101
|
### UT-101
|
||||||
@@ -7408,4 +7419,12 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016,
|
|||||||
- **Line:** 40
|
- **Line:** 40
|
||||||
- **Context:** `pub fn clamp_max_audio_channels(reported: Option<u32>) -> u32 {`
|
- **Context:** `pub fn clamp_max_audio_channels(reported: Option<u32>) -> u32 {`
|
||||||
|
|
||||||
|
### UT-142
|
||||||
|
|
||||||
|
**Locations:** 1 file(s)
|
||||||
|
|
||||||
|
- **File:** [`src-tauri/src/repository/device_profile.rs`](src-tauri/src/repository/device_profile.rs#L80)
|
||||||
|
- **Line:** 80
|
||||||
|
- **Context:** `Unknown`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jellytau",
|
"name": "jellytau",
|
||||||
"version": "0.4.6",
|
"version": "0.4.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ describe("live requirements.md", () => {
|
|||||||
|
|
||||||
expect(defined.UR).toBe(71);
|
expect(defined.UR).toBe(71);
|
||||||
expect(defined.IR).toBe(32);
|
expect(defined.IR).toBe(32);
|
||||||
expect(defined.DR).toBe(142);
|
expect(defined.DR).toBe(143);
|
||||||
expect(defined.JA).toBe(35);
|
expect(defined.JA).toBe(35);
|
||||||
expect(defined.total).toBe(280);
|
expect(defined.total).toBe(281);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+1
-1
@@ -2018,7 +2018,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jellytau"
|
name = "jellytau"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "jellytau"
|
name = "jellytau"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -50,10 +50,100 @@ pub fn max_audio_channels() -> u32 {
|
|||||||
clamp_max_audio_channels(reported)
|
clamp_max_audio_channels(reported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Audio codecs the webview's `<video>` element can decode.
|
||||||
|
///
|
||||||
|
/// Deliberately narrower than what the platform reports: see
|
||||||
|
/// [`video_audio_codecs`].
|
||||||
|
const WEBVIEW_AUDIO_CODECS: &[&str] = &["aac", "mp3", "opus", "vorbis", "flac"];
|
||||||
|
|
||||||
|
/// The codec claimed when a device reports nothing we can use. Every renderer
|
||||||
|
/// decodes AAC, and claiming *something* is what makes the server transcode to
|
||||||
|
/// it rather than give up.
|
||||||
|
const FALLBACK_AUDIO_CODEC: &str = "aac";
|
||||||
|
|
||||||
|
/// Narrow a detected audio-codec list to what the renderer that will actually
|
||||||
|
/// play the **video** can decode.
|
||||||
|
///
|
||||||
|
/// The platform list comes from `MediaCodecList`, which describes ExoPlayer —
|
||||||
|
/// but video does not play through ExoPlayer. Both Android and Linux render it
|
||||||
|
/// in a webview `<video>` element, and Chromium/WebKit decode a much smaller set
|
||||||
|
/// than the platform does. Advertising the raw list makes Jellyfin direct-play a
|
||||||
|
/// track the webview cannot decode, and the user gets picture with no sound.
|
||||||
|
///
|
||||||
|
/// The gap is widest on devices whose vendor licenses Dolby: a phone with
|
||||||
|
/// `c2.dolby.eac3.decoder` reports `eac3`, so it — and only it — gets a silent
|
||||||
|
/// direct play where a leaner device is transcoded to AAC and plays fine.
|
||||||
|
///
|
||||||
|
/// This applies to the *video* direct-play profile only. Audio-only playback
|
||||||
|
/// really is ExoPlayer's, so its profile keeps the full platform list.
|
||||||
|
///
|
||||||
|
/// TRACES: UR-004 | DR-148 | UT-142
|
||||||
|
pub fn video_audio_codecs(detected: &str) -> String {
|
||||||
|
let kept: Vec<&str> = detected
|
||||||
|
.split(',')
|
||||||
|
.filter_map(|codec| {
|
||||||
|
let codec = codec.trim();
|
||||||
|
// Match case-insensitively but emit our own spelling: the platform
|
||||||
|
// list is assembled from MIME strings and its casing is not ours to
|
||||||
|
// forward to the server.
|
||||||
|
WEBVIEW_AUDIO_CODECS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|supported| supported.eq_ignore_ascii_case(codec))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if kept.is_empty() {
|
||||||
|
FALLBACK_AUDIO_CODEC.to_string()
|
||||||
|
} else {
|
||||||
|
kept.join(",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_dolby_device_does_not_advertise_dolby_for_video() {
|
||||||
|
// The bug: a Motorola reporting c2.dolby.eac3.decoder direct-played
|
||||||
|
// E-AC-3 into a webview that cannot decode it — silent video, on that
|
||||||
|
// device only.
|
||||||
|
let codecs = video_audio_codecs("aac,ac3,amrnb,amrwb,eac3,flac,mp3,opus,pcm,vorbis");
|
||||||
|
assert_eq!(codecs, "aac,flac,mp3,opus,vorbis");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codecs_the_webview_cannot_decode_are_dropped() {
|
||||||
|
// AMR and raw PCM come from the AOSP set, so this is not a Dolby-only
|
||||||
|
// problem — it is just rarer content.
|
||||||
|
assert_eq!(video_audio_codecs("amrnb,amrwb,pcm,aac"), "aac");
|
||||||
|
assert_eq!(video_audio_codecs("dts,truehd,mp3"), "mp3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_list_the_webview_fully_supports_is_untouched() {
|
||||||
|
assert_eq!(
|
||||||
|
video_audio_codecs("aac,mp3,opus,vorbis,flac"),
|
||||||
|
"aac,mp3,opus,vorbis,flac"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nothing_decodable_still_claims_aac() {
|
||||||
|
// Claiming an empty list invites the server to give up rather than
|
||||||
|
// transcode. AAC is universally decodable, so ask for it.
|
||||||
|
assert_eq!(video_audio_codecs("eac3,dts"), "aac");
|
||||||
|
assert_eq!(video_audio_codecs(""), "aac");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spacing_and_case_in_the_platform_list_are_tolerated() {
|
||||||
|
// The list is assembled from MediaCodecList strings; do not let
|
||||||
|
// whitespace decide whether the user gets sound.
|
||||||
|
assert_eq!(video_audio_codecs("aac, EAC3 , Mp3"), "aac,mp3");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_unknown_route_falls_back_to_stereo() {
|
fn an_unknown_route_falls_back_to_stereo() {
|
||||||
// Codec detection has not run yet, or the platform has no answer. Never
|
// Codec detection has not run yet, or the platform has no answer. Never
|
||||||
|
|||||||
@@ -1356,8 +1356,9 @@ impl MediaRepository for OnlineRepository {
|
|||||||
// Linux desktop plays video through the WebKitGTK HTML5 <video> element,
|
// Linux desktop plays video through the WebKitGTK HTML5 <video> element,
|
||||||
// which cannot reliably decode HEVC/AV1/VP9. Advertise only codecs the
|
// which cannot reliably decode HEVC/AV1/VP9. Advertise only codecs the
|
||||||
// WebView can decode so Jellyfin transcodes anything else to h264 HLS.
|
// WebView can decode so Jellyfin transcodes anything else to h264 HLS.
|
||||||
// (Audio-only files still direct-play via MPV, but the PlaybackInfo
|
// (Audio-only files still direct-play via MPV; these codecs are what
|
||||||
// profile is shared, so we keep the broadly-supported audio codecs.)
|
// both renderers handle, and the audio profile keeps them in full while
|
||||||
|
// the video profile is narrowed below.)
|
||||||
#[cfg(all(not(target_os = "android"), target_os = "linux"))]
|
#[cfg(all(not(target_os = "android"), target_os = "linux"))]
|
||||||
let (video_codecs, audio_codecs) =
|
let (video_codecs, audio_codecs) =
|
||||||
("h264".to_string(), "aac,mp3,opus,vorbis,flac".to_string());
|
("h264".to_string(), "aac,mp3,opus,vorbis,flac".to_string());
|
||||||
@@ -1368,8 +1369,19 @@ impl MediaRepository for OnlineRepository {
|
|||||||
"aac,mp3,opus,vorbis,flac".to_string(),
|
"aac,mp3,opus,vorbis,flac".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Video plays in a webview <video> element on every platform, which
|
||||||
|
// decodes a narrower audio set than the platform does — so the video
|
||||||
|
// profile must claim less than the audio-only profile. Without this a
|
||||||
|
// Dolby-licensed device advertises eac3, gets a direct play, and shows
|
||||||
|
// picture with no sound.
|
||||||
|
let video_audio_codecs = super::device_profile::video_audio_codecs(&audio_codecs);
|
||||||
|
|
||||||
info!("[DeviceProfile] Using video codecs: {}", video_codecs);
|
info!("[DeviceProfile] Using video codecs: {}", video_codecs);
|
||||||
info!("[DeviceProfile] Using audio codecs: {}", audio_codecs);
|
info!("[DeviceProfile] Using audio codecs: {}", audio_codecs);
|
||||||
|
info!(
|
||||||
|
"[DeviceProfile] Audio codecs for video direct play: {}",
|
||||||
|
video_audio_codecs
|
||||||
|
);
|
||||||
|
|
||||||
// Bound every profile by what the audio route can actually voice, so a
|
// Bound every profile by what the audio route can actually voice, so a
|
||||||
// multichannel track is downmixed by the server rather than direct-played
|
// multichannel track is downmixed by the server rather than direct-played
|
||||||
@@ -1388,12 +1400,16 @@ impl MediaRepository for OnlineRepository {
|
|||||||
profile_type: "Video".to_string(),
|
profile_type: "Video".to_string(),
|
||||||
container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp".to_string(),
|
container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp".to_string(),
|
||||||
video_codec: Some(video_codecs.clone()),
|
video_codec: Some(video_codecs.clone()),
|
||||||
audio_codec: audio_codecs.clone(),
|
// The webview decodes this stream, not ExoPlayer/MPV.
|
||||||
|
audio_codec: video_audio_codecs.clone(),
|
||||||
},
|
},
|
||||||
DirectPlayProfile {
|
DirectPlayProfile {
|
||||||
profile_type: "Audio".to_string(),
|
profile_type: "Audio".to_string(),
|
||||||
container: "mp3,aac,flac,alac,wav,ogg,wma,opus".to_string(),
|
container: "mp3,aac,flac,alac,wav,ogg,wma,opus".to_string(),
|
||||||
video_codec: None,
|
video_codec: None,
|
||||||
|
// Audio-only really is the native player's, so it keeps the
|
||||||
|
// full platform list — narrowing it would transcode music
|
||||||
|
// that plays perfectly well.
|
||||||
audio_codec: audio_codecs.clone(),
|
audio_codec: audio_codecs.clone(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "jellytau",
|
"productName": "jellytau",
|
||||||
"version": "0.4.6",
|
"version": "0.4.7",
|
||||||
"identifier": "com.dtourolle.jellytau",
|
"identifier": "com.dtourolle.jellytau",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user