pax 7fdc67c613 popout/state: implement PlayingVideo + LoadingVideo + EOF race fix
First batch of real transitions. The EOF race fix is the headline —
this commit replaces fda3b10b's 250ms _eof_ignore_until timestamp
window with a structural transition that drops VideoEofReached in
every state except PlayingVideo.

Transitions implemented:

  ContentArrived(VIDEO) in any state →
    LoadingVideo, [LoadVideo, FitWindowToContent]
    Snapshots current_path/info/kind/width/height. Flips
    is_first_content_load to False (the saved_geo seeding lands in
    commit 8). Image and GIF kinds are still stubbed — they get
    DisplayingImage in commit 10.

  LoadingVideo + VideoStarted →
    PlayingVideo, [ApplyMute, ApplyVolume, ApplyLoopMode]
    The persistence effects fire on PlayingVideo entry, pushing the
    state machine's persistent values into mpv. This is the
    structural replacement for VideoPlayer._pending_mute's lazy-mpv
    replay (the popout layer now owns mute as truth; VideoPlayer's
    internal _pending_mute stays as defense in depth, untouched).

  PlayingVideo + VideoEofReached →
    Loop=NEXT: [EmitPlayNextRequested]
    Loop=ONCE: [] (mpv keep_open=yes pauses naturally)
    Loop=LOOP: [] (mpv loop-file=inf handles internally)

  *Anything* + VideoEofReached (not in PlayingVideo) →
    [], state unchanged
    **THIS IS THE EOF RACE FIX.** The fda3b10b commit added a 250ms
    timestamp window inside VideoPlayer to suppress eof events
    arriving from a previous file's stop. The state machine subsumes
    that by only accepting eof in PlayingVideo. In LoadingVideo
    (where the race lives), VideoEofReached is structurally invalid
    and gets dropped at the dispatch boundary. No window. No
    timestamp. No race.

  LoadingVideo / PlayingVideo + VideoSizeKnown →
    [FitWindowToContent(w, h)]
    mpv reports new dimensions; refit. Same effect for both states
    because the only difference is "did the user see a frame yet"
    (which doesn't matter for window sizing).

  PlayingVideo + TogglePlayRequested →
    [TogglePlay]
    Space key / play button. Only valid in PlayingVideo — toggling
    play during a load or seek would race with mpv's own state
    machine.

Tests passing after this commit (62 total → 35 pass, 27 fail):

  - test_loading_video_started_transitions_to_playing
  - test_loading_video_eof_dropped (RACE FIX!)
  - test_loading_video_size_known_emits_fit
  - test_playing_video_eof_loop_next_emits_play_next
  - test_playing_video_eof_loop_once_pauses
  - test_playing_video_eof_loop_loop_no_op
  - test_playing_video_size_known_refits
  - test_playing_video_toggle_play_emits_toggle
  - test_invariant_eof_race_loading_video_drops_stale_eof (RACE FIX!)
  - test_awaiting_content_arrived_video_transitions_to_loading
  - test_awaiting_content_arrived_video_emits_persistence_effects

Plus several illegal-transition cases for the (LoadingVideo, *)
events that this commit makes meaningfully invalid.

Phase A (16 tests in tests/core/) still green.

Tests still failing (27, scheduled for commits 5-11):
  - Open / NavigateRequested handlers (commit 5)
  - DisplayingImage transitions (commit 10)
  - SeekingVideo transitions (commit 6)
  - Closing transitions (commit 10)
  - Persistent viewport / drift events (commit 8)
  - mute/volume/loop persistence events (commit 9)
  - F11 round-trip (commit 7)

Test cases for commit 5 (Navigating + AwaitingContent + double-load):
  - dispatch NavigateRequested in PlayingVideo → AwaitingContent
  - second NavigateRequested in AwaitingContent doesn't re-stop
  - test_invariant_double_navigate_no_double_load
2026-04-08 19:32:04 -05:00
..