Skip to content

Stop reporting playback on a clip that ended during the play call - #75

Merged
danielchalmers merged 1 commit into
mainfrom
claude/playing-state-after-clip-end
Sep 17, 2026
Merged

danielchalmers merged 1 commit into
mainfrom
claude/playing-state-after-clip-end

Conversation

@danielchalmers

@danielchalmers danielchalmers commented Aug 30, 2026 •

Copy link
Copy Markdown
Owner

PlayAsync sets IsPlaying once its play operation completes, with nothing checking whether the clip is still running by then. A clip that reaches its end while that operation is in flight raises Ended, whose handler clears IsPlaying, and then the play call sets it straight back. The transport is left claiming to play a clip parked on its last frame until you press something else.

Evidence, re-measured on current main

Traced on the real stack (FlyleafLib 3.11.5) with a clip paused just before its end, seeked, and played straight away:

play-enter
+28ms  Flyleaf PlaybackStopped, status Ended   <- the clip finishes inside the play call
+30ms  controller IsPlaying=True               <- the play call then asserts playback
play-return

The Ended handler did run. Its IsPlaying = false just raised nothing, because the value was already false.

without guard with guard
seek near the end, play immediately 3/40 wrong 0/80 wrong (two runs)
same, human-paced 300ms gap 0/40 0/80

Rare by hand, since the window is only as long as the play call, and a play call is only slow while Flyleaf still has a seek in flight. The invariant is still wrong, though, and anything that makes play wait on a pending seek widens exactly this window.

A correction

The original description said the window came from "a play queued behind an open still joining secondary cameras, which took eight seconds". That was wrong. Those measurement runs still had the seek-settled gate from the Flyleaf 9.0 work, which waited up to 2s per player in the speed setter, and PlayAsync applies speed to 4 players: 4 × 2s = 8s. The gate was deleted before #74 merged. So the original trace overstated how reachable this is, and the numbers above are the honest ones.

The fix

A counter bumped by the Ended handler lets the play call tell that the clip finished underneath it and leave the settled state alone. FakeCameraPlayer gains a PlayCallback hook, mirroring its existing SeekCallback.

Two tests: one ends the clip from inside the play call and asserts the controller does not report playback, one asserts an ordinary resume still does. Verified the first fails without the guard.

Not in scope

This does not fix pressing play on a clip that has already finished, which can also leave the video frozen while the transport reports playback. That's a separate race between Flyleaf's asynchronous seek and its Play(), and it predates v1.0.0.

Rebased onto 9e2f466. 374 tests green, formatting clean.

PlayAsync sets IsPlaying once its play operation completes, with nothing checking whether the clip is still running by then.
A clip that reaches its end while that operation is in flight raises Ended, whose handler clears IsPlaying, and then the play call sets it straight back.
The transport is left claiming to play a clip parked on its last frame, and nothing clears it until the user presses something else.

Traced on the real stack, with a clip paused just before its end, seeked, and played straight away:

  play-enter
  +28ms  Flyleaf PlaybackStopped, status Ended   (the clip finishes inside the play call)
  +30ms  controller IsPlaying=True               (the play call then asserts playback)
  play-return

The Ended handler did run; its IsPlaying=false simply raised nothing, because the value was already false.

That sequence failed 3 of 40 iterations on current main.
With a human-paced gap of 300ms between the seek and the play it failed 0 of 40, because the window is only as long as the play call, and a play call is only slow while Flyleaf still has a seek in flight.
It is rare by hand, but the invariant is plainly wrong, and anything that makes play wait on a pending seek widens exactly this window.

A counter bumped by the Ended handler lets the play call tell that the clip finished underneath it and leave the settled state alone.
FakeCameraPlayer gains a PlayCallback hook, mirroring the SeekCallback it already had for interleaving an action mid-call.

With the guard, the same sequence passed 80 of 80 across two runs.
Two unit tests cover it: one ends the clip from inside the play call and asserts the controller does not report playback, one asserts an ordinary resume still does.
Verified the first fails without the guard.

This does not fix pressing play on a clip that has already finished, which can also leave the video frozen while the transport reports playback; that is a separate race inside Flyleaf's seek and play handling.
374 tests green.
@danielchalmers
danielchalmers force-pushed the claude/playing-state-after-clip-end branch from 25dbffb to 62cd391 Compare September 17, 2026 05:06
@danielchalmers
danielchalmers merged commit cb4c769 into main Sep 17, 2026
1 check passed
@danielchalmers
danielchalmers deleted the claude/playing-state-after-clip-end branch September 17, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant