Android ships no AC-3, E-AC-3, DTS or TrueHD decoders; they are licensed
codecs, present only where a vendor paid for them. The ROD2-W09 tablet has a
vendor DTS decoder and no AC-3/E-AC-3 at all. So every film with Dolby audio
was re-encoded by the server, for streaming and for download alike, and a
transcoded download has no Content-Length and ignores Range: ~1 MB/s,
restarting from byte zero on every network blip.
ExoPlayer now carries Jellyfin's media3 FFmpeg audio decoder in extension
mode ON (platform decoders first, FFmpeg for what they lack), and
CodecDetector reports its codecs so the device profile and the download
policy agree with what actually decodes. The download policy judges audio
against the renderer that will play the file (renderer_can_decode_audio)
instead of the webview's list, so Android downloads are always the direct
copy — a 910 MB E-AC-3 5.1 episode downloaded in 94 s and played offline.
The webview video path is removed on Android: it decodes none of these
codecs, so a stored "native video off" would play every original-file
download silent. Rust reports webview_video_fallback (false on Android, true
only beside mpv native video on Linux); Settings offers the switch and the
player honours it only then. Linux keeps the fallback and, with it, the
server transcode for undecodable audio.
The decoder is GPL-3.0; the distributed APK carries its terms and the source
stays MIT (THIRD_PARTY_NOTICES.md). The on-device remux spec this replaces is
folded into 05-platform-backends.md and deleted.
DR-293, UT-259, UT-262.
A spec was a promise; sixteen of them had become descriptions of code that
already shipped, sitting beside four that describe work still outstanding, with
nothing in the file telling the two apart. Half the statuses were also wrong —
audio-equalizer read "Accepted" with the EQ live on both platforms, the native
video spec said the flag stays off after the default was flipped on.
The shipped designs move into docs/architecture, which is the maintained
description of the build, and the spec files go. Git history keeps the
originals; what a future change still needs is carried across:
- 01-rust-backend: favourites rewritten (the old section named a file that no
longer exists and called shipped buttons "planned"), domain vocabulary owned
by Rust (SearchScope, exclusions, the bitrate ladder), background workers
- 02-svelte-frontend: app shell and chrome, library mosaic, series/episode
navigation, downloaded browse, safe-area insets, native-video store, logging
- 03-data-flow: locally-indexed search
- 05-platform-backends: audio settings on ExoPlayer, the equalizer's band
vocabulary, native video compositing, the background-audio handoff
- 06-downloads-and-offline: one storage model, offline catalog visibility
- 09-security: path confinement and input binding
docs/specs/README.md now says what the directory is for and where each shipped
design went. Deferred work the specs recorded is kept beside the code it
concerns rather than lost: season-bounded autoplay, the two dead search
commands, why indexing is a full crawl.
requirements.md had fourteen stale statuses — Android audio parity still read
"Linux only", DR-150 still said the native-video default was off, DR-190 was
Proposed after DR-196 implemented it, and five tooling requirements were
Proposed after landing. Three unbuilt specs suggested requirement ids that have
since been allocated to other work; each now carries a warning.
Establish a decoupled player boundary so UI and backend interact with video
through one contract, with the HTML5 (Linux/interim-Android) and native
(ExoPlayer) providers as interchangeable primitive-executor adapters.
- PlayerAdapter interface + AdapterHost callback bag (adapters/types.ts): the
adapter owns only decision-free element PRIMITIVES (seekElement, reloadSource,
play/pause, setVolume, selectSubtitle); it never branches on strategy.
- Seek/audio-track DECISIONS stay in Rust (player_seek_video / _switch_audio_track
return a strategy); the facade dispatches the chosen primitive to the active
adapter. Both providers share the one decision path — logic lives once, in Rust.
- Facade holds the active adapter; a new ControlCommand PlayerStatusEvent lets
backend control (lockscreen/remote/sleep) drive the webview <video> element.
- Html5PlayerAdapter resolves the LIVE element via the bridge (fixes play/pause
silently no-opping when the element was re-bound).
- Do not emit a "stopped" player state on natural end-of-video: it flipped the
player/mode to idle mid-handoff and suppressed next-episode auto-advance under
a sleep timer. Jellyfin progress reporting is preserved; the backend's
on_video_playback_ended owns the transition.
- VideoPlayer net -300 lines (strategy/HLS-reload logic relocated to the adapter).
- Adds 20 adapter unit tests; existing suites stay green (vitest 457, cargo 416).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>