From 3b91922ccab686487679b5dbd5e44499ba21bf0a Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 22 Aug 2026 21:17:50 +0200 Subject: [PATCH] feat(player): the MediaPlayer contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DR-242. Intent, not device operations. `open` carries the start position, so no caller sequences load-then-seek and none can race an engine's asynchronous load — the engine is the only layer that knows when its pipeline can accept a position, and it absorbs that internally by deferring or re-opening. `seek` states a destination and nothing else. Whether that is an in-place seek or a re-opened stream is the engine's business: hls.js seeks within a VOD playlist, mpv's HLS demuxer cannot make a server transcode from a new offset. Callers stop guessing on behalf of engines they do not own. `snapshot` is one coherent read rather than a dozen getters, because reading position and duration separately is how a player reported / 0.0 when a file unloaded between the two calls. `Phase::Opening` names the state the previous design could not express, and is the direct cause of DR-241: a seek arriving with nothing loaded had no phase to be queued against, so it was discarded. `Capabilities` exists so callers adapt without naming engines. If a caller ever branches on which engine it holds, this struct is missing something — engine identity leaking into callers is the coupling DR-238 came from. Nothing consumes it yet; PlayerController is ported in DR-245. Carries an explicit allow(dead_code) tied to that step rather than being hidden behind cfg(test), because it is production code being built in shippable pieces. --- docs/requirements.md | 11 ++ src-tauri/src/player/media_player.rs | 206 +++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 src-tauri/src/player/media_player.rs diff --git a/docs/requirements.md b/docs/requirements.md index c994dc87..96d76375 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -90,6 +90,7 @@ For a narrative overview of the system design, see | UR-078 | JellyTau keeps a record of what it did, and can hand it over. The app forgot everything the moment it exited: the backend logged to stdout only — which a user launching from a desktop icon never sees, and which on Android is not logcat, so the Rust half was invisible on the platform carrying the hardest bugs. A crash left nothing at all. Logs are now written to a size-capped rotating file, a panic is recorded before the process dies, the frontend's messages land in the same timeline as the backend's, and Settings exports the lot as one file to attach to a bug report. Nothing is transmitted anywhere — the user attaches it themselves, which is also what keeps this from being telemetry. Access tokens and passwords never reach the file | Medium | Done | | UR-079 | The app decides *what stream to play* and says so. Playing a video used to mean asking the server to re-encode it, always — a decision made nowhere, written down nowhere, and re-derived downstream by whoever needed it: the player worked out whether it had been handed a playlist by looking for `.m3u8` in the URL. So a viewer paid for a transcode of a file their device could have played untouched, and the app could not tell them which it was. Now one negotiation produces one self-describing answer — direct play, remux, or transcode; over a playlist, a plain HTTP file, or a local one — and every renderer consumes that same answer instead of guessing from a string. On Android, where the player decodes almost everything the library holds, this stops around 85% of plays from starting a transcode nobody needed | Medium | Done | | UR-080 | Video on the desktop plays as itself. The picture was drawn by a webview `