Repository navigation
feat(scene)!: stinger transitions - #52
Conversation
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>
|
Warning Review limit reachedYou'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. View limit details
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. |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed commit: b362246
- [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. - [P2] Include stinger mask sources in on-program tracking —
rs/moq-studio/src/engine.rs:223–227.State::shownadds 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.
There was a problem hiding this comment.
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::Stingertype — adds{ source, cut }, validates that the source isMedia, and exposesduration()/stinger()helpers.- Source playback —
Feed::play()drives one clip per transition and reportsRun.begun/Run.endedfor 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 restingEndedin 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.
big-pickle (free) | 𝕏
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>
|
Design change: per the maintainer's decision of 2026-11-05 ("Iterate: add stop here"), media sources get Also on b362246: the OpenAI review's two findings (looping stinger audio, stinger mask sources missing from (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
ℹ️ 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::stopover aCue { play, stopped }channel, so a media source stops blank and silent and plays again from its start, recordingRun.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::cutandtransitionnow take&Sources. - Included the stinger clip's source-filter masks in
State::shownwith the sharedmasks()helper. - Compared the cut point with
saturating_duration_since, removing theInstantoverflow panic. - Restricted Health's
Endedsuppression to a stinger clip no scene on air places, throughState::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".big-pickle (free) | 𝕏
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Confirmed in 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.
|
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
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
✅ 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::stopblank 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 ina_stinger_clip_waits_for_its_play). - Dropped the earlier
biasedselect!and the driver-side re-blank, now redundant with that guard. - Merged
main(recording repair), and in the earlier merge resolved theeffect.rsconflict by keeping the PR'sPicture::size()alongside main's premultiplied-sampling change.
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>
|
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 ( (Written by Claude Opus 5.5) |
kixelated
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
✅ 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::stopon every ended transition with the looping-only stop inCompositor::progress, which is the only path that can now blank a clip at its end. - Reworded the
Switch::stop,Compositor::transition, andCompositor::progressdocs and the README Transitions paragraph to match. - Extended
a_stinger_cuts_under_its_clipto assert the clip's last picture is held after the transition ends and that the next stinger still starts blank.
big-pickle (free) | 𝕏

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
Transition::Stinger { source, cut }sits beside the other kinds.sourcenames aSource::Media;cutis how far into the clip the scenes switch.Collection::validaterefuses a stinger naming anything but a media source, andDraft::remove_sourcesets the default fade back when it removes the stinger's source, undoing whole. The settings change throughDraft::set_transition, so they undo like any edit.Transitionis no longerCopy(it holds a name);Transition::durationandduration_mutreturnOption,Nonefor a stinger, which lasts as long as its clip.Feed::runreports 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::playcounts 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 oncecuthas passed sincebegunand ends the transition atended. The cut point therefore lands on the same frame of the clip however long the decoder takes to open.Feed::stop, besideFeed::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 (playorstopped) over the watch that carried the play count; a stinger's source opens on a stopped cue, which replaces the driver'sheldflag. 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 lockFeed::playandFeed::stophold while they blank the picture, so a play cut off never shows a picture over the next one's blank, whichever thread runs first.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 inCompositor::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::stingernames the clip under way.State::showncounts 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).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
[transition]gainskind = "stinger"withsourceandcut. Additive; no migration.Compositor::cut(&Sources)andCompositor::transition(Transition, &Sources, Instant)take the sources, to stop a stinger's clip. Migrate by passing theSourcesgiven torender.TransitionisClonebut notCopy;Transition::durationandduration_mutreturnOption(Nonefor a stinger). Migrate with.clone()and by handlingNone.Transition::Stinger,Transition::stinger,source::Run,Feed::run,Feed::stop,Compositor::stinger.README.mddocuments the stinger under Transitions, Media files, and the scene file, and Stop in the Sources panel.Decisions
Feed::stopsilences 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.Choices made without asking
begun), not from the button press, so decoder start-up never moves it.restartmedia sources start as the transition starts, as with every other transition (and OBS), not at the cut point.Feed::stopsucceeds on a media source whose driver is gone, which plays nothing anyway; only another kind of source refuses it.Alternatives
Sourcestocutandtransition: no signature change, but a frame more of sound and one more piece of compositor state.Validation
On macOS (Apple GPU, Metal), at 8f91eb9 (
8f91eb9217cff4d76025253478b364e2b40df53d):just cipasses: lints for both feature sets,quest check, and 236 tests.compositor::tests(34) andengine::tests::the_program_*(4, withoutopenh264).source::media_tests::a_stinger_clip_waits_for_its_playon Linux: a picture of the play cut off showed afterFeed::playblanked it (the driver'sselect!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).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;runreportsbegunandended, 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 incut), 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:
State::shownleft out the masks the stinger clip's filters read. Fixed, and tested.begun + cutcould overflow on a huge cut point. Fixed withsaturating_duration_since.Endedfor a stinger clip a scene also places. Fixed: only an unplaced clip is ignored, and tested.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.Compositor::shownleft out the clip and its masks).--new) after the design change addingFeed::stop:source.rs: preferring the new cue inselect!and blanking again still let another thread show a cut-off play's picture afterFeed::playblanked 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.--new) after the design change to hold the last frame:🤖 Generated with Claude Code