From 9cba7d55835170b17e0ab7a69568024f26d4d9b9 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 19:17:03 -0500 Subject: [PATCH] VideoPlayer: add playback_restart Signal for state machine adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- booru_viewer/gui/media/video_player.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/booru_viewer/gui/media/video_player.py b/booru_viewer/gui/media/video_player.py index d9692f5..47846bb 100644 --- a/booru_viewer/gui/media/video_player.py +++ b/booru_viewer/gui/media/video_player.py @@ -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