VideoPlayer: add playback_restart Signal for state machine adapter

Adds a Qt Signal that mirrors mpv's `playback-restart` event. The
upcoming popout state machine refactor (Prompt 3) needs a clean,
event-driven "seek/load completed" edge to drive its SeekingVideo →
PlayingVideo and LoadingVideo → PlayingVideo transitions, replacing
the current 500ms timestamp suppression window in `_seek_pending_until`.

mpv's `playback-restart` fires once after each loadfile (when playback
actually starts producing frames) and once after each completed seek.
Empirically verified by the pre-commit-1 probe in
docs/POPOUT_REFACTOR_PLAN.md: a load + 2 seeks produces exactly 3
events, with `seeking=False` at every event (the event represents the
completion edge, not the in-progress state).

The state machine adapter will distinguish "load done" from "seek done"
by checking the state machine's current state at dispatch time:
- `playback-restart` while in LoadingVideo → VideoStarted event
- `playback-restart` while in SeekingVideo → SeekCompleted event

Implementation:

- One Signal added near the existing play_next / media_ready /
  video_size definitions, with a doc comment explaining what fires
  it and which state machine consumes it.
- One event_callback registration in `_ensure_mpv` (alongside the
  existing observe_property calls). The callback runs on mpv's
  event thread; emitting a Qt Signal is thread-safe and the
  receiving slot runs on the GUI thread via Qt's default
  AutoConnection (sender and receiver in the same thread by the
  time the popout adapter wires the connection).
- The decorator-based `@self._mpv.event_callback(...)` form is used
  to match the rest of the python-mpv idioms in the file. The inner
  function name `_emit_playback_restart` is local-scoped — mpv keeps
  its own reference, so there's no leak from re-creation across
  popout open/close cycles (each popout gets a fresh VideoPlayer
  with its own _ensure_mpv call).

This is the only commit in the popout state machine refactor that
touches `media/video_player.py`. All subsequent commits land in
`popout/` (state.py, effects.py, hyprland.py, window.py) or
`gui/main_window.py` interface updates. 21 lines added, 0 removed.

Verification:
- Phase A test suite (16 tests) still passes
- Module imports cleanly with the new Signal in place
- App launches without errors (smoke test)

Test cases for state machine adapter (Prompt 3 popout/state.py):
- VideoPlayer.playback_restart fires once on play_file completion
- VideoPlayer.playback_restart fires once per _seek call
This commit is contained in:
pax 2026-04-08 19:17:03 -05:00
parent bf14466382
commit 9cba7d5583

View File

@ -36,6 +36,17 @@ class VideoPlayer(QWidget):
play_next = Signal() # emitted when video ends in "Next" mode
media_ready = Signal() # emitted when media is loaded and duration is known
video_size = Signal(int, int) # (width, height) emitted when video dimensions are known
# Emitted whenever mpv fires its `playback-restart` event. This event
# arrives once after each loadfile (when playback actually starts
# producing frames) and once after each completed seek. The popout's
# state machine adapter listens to this signal and dispatches either
# VideoStarted or SeekCompleted depending on which state it's in
# (LoadingVideo vs SeekingVideo). The pre-state-machine code did not
# need this signal because it used a 500ms timestamp window to fake
# a seek-done edge; the state machine refactor replaces that window
# with this real event. Probe results in docs/POPOUT_REFACTOR_PLAN.md
# confirm exactly one event per load and one per seek.
playback_restart = Signal()
# QSS-controllable letterbox / pillarbox color. mpv paints the area
# around the video frame in this color instead of the default black,
@ -246,6 +257,16 @@ class VideoPlayer(QWidget):
self._mpv.observe_property('duration', self._on_duration_change)
self._mpv.observe_property('eof-reached', self._on_eof_reached)
self._mpv.observe_property('video-params', self._on_video_params)
# Forward mpv's `playback-restart` event to the Qt-side signal so
# the popout's state machine adapter can dispatch VideoStarted /
# SeekCompleted events on the GUI thread. mpv's event_callback
# decorator runs on mpv's event thread; emitting a Qt Signal is
# thread-safe and the receiving slot runs on the connection's
# target thread (typically the GUI main loop via the default
# AutoConnection from the same-thread receiver).
@self._mpv.event_callback('playback-restart')
def _emit_playback_restart(_event):
self.playback_restart.emit()
self._pending_video_size: tuple[int, int] | None = None
# Push any QSS-set letterbox color into mpv now that the instance
# exists. The qproperty-letterboxColor setter is a no-op if mpv