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>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-03 20:57:45 +02:00
1 parent 078bd07d94
commit ae85428065
18 files changed
+300 -7

No files matched your search

@@ -1153,6 +1153,19 @@ export interface ElectronBridgeApi {
sessionId: string,
seconds: number
) => Promise<EmbeddedMpvSession | null>;
/**
* Relative seek by `deltaSeconds` (negative = backwards), resolved by mpv
* against its own playback position. Keyboard and button steps must use
* this instead of `seekEmbeddedMpv(position + delta)`: the renderer's
* `positionSeconds` is a whole-second snapshot refreshed at most every
* 500 ms and a seek reply does not carry the new position yet, so rapid
* presses computed from it collapse onto one target. mpv merges queued
* relative seeks instead, so presses accumulate.
*/
seekEmbeddedMpvBy?: (
sessionId: string,
deltaSeconds: number
) => Promise<EmbeddedMpvSession | null>;
setEmbeddedMpvVolume: (
sessionId: string,
volume: number