Skip to content

Hand a cued clip's warm decoder to Program (T1.11 / #449 step 2) - #492

Merged
iamfatness merged 1 commit into
mainfrom
fix/media-cue-handoff
Sep 12, 2026
Merged

iamfatness merged 1 commit into
mainfrom
fix/media-cue-handoff

Conversation

@iamfatness

Copy link
Copy Markdown
Owner

Closes #449 (step 2). docs/BACKLOG.md T1.11.

A clip taken from Preview to Program cold-started: a placeholder slab on air for the ticks before its first frame, and a take record reading rebuilt with missingSources=[media:<id>].

Why it cold-started

A clip changes identity twice on go-live:

Preview cue After the Take
sourceId preview:media:<id> media:<id>
mediaPlaybackKey media:<id>:live:<n> media:<id>:live:<n+1>

Both are deliberate — the preview: prefix keeps a paused poster from replacing Program's rolling copy, and the generation bump is what guarantees "roll from 0, audio on". But OwnedMediaFrameSource::requests() keys a decoder on sourceId|path|assetId|playbackKey|loop, so the arriving request matched nothing, a fresh decoder opened, and resolveLayers painted colorFromParticipantId over PROGRAM.

The fix

adoptCuedDecoders re-keys the cue's entry onto the live request instead of letting it retire. Entry is a shared_ptr whose worker holds its own reference, so the hand-over is a map re-key — the decoder and its held poster never notice.

This is not an exception to the go-live contract, it is the contract. The cue poster sits paused at frame 0 (MediaVideoPresentation::hold shows the first prepared frame and never advances), so resuming it is exactly "roll from 0, audio on".

The decision is pure (modules/MediaCueHandoff.h, the CaptureReaderStallPolicy / TakeRecordPolicy shape). Every condition is required: same asset id and same path, same loop flag, the retiring sourceId is exactly "preview:" + arriving.sourceId, the arriving generation is exactly +1, and — the load-bearing one — the cue never rolled. A decoder that has played is at an arbitrary position, and adopting it would put a clip on air mid-roll while the take record still read cut. Two candidates for one arrival is refused loudly and cold-starts: never guess which cue is the predecessor.

Three things the tests caught that reasoning had not

  1. It must run on the REQUEST path (selectVideo / pollMediaAudioFrames), never in manage(). The request set changes on the take tick, but manage() is a separate thread on a 2 ms wait, so an adoption deferred to it lands a tick late — at 60 Hz, exactly one frame of the placeholder this exists to remove. Adoption starts no thread and does no I/O, which is what makes it safe on the caller's path where creating a worker would not be.
  2. The queued frames must be dropped and current_ kept (MediaVideoPresentation::dropQueued). They were scheduled against the cue's paused epoch and can never come due on the go-live clock, so keeping them froze the clip on its poster forever. The held poster is what covers the refill.
  3. The owner names the source, not the decoder. Every decoder stamps participantId from the layer it was handed, so selectVideo re-stamping it is normally a no-op — but an adopted poster was decoded under the preview: id, and the compositor looks a media layer up by the LIVE source id. Without the re-stamp the hand-off delivered a frame nothing could match and Program painted the placeholder anyway.

The take record now reads missingSources=[] for this case because the cold start stopped happening. CLAUDE.md says that record is honest and the judge must not be taught to excuse a cold start — the judge was not touched.

Verification

903 native tests pass, 0 fail (892 on main before this). The threaded tests were run three times each to check for flakiness; stable.

Red-green, verified by reverting the production wiring and rebuilding:

Test Without the fix
OwnedMediaFrameSource.ACuedClipHandsItsWarmDecoderToProgram FAILS — drove the adoption
ProgramPixelContinuity.ACuedClipTakenToProgramNeverShowsThePlaceholder FAILS at tick 0 — the end-to-end pixel proof, over a REAL OwnedMediaFrameSource
AnAdoptedCueRollsInsteadOfFreezingOnItsPoster passes; it was red against the intermediate build that adopted without dropQueued, which is where I watched it fail
ACueThatAlreadyRolledIsNeverHandedOver passes either way — a refusal guard that catches the worker failing to set everPlayed, which the unit test cannot
AnAdoptedCueTurnsItsAudioOn passes either way — a contract guard, not red-first
MediaCueHandoff.* (6) new pure code; red as compile errors

A sensitivity check on the audio test found two dead lines in the adoption path (the creation loop re-arms wantsAudio unconditionally in the same manage() pass); they were removed rather than kept as decoration.

Not verified: no live run. The proof is the pixel test over the real owner plus the take record, not a real meeting.

What this does not fix

A clip cut to Program that was never cued in Preview has no warm decoder to adopt and still cold-starts. That is step 1 of #449 — hold the outgoing picture until the first real frame — and is next.

No shell code changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

A clip taken from Preview to Program cold-started: a placeholder slab on air
for the ticks before its first frame, and a take record reading `rebuilt` with
`missingSources=[media:<id>]`.

It cold-started because a clip changes identity TWICE on go-live. The `preview:`
namespace that keeps a paused poster from replacing Program's rolling copy
collapses, AND MediaGoLiveLedger advances the generation baked into the playback
key. So the arriving request matched no entry in OwnedMediaFrameSource, a fresh
decoder opened, and resolveLayers painted colorFromParticipantId over PROGRAM.

adoptCuedDecoders now RE-KEYS the cue's entry onto the live request instead of
letting it retire. Entry is a shared_ptr whose worker holds its own reference,
so the hand-over is a map re-key: the decoder and its held poster never notice.

This is not an exception to the go-live contract, it is the contract. The cue
poster sits paused at frame 0 (MediaVideoPresentation::hold shows the first
prepared frame and never advances), so resuming it is exactly "roll from 0,
audio on".

The decision is pure (modules/MediaCueHandoff.h, the CaptureReaderStallPolicy /
TakeRecordPolicy shape). Every condition is required: same asset id AND same
path, same loop flag, the retiring sourceId is exactly "preview:" + the arriving
one, the arriving generation is exactly +1, and the cue NEVER ROLLED. That last
one is load-bearing: a decoder that has played is at an arbitrary position, and
adopting it would put a clip on air mid-roll while the take record still read
`cut`. Two candidates for one arrival is refused loudly and cold-starts —
never guess which cue is the predecessor.

Three things the tests caught that reasoning had not:

- It must run on the REQUEST path (selectVideo / pollMediaAudioFrames), never in
  manage(). The request set changes on the take tick, but manage() is a separate
  thread on a 2 ms wait, so an adoption deferred to it lands a tick late — at
  60 Hz, exactly one frame of the placeholder this exists to remove. Adoption
  starts no thread and does no I/O, which is what makes it safe on the caller's
  path where creating a worker would not be.
- The queued frames must be DROPPED and current_ kept
  (MediaVideoPresentation::dropQueued). They were scheduled against the cue's
  paused epoch and can never come due on the go-live clock, so keeping them
  froze the clip on its poster forever. The held poster is what covers the
  refill, and is what removes the flash.
- The OWNER names the source, not the decoder. Every decoder stamps
  participantId from the layer it was handed, so selectVideo re-stamping it is
  normally a no-op — but an adopted poster was decoded under the `preview:` id
  and the compositor looks a media layer up by the LIVE source id. Without the
  re-stamp the hand-off delivered a frame nothing could match and Program
  painted the placeholder anyway.

The take record now reads missingSources=[] for this case because the cold start
stopped happening. The judge was not touched.

Tests (903 pass, 0 fail; the threaded ones stable over three repeats):
MediaCueHandoffTest.cpp covers every refusal;
OwnedMediaFrameSource.ACuedClipHandsItsWarmDecoderToProgram and
ProgramPixelContinuity.ACuedClipTakenToProgramNeverShowsThePlaceholder both fail
against main (verified by reverting the wiring and rebuilding);
AnAdoptedCueRollsInsteadOfFreezingOnItsPoster failed against the intermediate
build that adopted without dropQueued; ACueThatAlreadyRolledIsNeverHandedOver
and AnAdoptedCueTurnsItsAudioOn are contract guards that pass either way.

Still cold-starts, honestly: a clip cut to Program that was never cued in
Preview has no warm decoder to adopt. That is step 1 of #449, not done here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
@iamfatness
iamfatness merged commit aac2d98 into main Sep 12, 2026
12 of 19 checks passed
@iamfatness
iamfatness deleted the fix/media-cue-handoff branch September 12, 2026 11:43
iamfatness pushed a commit that referenced this pull request Sep 12, 2026
beta-2026-09-12-aac2d98 is cut and published, so the "live check with the
batch beta" that T1.14-T1.17 each name is now possible. It is still OWED:
none of those fixes, nor T1.11 step 2, has been seen in a real meeting, and
none may be ranked verified until it has. Build provenance recorded next to
the claim so a later reader can tell what the beta actually proves.

T1.11 stays at order 1 and drops from "S then M" to "S": #492 shipped step 2
(the warm cue decoder is handed to Program), so a clip cued in Preview no
longer flashes. Step 1 remains for a clip taken without a cue, which has no
warm decoder to adopt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
iamfatness added a commit that referenced this pull request Sep 12, 2026
* docs(backlog): reconcile with main and the issue tracker

Three things the file and the tracker disagreed on, all verified against
main @ eefee0c and `gh issue`/`gh pr` state. No re-ranking.

- T1.17 (#479) was still the next open Tier 1 item. It is closed by #490,
  which is on main. Moved to Done; the open order renumbers 1-3, so the
  top of the list is now T1.11 (#449), T1.12 (#475), T1.13 (#473).
- T3.8 (#482, ISO crops/pads a guest on a mid-recording frame-size change)
  was filed 2026-09-11 with no tier row, against this file's own rule that
  a new defect gets an issue AND a row. Placed in Tier 3 as a proposal,
  labelled as such — the tier is not an owner ruling.
- The "shipped in beta-2026-09-11-404602b" line covered the whole Done
  table, but T1.14-T1.17 (#487, #488, #489, #490) merged after that beta
  was cut. Each one's Done row asks for a "live check with the batch
  beta", and no such beta exists yet. Said so explicitly.

Also noted why Tier 3 has no T3.6 (#465 was promoted to T1.15).

Separately, #432 (T1.5) was closed on GitHub: #462 said "Fixes #432" and
merged as 3a3bedf but never auto-closed it, so the issue contradicted
this file's Done row. #457 (T1.7) has the same unclosed-by-#462 shape but
is left open deliberately — its own PR says the real check is a long
in-meeting close, which has not run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

* docs(backlog): the batch beta exists; T1.11 step 2 shipped

beta-2026-09-12-aac2d98 is cut and published, so the "live check with the
batch beta" that T1.14-T1.17 each name is now possible. It is still OWED:
none of those fixes, nor T1.11 step 2, has been seen in a real meeting, and
none may be ranked verified until it has. Build provenance recorded next to
the claim so a later reader can tell what the beta actually proves.

T1.11 stays at order 1 and drops from "S then M" to "S": #492 shipped step 2
(the warm cue decoder is handed to Program), so a clip cued in Preview no
longer flashes. Step 1 remains for a clip taken without a cue, which has no
warm decoder to adopt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
iamfatness added a commit that referenced this pull request Sep 12, 2026
…acOS CI (#498)

I broke main with #492. Three path literals were written with SINGLE
backslashes -- "C:\media\clip.mp4" -- so \m, \c and \o are invalid escape
sequences. MSVC accepts them with a warning and the tests passed locally; GCC
and Clang reject them outright:

  native/tests/MediaCueHandoffTest.cpp:56:30:
    error: '\o' not followed by '{'

That failed native-stub, native-stub-tsan, native-stub-macos,
native-metal-macos and mac-show-drill on every commit and PR since aac2d98.
I reported "903 tests pass" and it was true, but only on the one platform I
built. I did not check CI after merging, which is the actual mistake.

A Windows-only local build cannot catch this class, so the fix ships with the
check that can: scripts/qa/check-string-escapes.py walks the native tree for
escape sequences that are invalid in standard C++. It found a SECOND instance I
had missed by grep (ProgramPixelContinuityTest.cpp:190), which would have kept
CI red after a fix that looked complete.

903 native tests still pass on Windows with the corrected literals; the paths
are test data compared as strings, so the assertions are unchanged.


Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

Co-authored-by: Claude <noreply@anthropic.com>
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.

[T5.3] A clip entering Program cold-starts with a placeholder flash (hand the warmed cue decoder to Program)

2 participants