fix(playback): seek Embedded MPV steps relative to mpv's own position (#1518)

* fix(playback): seek Embedded MPV steps relative to mpv's own position

Arrow keys and the ±10 s buttons in the Embedded MPV player advanced only
about a second per press when pressed repeatedly or held. The shortcuts
already asked for 5 s steps, but `EmbeddedMpvCommandRunner.seekBy` turned
each step into an absolute `seek` computed from `session.positionSeconds`,
which is floored to whole seconds, polled every 500 ms (helper snapshots at
most every 250 ms) and not refreshed by the seek reply. Every press inside
that window therefore landed on the same target.

Steps now go through a new `EMBEDDED_MPV_SEEK_BY` IPC / `seekEmbeddedMpvBy`
bridge method that every backend forwards as mpv `seek <delta>
relative+exact`: `seekBy` exports in the macOS addon and the Windows/Linux
`wid` addon (Linux over its JSON IPC socket), and a `seek-by` stdin command
in the frame-copy helper. mpv resolves the delta against its own position
and merges queued relative seeks, so presses accumulate as in mpv itself.
The absolute form survives only as a fallback for a preload without the
method or an addon binary without `seekBy`; the timeline scrub still
commits an absolute target.

Validated with a real mpv 0.39 IPC probe: three relative seeks in a burst
advance +15 s, three absolute seeks from one stale base advance +5 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(playback): drop speculative position update from relative Embedded MPV seeks

Review follow-up for the relative seek path.

The macOS and Windows/Linux `seekBy` exports advanced `snapshot.positionSeconds`
by the delta after dispatching the mpv command. That is not idempotent the way
the absolute seek's optimistic write is: the observer (mpv event thread, or
the Linux IPC poll) can already have stored the post-seek `time-pos` under the
same mutex, so adding the delta on top counted the step twice, and while paused
nothing corrected it. On Linux it also advertised a position that a failed
socket delivery never reached. Relative steps now leave the snapshot alone;
only the observed `time-pos` updates the position.

The packaged Linux frame-copy smoke now drives `seekEmbeddedMpvBy` through the
built app: a burst of three +2 s steps issued without waiting for snapshots has
to land on 6 s, and a -60 s step has to clamp at 0. The generated Y4M fixture
grows from 2 s to 12 s (about 415 KB) so the burst and the playing section that
follows stay inside the clip. Replayed against a local mpv 0.39 with the same
fixture and media server: burst -> 6.0, -60 -> 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(agents): mirror the Embedded MPV relative-seek contract into AGENTS.md

Review follow-up: the Shared Player Controls section documents the frame-copy
commands and shortcuts, so the relative seekEmbeddedMpvBy invariant lives
there too, next to the CLAUDE.md note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(playback): reject a Linux relative seek the mpv IPC socket did not accept

Review follow-up: the Linux branch of SeekBy discarded the socket transaction
result and returned normally, so a step that never reached mpv looked like a
seek still awaiting observation. It now throws like a failed mpv_command_async
on the in-process engines; the renderer swallows the rejection and resyncs
from the next snapshot, and the main process logs it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 authored and GitHub committed 2026-09-04 17:36:54 +02:00
1 parent ad81fbfc45
commit b2ca85172c
21 files changed
+392 -11

No files matched your search

+1 -1
View File
@@ -1008,7 +1008,7 @@ app as a real argument, so it is not an option.
API. Radio's `<audio>` deliberately never blocks display sleep. Embedded
MPV holds its own blocker in `EmbeddedMpvNativeService`; external MPV/VLC
inhibit the screensaver themselves.
- Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. Two per-session knobs are captured at session creation from the main-process settings mirror (`readEmbeddedMpvSessionOptions()`, read in the `EMBEDDED_MPV_CREATE_SESSION` handler — never inside the service, whose specs would otherwise construct electron-conf): `Settings.embeddedMpvExtraOptions` (free-form `key=value` libmpv lines, forbidden embed-critical keys refused by the form, network defaults `network-timeout=10` + ffmpeg `reconnect` prepended, applied on every engine after its built-ins — Windows/macOS via `mpv_set_option_string`, Linux through a user-only `--include` config file, frame-copy as the helper's first stdin line — never on a command line, where `ps` could read a credential-bearing header option) and `Settings.embeddedMpvAutoReconnect` (default on: `EmbeddedMpvReconnectCoordinator` in `embedded-mpv-reconnect.ts` reloads the last playback on `error`, or `ended` for live, only if it had played, with 2 s→30 s backoff, six attempts per outage, budget reset after 30 s stable playing, cancelled by user loads/pause/dispose; a recording running at the drop is finalized as an interrupted partial when the reload actually replaces the stream (a stream that recovers on its own keeps recording) and restarted into a new file once that reload plays, and an external subtitle file added through `sub-add` is re-added once the reload plays unless the user picked another track or loaded something else since; the renderer only displays `EmbeddedMpvSession.reconnect`). Contract: `docs/architecture/embedded-mpv-native.md` ("Session Options", "Network Auto-Reconnect"). macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=<x11-window>` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Renderer bounds are CSS pixels; the service converts them to native units in the main process (`embedded-mpv-bounds.util.ts`: × page zoom everywhere, × display scale on Windows/Linux whose child windows are positioned in physical pixels; frame-copy bounds stay unscaled), and the session controller re-syncs bounds when `devicePixelRatio` changes and polls (500 ms, drift-gated) for position-only layout shifts that `ResizeObserver` cannot observe. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`.
- Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. Two per-session knobs are captured at session creation from the main-process settings mirror (`readEmbeddedMpvSessionOptions()`, read in the `EMBEDDED_MPV_CREATE_SESSION` handler — never inside the service, whose specs would otherwise construct electron-conf): `Settings.embeddedMpvExtraOptions` (free-form `key=value` libmpv lines, forbidden embed-critical keys refused by the form, network defaults `network-timeout=10` + ffmpeg `reconnect` prepended, applied on every engine after its built-ins — Windows/macOS via `mpv_set_option_string`, Linux through a user-only `--include` config file, frame-copy as the helper's first stdin line — never on a command line, where `ps` could read a credential-bearing header option) and `Settings.embeddedMpvAutoReconnect` (default on: `EmbeddedMpvReconnectCoordinator` in `embedded-mpv-reconnect.ts` reloads the last playback on `error`, or `ended` for live, only if it had played, with 2 s→30 s backoff, six attempts per outage, budget reset after 30 s stable playing, cancelled by user loads/pause/dispose; a recording running at the drop is finalized as an interrupted partial when the reload actually replaces the stream (a stream that recovers on its own keeps recording) and restarted into a new file once that reload plays, and an external subtitle file added through `sub-add` is re-added once the reload plays unless the user picked another track or loaded something else since; the renderer only displays `EmbeddedMpvSession.reconnect`). Contract: `docs/architecture/embedded-mpv-native.md` ("Session Options", "Network Auto-Reconnect"). macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=<x11-window>` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Renderer bounds are CSS pixels; the service converts them to native units in the main process (`embedded-mpv-bounds.util.ts`: × page zoom everywhere, × display scale on Windows/Linux whose child windows are positioned in physical pixels; frame-copy bounds stay unscaled), and the session controller re-syncs bounds when `devicePixelRatio` changes and polls (500 ms, drift-gated) for position-only layout shifts that `ResizeObserver` cannot observe. Arrow-key and ±10 s button steps go through the relative `seekEmbeddedMpvBy` IPC (mpv `seek <delta> relative+exact`; addon export `seekBy`, helper stdin command `seek-by`, Linux JSON IPC), never an absolute target computed from the renderer's whole-second, 500 ms-polled `positionSeconds` — that stale base collapsed rapid presses onto one target (about 1 s of progress per press); only the timeline scrub commits an absolute `seek`. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`.
- Embedded MPV frame-copy engine (experimental, macOS Apple Silicon + Linux
x64 + Windows; enabled via `Settings > Playback > Embedded MPV: frame-copy
engine` (restart required) or