Skip to content

feat(scene)!: stinger transitions - #52

Merged
kixelated merged 13 commits into
mainfrom
quest/m0/stinger
Nov 5, 2026
Merged

kixelated merged 13 commits into
mainfrom
quest/m0/stinger

Conversation

@kixelated

@kixelated kixelated commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Finishes quest/m0/stinger.md.

Problem

A scene change could fade, slide, wipe, or move (moq-dev/studio#33), but not run a stinger: a clip played over the program that switches scenes at its cut point, as broadcast graphics do. The media file source (moq-dev/studio#44) can play a clip on demand, but nothing tied that play to a transition, so a cut point would land wherever decoding happened to start. Nor could a media source be stopped, so a stinger cut short had nothing to silence its clip with.

Approach

  • Scene graph: Transition::Stinger { source, cut } sits beside the other kinds. source names a Source::Media; cut is how far into the clip the scenes switch. Collection::validate refuses a stinger naming anything but a media source, and Draft::remove_source sets the default fade back when it removes the stinger's source, undoing whole. The settings change through Draft::set_transition, so they undo like any edit. Transition is no longer Copy (it holds a name); Transition::duration and duration_mut return Option, None for a stinger, which lasts as long as its clip.
  • Timing on the clip's own clock: Feed::run reports the media source's latest play (source::Run): which play it is, when its first picture or sound was due (begun), and when it ended (ended: its end, a looping clip's first pass, or a stop). Feed::play counts plays before the driver hears of them, and the driver writes only the run of the play it is in, and nothing once it ended, so a play cut off by the next or stopped never reports for it. The compositor switches the scenes once cut has passed since begun and ends the transition at ended. The cut point therefore lands on the same frame of the clip however long the decoder takes to open.
  • Stop: Feed::stop, beside Feed::play, stops a media source at once: it blanks the picture, ends the play under way, and the driver drops the source's bus channel, so no sound it queued plays, then rests ended until the next play. The feed sends the driver a cue (play or stopped) over the watch that carried the play count; a stinger's source opens on a stopped cue, which replaces the driver's held flag. The bus channel is now taken at the start of each play rather than at open. The driver shows a picture only while its play is the latest and not stopped, checked under the lock Feed::play and Feed::stop hold while they blank the picture, so a play cut off never shows a picture over the next one's blank, whichever thread runs first.
  • Compositor: the first render of a stinger calls Feed::play (before pulling pictures, so the frame never shows where the last stinger ended). Until the cut it draws the outgoing scene, then the incoming one, with the clip drawn over the whole canvas and the top layer over that. The clip fills each canvas cropped to that canvas's shape (cover), after the source's own crop and through its filters, so a green-screen or luma-keyed clip works. The stinger's progress is computed once per render in Compositor::progress, before any canvas is drawn, and the per-canvas draw (moq-dev/studio#42) only reads it, so every canvas switches on the same frame of the clip. A stinger interrupted by a Cut or another transition stops its clip (unless something else played it since), so it leaves no sound. One that plays to its end leaves its clip ended on its last picture, as a media file that played once rests, except that a looping clip stops after its first pass, so its sound ends with the transition. A clip that cannot play, fails, or is played again from elsewhere or reloaded mid-stinger cuts at once with a warning; one stopped from elsewhere ends the stinger. Compositor::stinger names the clip under way.
  • Sources: the media source a stinger plays opens stopped, blank and silent until its first play, rather than playing once, sound included, when the studio starts.
  • Engine: State::shown counts the stinger's clip while it plays, and the masks its source's filters read, so its own track encodes as any shown source's. Health no longer counts the stinger's clip as ended, since it rests ended between stingers, unless a scene on air places it (State::placed).
  • UI: the transition picker offers stinger once there is a media source, with a source picker and a "cut at" field in ms. It remembers the last duration and cut point across kinds. The Sources panel gets Stop beside Play for a media file.

Performance: during a stinger the program draws one more item per canvas, the clip, with its source's filters; no extra GPU copy and no encoder change. The clip decodes like any media source, one decoder per play. A stop costs one bus channel drop.

Impact

  • Scene file: [transition] gains kind = "stinger" with source and cut. Additive; no migration.
  • Program panel: stinger in the transition picker, its source and cut point beside it. A Cut or another transition mid-stinger stops the clip, sound included. A stinger played to its end leaves its clip holding its last frame; a looping clip stops after one pass.
  • Sources panel: Stop for a media file, blank and silent until played again.
  • A media source named by the stinger no longer plays when it opens; it waits for the first stinger.
  • Health: the stinger's clip resting ended is not a problem unless a scene on air places it.
  • API (moq-scene), breaking:
    • Compositor::cut(&Sources) and Compositor::transition(Transition, &Sources, Instant) take the sources, to stop a stinger's clip. Migrate by passing the Sources given to render.
    • Transition is Clone but not Copy; Transition::duration and duration_mut return Option (None for a stinger). Migrate with .clone() and by handling None.
    • Additive: Transition::Stinger, Transition::stinger, source::Run, Feed::run, Feed::stop, Compositor::stinger.
  • README.md documents the stinger under Transitions, Media files, and the scene file, and Stop in the Sources panel.

Decisions

  • Mid-stinger Cut or other transition, 2026-11-05 (maintainer):
    • ✅ Iterate: add stop here. Feed::stop silences and blanks the clip at once and drops its queued sound; the Sources panel gets Stop; a Cut or another transition mid-stinger uses it.
    • Land as is, with the clip's sound playing out and a follow-up for stop.
  • This supersedes the earlier "known gap" choice, which let the sound play out; the README note about it is gone.
  • A stinger that plays to its end, 2026-11-05 (maintainer):
    • ✅ Iterate: hold last frame. Only an interrupted stinger stops its clip; one that plays to its end leaves the clip ended and holding its last frame. A looping clip still stops at the end of its first pass, so no sound outlives the transition.
    • Stop the clip at every stinger end, blanking its last frame (what 709575c did).
  • This supersedes the earlier choice, listed without asking, to stop the clip at every stinger's end.

Choices made without asking

  • The stinger lasts as long as its clip's first play rather than a set duration, as OBS does. A looping clip ends after one pass.
  • The cut point counts from the clip's first due picture or sound (begun), not from the button press, so decoder start-up never moves it.
  • The clip covers each canvas cropped to its shape rather than stretched, so one clip works on a landscape and a portrait canvas.
  • The top layer stays over the stinger, as it stays over every transition.
  • The incoming scene's keyframes and restart media sources start as the transition starts, as with every other transition (and OBS), not at the cut point.
  • A looping clip is stopped, and so blanked, at the end of its first pass, rather than given a one-shot play that would hold its last frame: a second play mode only the stinger would use.
  • A stop from the Sources panel mid-stinger ends the stinger at once onto the incoming scene, without a warning, since it was asked for.
  • Feed::stop succeeds on a media source whose driver is gone, which plays nothing anyway; only another kind of source refuses it.
  • The stinger's source reads "ended" in the Sources panel between stingers, which is true; only Health ignores it.
  • A new stinger's cut point defaults to 500 ms in the picker.

Alternatives

  • A user-set duration beside the cut point: simpler compositor, but a mismatch with the clip either cuts the clip off or holds its last picture over the program.
  • Timing the cut from the transition's start: a decoder that takes 100 ms to open would switch the scenes 100 ms before the clip covers them.
  • A per-source "wait for play" flag in the scene file instead of holding the stinger's source: one more knob for a behavior only a stinger wants.
  • Stopping a cut-off stinger at the next render instead of passing Sources to cut and transition: no signature change, but a frame more of sound and one more piece of compositor state.
  • A one-shot play for a looping stinger instead of stopping it at the stinger's end: a second play mode only the stinger would use.

Validation

On macOS (Apple GPU, Metal), at 8f91eb9 (8f91eb9217cff4d76025253478b364e2b40df53d):

  • just ci passes: lints for both feature sets, quest check, and 236 tests.
  • GPU tests pass: compositor::tests (34) and engine::tests::the_program_* (4, without openh264).
  • CI on e3b4a52 failed source::media_tests::a_stinger_clip_waits_for_its_play on Linux: a picture of the play cut off showed after Feed::play blanked it (the driver's select! could pick its due picture over the new cue). It did not reproduce on macOS in 200 runs, where decoding is ordered differently; f5c15e5 removes the race at its root (see Stop above).
  • New and extended tests:
    • collection::tests::reads_a_stinger: the scene-file shape round-trips, and a stinger naming a missing source is refused.
    • edit::tests::a_stinger_follows_its_source: settings undo, a non-media source is refused, and removing the source sets the fade back, undoing whole.
    • source::media_tests::a_stinger_clip_waits_for_its_play (paused clock): the held clip plays nothing until asked; run reports begun and ended, and forgets a play cut off by the next.
    • source::media_tests::a_stinger_clip_asked_while_opening_plays (paused clock): a play asked for while the file opens is not lost.
    • source::media_tests::a_stopped_media_file_goes_blank_and_silent (paused clock, bus advanced with the clock): Stop on its own blanks the picture at once, ends the run there, leaves the program mix silent with nothing queued playing, and the next play starts from the beginning; a color source refuses it. Fails if the stop keeps the channel.
    • compositor::tests::a_stinger_cuts_under_its_clip (GPU): the clip covers the canvas, the outgoing scene shows until 1 ms before the cut and the incoming one from the cut, the transition ends with the clip, the clip then rests ended holding its last picture (fails if the stinger's end stops it), and the next stinger starts blank rather than on the last one's end.
    • compositor::tests::a_stinger_stops_its_clip (GPU): a Cut mid-stinger leaves no clip sound on the program mix (fails without the stop in cut), a stop from elsewhere ends the stinger onto the incoming scene, and a looping clip stops, silent, with its stinger after one pass.
    • compositor::tests::a_stinger_that_cannot_play_cuts (GPU): an unreadable clip, or one played again mid-stinger, cuts at once.
    • compositor::tests::a_stinger_covers_the_canvas: the cover crop for portrait, landscape, and a pre-cropped source.
    • engine::tests::the_program_rests_a_stinger_clip (GPU): Health ignores the resting clip but still counts another ended media file, and counts the clip once a scene on air places it; the program shows the clip and the mask source its filters read while the stinger plays.

Online reviews

On b362246, all addressed in 709575c:

  • OpenAI review and pullfrog: a looping stinger's sound looped on after the transition. Fixed: the stinger's end stops its clip.
  • OpenAI review and pullfrog: State::shown left out the masks the stinger clip's filters read. Fixed, and tested.
  • pullfrog: begun + cut could overflow on a huge cut point. Fixed with saturating_duration_since.
  • pullfrog: Health hid Ended for a stinger clip a scene also places. Fixed: only an unplaced clip is ignored, and tested.
  • CodeRabbit: rate-limited, no review.

On d53206e: pullfrog confirmed the four fixed and noted Feed::play's doc still said it refuses "a media source that stopped". Fixed in e3b4a52: it refuses only one whose driver is gone.

Review

Codex (quest review), gpt-6.1-sol, medium effort.

  • Earlier session, before the stop decision:
    • Round 1, head d2b3a68: 2 findings, both fixed in b362246 (a stinger asked to play while its file opened never ended; Compositor::shown left out the clip and its masks).
    • Round 2, head b362246: no findings.
  • New session (--new) after the design change adding Feed::stop:
    • Round 1, head 709575c, base 4811cb8: no findings.
    • Round 2, head 7794f99, base fce9c4c: 1 finding, fixed in f5c15e5. source.rs: preferring the new cue in select! and blanking again still let another thread show a cut-off play's picture after Feed::play blanked it; the driver now shows a picture only while its play is the latest and not stopped, under the lock the feed takes to blank it.
    • Round 3, head 3705cda, base fce9c4c: no findings.
  • New session (--new) after the design change to hold the last frame:

🤖 Generated with Claude Code

kixelated and others added 4 commits November 5, 2026 00:37
Quest-Claim: 019fffac101070e9
A stinger plays a media source's clip over the program from its start and
switches scenes under it at its cut point, timed from the clip's own first
picture or sound, lasting as long as the clip. The clip covers each canvas
it is drawn on, cropped to the canvas's shape, through its source's crop
and filters. Its source waits for the first stinger rather than playing
once as it opens, and a clip that cannot play cuts at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts its clip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aints what its filters read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Stinger transitions feat(scene): stinger transitions Nov 5, 2026
@kixelated
kixelated marked this pull request as ready for review November 5, 2026 09:08
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 58d62243-82fb-44f1-b324-aac916c90d7b
Commits

Reviewing files that changed from the base of the PR and between 8d8c5ea and 8f91eb9.

Files selected for processing: 11

  • README.md
  • quest/m0/README.md
  • quest/m0/stinger.md
  • rs/moq-scene/src/collection.rs
  • rs/moq-scene/src/compositor.rs
  • rs/moq-scene/src/edit.rs
  • rs/moq-scene/src/effect.rs
  • rs/moq-scene/src/source.rs
  • rs/moq-studio/src/engine.rs
  • rs/moq-studio/src/ui.rs
  • rs/moq-studio/tests/browser.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated kixelated left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: b362246

  1. [P2] Stop looping stinger audio after the first pass — rs/moq-scene/src/source.rs:1333–1337. A looping stinger records an end after its first pass, so the compositor removes it visually, but the source immediately loops again and its audio remains routed. The transition can finish while its sound repeats indefinitely. Make stinger playback one-shot or stop the source at the reported end; test a looping clip with audio.
  2. [P2] Include stinger mask sources in on-program tracking — rs/moq-studio/src/engine.rs:223–227. State::shown adds the clip but not masks used by its source filters. An otherwise unplaced restartable media mask therefore is not restarted or treated as on-program, so the stinger can use its held final frame instead of the mask animation. Include those masks and test this case.

The stinger integration follows the existing transition model, but its source lifecycle needs to match the visible transition. Verification: GitHub-only static review; no GPU/audio tests run. Hosted Check was still running when inspected.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two source-lifecycle bugs mean a stinger can outlive its transition (looping clips loop audibly forever, and a stinger's mask sources are never restarted), plus two smaller correctness issues (an overflow panic on a large cut, and Health hiding a genuinely ended placed clip). All four are inline below.

Reviewed changes

Reviewed the full stinger-transition feature against b362246: the new Transition::Stinger scene type, its playback in the compositor and source driver, on-program/Health tracking, and the UI, docs, and quest updates.

  • Transition::Stinger type — adds { source, cut }, validates that the source is Media, and exposes duration()/stinger() helpers.
  • Source playback — Feed::play() drives one clip per transition and reports Run.begun/Run.ended for the compositor's cut point and end.
  • Compositor — plays the clip over the program, switches scenes at cut, covers the canvas without stretching, and reports the clip as shown while under way.
  • Engine — includes the stinger clip in State::shown (so it is published/encoded) and suppresses its resting Ended in Health.
  • UI — offers the stinger transition once a media source exists, remembers the last cut, and renders its controls.
  • Docs and quest — README Transitions/Media/Health and the scene-file example; deletes the completed quest/m0/stinger.md.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using big-pickle (free) | 𝕏

Comment thread rs/moq-scene/src/source.rs Resolved
Comment thread rs/moq-studio/src/engine.rs Resolved
Comment thread rs/moq-scene/src/compositor.rs Resolved
Comment thread rs/moq-studio/src/engine.rs Resolved
kixelated and others added 3 commits November 5, 2026 07:22
The stinger's draw moves into the per-canvas draw multi-canvas added: its
progress is computed once per render, so every canvas switches on the same
frame of the clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A media source stops at once: blank, silent, and with the sound it queued
dropped, until played again. The Sources panel gets a Stop button. A Cut or
another transition mid-stinger stops the clip, as does the stinger's own
end, so a looping clip plays one pass.

Also from review: the program counts the masks a stinger clip's filters
read, Health counts the clip ended once a scene on air places it, and a
large cut point no longer overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(scene): stinger transitions feat(scene)!: stinger transitions Nov 5, 2026
@kixelated

Copy link
Copy Markdown
Contributor Author

Design change: per the maintainer's decision of 2026-11-05 ("Iterate: add stop here"), media sources get Feed::stop, which blanks the clip, ends its play, and drops its queued sound. The Sources panel has Stop beside Play. A stinger stops its clip when it ends, is cut, or is replaced by another transition, so a looping clip plays one pass and a cut-short stinger leaves no sound. Compositor::cut and Compositor::transition now take &Sources to do it. The next local review started a new session (quest review --new).

Also on b362246: the OpenAI review's two findings (looping stinger audio, stinger mask sources missing from State::shown) are fixed in 709575c, along with pullfrog's four inline findings. Each thread has its own reply. CodeRabbit was rate-limited and posted no review.

(Written by Claude Opus 5.5)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — the four earlier findings are fixed; one stale doc line worth a reword.

Reviewed changes

Reviewed the delta since the prior pullfrog review (b362246 → d53206e): the stoppable media play and the extended stinger lifecycle, plus the merge of main.

  • Added Feed::stop over a Cue { play, stopped } channel, so a media source stops blank and silent and plays again from its start, recording Run.ended.
  • Stopped a stinger's clip when its transition ends, is cut, or is replaced, via Switch::stop, keeping a looping clip to one pass; Compositor::cut and transition now take &Sources.
  • Included the stinger clip's source-filter masks in State::shown with the shared masks() helper.
  • Compared the cut point with saturating_duration_since, removing the Instant overflow panic.
  • Restricted Health's Ended suppression to a stinger clip no scene on air places, through State::placed.
  • Added Sources Play/Stop controls and documented stoppable media and stinger end/cut behavior in the README.

ℹ️ Feed::play's docs now contradict themselves about a stopped source

play's new sentence says a source that ended or stopped plays again, matching Feed::stop and the README, but the retained line just below still says play is "Refused ... by a media source that stopped". play only refuses once the driver is gone (cues.is_closed()), so a reader may wrongly believe the Stop button is terminal.

Technical details
# `Feed::play` docs say a stopped source both plays again and is refused

## Affected sites
- `rs/moq-scene/src/source.rs:425-429` — the added "One that ended or stopped plays again" sits directly above the pre-existing "Refused ... by a media source that stopped".
- `rs/moq-scene/src/source.rs` (`Feed::play`) — the only refusal besides a non-media source is `cues.is_closed()`, i.e. the driver is gone.
- `rs/moq-scene/src/source.rs` (`Feed::stop`) and the README — both say a stopped source plays again on the next play.

## Required outcome
- The refusal line names the actual condition, such as "once its driver is gone" or "whose driver stopped or failed", rather than "a media source that stopped".

Pullfrog  | Fix it ➔ | View workflow run | Using big-pickle (free) | 𝕏

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

kixelated commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Pullfrog's note on d53206e is fixed in e3b4a52: Feed::play's doc now says it refuses only a media source whose driver is gone, matching cues.is_closed(). A stopped source plays again.

(Written by Claude Opus 5.5)

@pullfrog

pullfrog Bot commented Nov 5, 2026

Copy link
Copy Markdown

Confirmed in e3b4a52: Feed::play's doc now says it refuses only a media source "whose driver is gone, such as after it failed", matching the cues.is_closed() check in rs/moq-scene/src/source.rs:429-435. A stopped source plays again, so the docs, Feed::stop, and the README now agree.

No further change needed. That note was in a review body with no inline thread, so there is nothing to resolve; the four earlier inline threads are all resolved, and the tree is clean.

Pullfrog  | View workflow run | via Pullfrog | Using big-pickle (free) | 𝕏

kixelated and others added 4 commits November 5, 2026 08:43
The media driver raced a play or stop against its own next picture: its
select! picked at random, so a picture due as a new play came could show
after the feed blanked it, flashing the play cut off. CI caught it in
a_stinger_clip_waits_for_its_play. The cue now wins, and the driver blanks
the picture again on every new cue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select order and a second blank still let a picture of the play cut off
slip past the feed's blank from another thread. The driver now shows a
picture only while its play is the latest and not stopped, checked under
the lock the feed takes to start a play or stop and blank the picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	rs/moq-scene/src/effect.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@kixelated kixelated left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit 3e37f61. No new actionable findings. Both earlier stinger findings remain addressed. The new shared-lock generation check closes the stale-picture race between publication and play/stop, and the merge preserves stinger dimensions alongside premultiplied-alpha changes. This synchronization is a reasonable direction.

Verification: GitHub-only static review; I did not run GPU or audio tests.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior pullfrog review: the media driver's playback-state race fix, plus the merges of main into the branch.

  • Guarded media playback against the feed — the driver now shows a picture only for the latest play and only while not stopped, checked under the same lock Feed::play/Feed::stop blank the picture with, so a play cut off by another play or by Stop can no longer flash a frame after the blank (root fix for the Linux CI failure in a_stinger_clip_waits_for_its_play).
  • Dropped the earlier biased select! and the driver-side re-blank, now redundant with that guard.
  • Merged main (recording repair), and in the earlier merge resolved the effect.rs conflict by keeping the PR's Picture::size() alongside main's premultiplied-sampling change.

Pullfrog  | View workflow run | Using big-pickle (free) | 𝕏

Per the maintainer's pick, only an interrupted stinger (a Cut or another
transition) stops its clip. One that plays to its end leaves the clip
ended on its last picture, as a media file that played once rests; a
looping clip still stops after its first pass so no sound outlives the
transition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Contributor Author

Design change: per the maintainer's pick "Iterate: hold last frame", only an interrupted stinger (a Cut or another transition mid-stinger) stops its clip. A stinger that plays to its end leaves the clip ended and holding its last frame, as a media file that played once does. A looping clip still stops, blank and silent, at the end of its first pass, so no sound outlives the transition. Done in 8f91eb9; the next local review starts a new session (quest review --new).

(Written by Claude Opus 5.5)

@kixelated kixelated left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit 8f91eb9. No new actionable findings since 3e37f61. Retaining a non-looping stinger's final picture matches the documented decision while looping and interrupted playback still stop; the added regression covers retention and clearing on the next stinger.

Verification: GitHub-only static review. I did not run tests or GPU execution.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior pullfrog review (3e37f61 → 8f91eb9): what a stinger leaves behind when its clip plays to its end.

  • Let a non-looping stinger clip play out to its end and rest on its last picture instead of stopping it, so it behaves like any media file that played once; a looping clip still stops, blank and silent, after its first pass so no sound outlives the transition.
  • Replaced the unconditional Switch::stop on every ended transition with the looping-only stop in Compositor::progress, which is the only path that can now blank a clip at its end.
  • Reworded the Switch::stop, Compositor::transition, and Compositor::progress docs and the README Transitions paragraph to match.
  • Extended a_stinger_cuts_under_its_clip to assert the clip's last picture is held after the transition ends and that the next stinger still starts blank.

Pullfrog  | View workflow run | Using big-pickle (free) | 𝕏

@kixelated
kixelated merged commit 5709ef7 into main Nov 5, 2026
3 checks passed
@kixelated
kixelated deleted the quest/m0/stinger branch November 5, 2026 18: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