Commit Graph
10 Commits
Author SHA1 Message Date
dtourolle bb14c66e71 fix(player): three defects from review, and one duplicate removed
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 21m37s
🏗️ Build and Test JellyTau / Supply Chain (push) Successful in 2m55s
Traceability Validation / Check Requirement Traces (push) Successful in 23s
Build & Release / Run Tests (push) Successful in 15m34s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m26s
Build & Release / Build Linux (push) Successful in 20m58s
Build & Release / Build Windows (push) Successful in 16m10s
Build & Release / Build Android (push) Successful in 31m25s
Build & Release / Create Release (push) Successful in 1m5s
Verified each against the code before acting; four of the five findings held,
one did not.

DR-253 — a deferred seek outlived its file. `seek` holds a position while MPV
has nothing loaded and `FileLoaded` applies it (DR-241), but neither `load` nor
`stop` discarded it. Scrub near the end of a transcoded item — which re-opens
the stream — then skip to the next item before the reload completes, and the
old position lands on the new item. It starts wherever the previous one was
scrubbed to, silently. Both lifecycle points clear it now.

DR-254 — a per-playback quality ceiling outlived its playback. The override is
process-wide and describes one playback: dropping to 720p for a struggling
episode says nothing about the next. Every advance the frontend drives clears
it through player_play_item, but the background audio-only advance loads the
next episode in Rust and skipped all three clearing sites — so every later
episode stayed capped, with nothing in the UI explaining why.

DR-255 — `playable_url` was a byte-identical copy of `playback_url`, added for
the cross-platform open path. The original is `#[cfg(target_os = "android")]`,
so it does not exist in a Linux build and nothing warned. Two matches over
MediaSource meant a new variant could be handled in one and forgotten in the
other. The gate is gone and the copy with it.

The fifth finding — that the comment on `video_audio_codecs` describes a
renderer switch the code no longer has — does not hold. `get_player_status`
hard-codes Android to Native, but `experimentalNativeVideo` is still live in
VideoPlayer.svelte as a suppressor that can force HTML5 even when Rust says
native. The switch exists, so the narrow codec list is still doing its job.

Both correctness fixes are red-then-green. The tests are wiring assertions in
the style of UT-218: what matters is the call site, and reaching these at
runtime needs a live MPV handle or a repository, a server and a player. That
technique now appears three times and is worth watching — it pins call sites,
not behaviour.

The review's sharpest point is one it raised as redundancy: MpvPlayer already
handles DR-253 correctly, resetting deferred state on every open, and the old
path had to be patched separately. That is the drift two parallel engines
produce, and the argument for finishing DR-248/249 rather than leaving
LegacyPlayer in place indefinitely.

795 Rust tests, 1088 frontend, every CI check green locally.
2026-08-23 11:50:47 +02:00
dtourolle 11d9d760d8 feat(player): native video on Linux, and one contract for every player (v0.11.0)
mpv now decodes video on Linux, drawn into a framebuffer we own and blitted
into the default vbox's draw handler. Tauri's widget tree is untouched, so an
upgrade that assumes its own layout cannot invalidate this. Direct play means
the original file, hardware decoding, and no server transcode at all — where
previously every desktop video was re-encoded to h264 for the browser engine,
whatever the file actually was. Off by default: JELLYTAU_NATIVE_VIDEO=1.

That settles finding 2 of playback-backend-unification.md — "native video
cannot be composited with a Tauri webview" — by demonstration rather than
argument, on X11 and Wayland both.

Turning it on exposed nine defects, none of them mpv's. Each was the same
mistake in a different place: a capability written down as a compile-time fact
about the platform, or a state asserted instead of confirmed.

  DR-238/246  a seek routed by the stream's container rather than by what the
              engine could do with it - correct only while one player handled
              those streams, silent the moment another did
  DR-239      a property handled but never observed, so the play/pause button
              waited for an event that could not arrive
  DR-240      fullscreen expanding the document while the window stayed put
  DR-241      a seek issued before the engine had a file, failed, and discarded
              - which is why resume began at zero
  DR-247      a Linux-only gate outliving the caller that made it Linux-only,
              breaking the Android build outright
  DR-250      a stop aimed at whichever renderer bookkeeping believed was in
              charge, missing the one actually making sound
  DR-251      a duration of zero believed, leaving the seek bar no scale
  DR-252      a junk float converted to a Duration, panicking the backend the
              instant a length-less stream appeared

So the MediaPlayer contract (DR-242 … DR-247): `open` carries a start position,
so no caller sequences load-then-seek and none can race an engine's load;
`seek` states a destination and leaves in-place-versus-re-open to the engine;
`snapshot` is one coherent read; and `Phase::Opening` names the window where
intent used to be lost. One conformance suite runs against every engine —
FakePlayer and mpv under cargo test, ExoPlayer instrumented on a device — so an
engine is either correct or visibly failing.

Two of the nine were introduced during this work and caught on hardware, not by
any suite: an over-broad capability that grouped ExoPlayer with mpv, and the
Duration panic. The suites test engines that behave. That is recorded in
docs/native-player-verification.md, which asks for the exact action sequences
that found them.

Verified: all automated gates, conformance (mpv 9/9, legacy 8/9 by design,
ExoPlayer 7/7 on device), and manual desktop and Android passes on real
hardware.

Known open and deliberately shipped: resume reads local progress and never the
server's; the background-audio handoff still declares a state swap it does not
confirm (the symptom is now impossible, the race is not); and `bun run
android:dev` builds an APK carrying the release application id, whose failure
message advises an uninstall that would destroy app data. Fix that last one
before anyone else builds for Android.

Squashed from worktree-linux-native-video, which keeps the per-defect history.
2026-08-23 10:51:45 +02:00
dtourolle 6a712c46cb fix(player): send subtitle tracks to ExoPlayer on Android (UR-020)
Selecting a subtitle on Android did nothing. The Kotlin side has been
complete for a long time — JellyTauPlayer.load() parses a subtitles JSON
array into MediaItem.SubtitleConfigurations and setSubtitleTrack() drives a
TrackSelectionOverride — but nothing ever reached it.

VideoPlayer built the list and then threw it away: it resolved every
subtitle stream's URL into a subtitleTracks array and the
commands.playerPlayItem({...}) call two lines below passed only streamUrl,
title, id, videoCodec and needsTranscoding. PlayItemRequest had no subtitle
field to put them in, so create_media_item hardcoded subtitles: vec![],
android/mod.rs serialized "[]" across JNI, and every MediaItem reached
ExoPlayer with zero SubtitleConfigurations. A later set_subtitle_track then
found no text track groups and logged "Invalid subtitle track index".

PlayItemRequest now carries the tracks (defaulted, so the background-audio
handoff and next-episode callers are unchanged) and create_media_item
threads them onto the MediaItem.

Serialization: SubtitleTrack is reused verbatim rather than given an
IPC-specific twin, and deliberately keeps snake_case. The same struct feeds
two consumers that both spell mime_type — the JNI JSON that
JellyTauPlayer.load() reads with optString("mime_type"), and the generated
binding the frontend types against. camelCasing it would not fail the build
or the IPC; Kotlin would silently fall back to its default MIME type for
every track. UT-146 asserts the exact serialized keys so a future
rename_all cannot pass unnoticed.

The index mapping was NOT already correct. setSubtitleTrack(n) indexes
ExoPlayer's filtered text track groups, i.e. the position of the sideloaded
configuration — but the menu passed its own {#each} row number, which counts
every subtitle *stream*, including ones whose URL failed to resolve and were
therefore never sideloaded. One failed URL and every track below it selected
the wrong subtitle. The position is now looked up in the exact array that
was sent (nativeSubtitleArrayIndex), and a stream that was never sent maps
to "off" rather than to a guessed position.

The resolution loop also reuses resolveSubtitleTracks() from the Linux fix
instead of duplicating it, which fans the URL requests out in parallel
rather than awaiting them one per stream before playback can start. The
awaits are safe where they sit: the native-mode pitfall is about Svelte
lifecycle calls after an await, and nothing is registered here — the
background-audio subscriptions above still run synchronously.

No Kotlin change was needed.

Tests (UT-145, UT-146, UT-147) were written first and failed: PlayItemRequest
had no subtitles field to compile against, nativeSubtitleTracks and
nativeSubtitleArrayIndex did not exist, and the playerPlayItem call carried
no subtitles key.

TRACES: UR-020 | IR-016, JA-008 | UT-145, UT-146, UT-147
2026-08-11 20:03:19 +02:00
dtourolle 93d198ce21 domain: primaryImageTag -> imageId end-to-end (phase 4a/4b)
Rust: PlayerMediaItem and MergedMediaItem gain image_id (dual-carry),
populated from primary_image_tag at every construction/conversion site.
Regenerated bindings.

Frontend: all catalog + player + merged readers now use imageId. The
NowPlayingItem->MediaItem bridge (player.ts) properly maps the remote
session's Jellyfin fields (Type, runTimeTicks, primaryImageTag) onto the
neutral kind/durationMs/imageId. Types that are genuinely out of scope
(Person, NowPlayingItem, PlayItemRequest) keep primaryImageTag.

Rust 456, frontend 644, check clean.
2026-07-23 21:40:58 +02:00
dtourolle 3fbf6afdbc Background-audio handoff for video + repository/player refactor
Hand video playback off to a native audio-only stream when the app is
backgrounded or locked, with no on-device video decode (UR-040). Adds
player_enter/exit_background_audio commands, an audio-only stream URL
for video items across the repository layer, and the frontend handoff
state machine wired into VideoPlayer. Includes accompanying
repository/offline/player refactoring and regenerates the traceability
matrix.
2026-07-22 21:52:07 +02:00
dtourolle ea342d76e3 Fix download + casting breakages from the specta migration
Casting: restore camelCase serialization on SessionInfo/NowPlayingItem/PlayState
(container rename_all = "camelCase" + per-field PascalCase serde aliases so Jellyfin
PascalCase still deserializes). Bindings and the existing frontend are camelCase
again — casting works with no frontend session changes.

Downloads: migrate DownloadButton.svelte and downloads.ts to the typed
commands.downloadItemAndStart / downloadItem / downloadVideo wrappers (request
objects), matching the bundled backend signatures.

Tooling:
- Enable tauri-specta ErrorHandlingMode::Throw so commands.* return Promise<T>
  and throw (drop-in for invoke()).
- Disambiguate specta name collisions: player MediaItem/MediaSource ->
  PlayerMediaItem/PlayerMediaSource, auth ServerInfo -> AuthServerInfo.
- Regenerate bindings.ts; svelte-check passes (0 errors).
2026-06-20 19:27:29 +02:00
dtourolle ada3ed64ab Workstream E (backend): wire tauri-specta — annotate commands, derive Type, generate-ready Builder
- Add #[specta::specta] to all 201 #[tauri::command] functions.
- Derive specta::Type on all IPC DTOs (repository/types, settings, player/storage/
  download command DTOs, player enums, jellyfin SessionInfo/NowPlayingItem/PlayState,
  ThumbnailCacheStats, DownloadInfo, CacheConfig, etc.).
- Replace tauri::generate_handler! with a tauri_specta::Builder + collect_commands!
  in lib.rs (exports bindings.ts in debug builds).

Two contract changes required by specta constraints (frontend migration follows):
- specta caps command arity at 10 args: download_item_and_start / download_item /
  download_video now take a single request struct (params bundled, body unchanged
  via destructuring).
- specta can't parse split serde rename_all: SessionInfo/NowPlayingItem/PlayState
  switched to rename_all = "PascalCase" (Jellyfin deserialization preserved; these
  now serialize PascalCase to the frontend).

cargo check --lib is clean (0 errors). Frontend migration to bindings.ts is the next step.
2026-06-20 18:20:25 +02:00
dtourolle a5e065daed fix tests
Traceability Validation / Check Requirement Traces (push) Has been cancelled
🏗️ Build and Test JellyTau / Build APK and Run Tests (push) Has been cancelled
2026-02-14 09:27:12 +01:00
dtourolleandClaude Haiku 4.5 6d1c618a3a Implement Phase 1-2 of backend migration refactoring
CRITICAL FIXES (Previous):
- Fix nextEpisode event handlers (was calling undefined methods)
- Replace queue polling with event-based updates (90% reduction in backend calls)
- Move device ID to Tauri secure storage (security fix)
- Fix event listener memory leaks with proper cleanup
- Replace browser alerts with toast notifications
- Remove silent error handlers and improve logging
- Fix race condition in downloads store with request queuing
- Centralize duration formatting utility
- Add input validation to image URLs (prevent injection attacks)

PHASE 1: BACKEND SORTING & FILTERING 
- Created Jellyfin field mapping utility (src/lib/utils/jellyfinFieldMapping.ts)
  - Maps frontend sort keys to Jellyfin API field names
  - Provides item type constants and groups
  - Includes 20+ test cases for comprehensive coverage
- Updated route components to use backend sorting:
  - src/routes/library/music/tracks/+page.svelte
  - src/routes/library/music/albums/+page.svelte
  - src/routes/library/music/artists/+page.svelte
- Refactored GenericMediaListPage.svelte:
  - Removed client-side sorting/filtering logic
  - Removed filteredItems and applySortAndFilter()
  - Now passes sort parameters to backend
  - Uses backend search instead of client-side filtering
  - Added sortOrder state for Ascending/Descending toggle

PHASE 3: SEARCH (Already Implemented) 
- Search now uses backend repository_search command
- Replaced client-side filtering with backend calls
- Set up for debouncing implementation

PHASE 2: BACKEND URL CONSTRUCTION (Started)
- Converted getImageUrl() to async backend call
- Removed sync URL construction with credentials
- Next: Update 12+ components to handle async image URLs

UNIT TESTS ADDED:
- jellyfinFieldMapping.test.ts (20+ test cases)
- duration.test.ts (15+ test cases)
- validation.test.ts (25+ test cases)
- deviceId.test.ts (8+ test cases)
- playerEvents.test.ts (event initialization tests)

SUMMARY:
- Eliminated all client-side sorting/filtering logic
- Improved security by removing frontend URL construction
- Reduced backend polling load significantly
- Fixed critical bugs (nextEpisode, race conditions, memory leaks)
- 80+ new unit tests across utilities and services
- Comprehensive infrastructure for future phases

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-13 23:34:18 +01:00
dtourolle cfddc1edea First working POC 2026-01-26 22:21:54 +01:00