Repository navigation
Recording: survive an ISO participant leaving mid-session (#1030) - #1035
Merged
Merged
Conversation
iamfatness
force-pushed
the
codex/rc-1030
branch
from
October 23, 2026 22:09
0b71d4b to
5265103
Compare
Live show 2026-10-23: a Zoom ISO participant left, his ISO kept muxing audio silence with no video, Media Foundation's ThrottleStream blocked that WriteSample forever, and closeWriters' unconditional worker join wedged the recording file-control lane for the rest of the show. 1. Set MF_SINK_WRITER_DISABLE_THROTTLING on the one sink-writer creation path shared by Program.mp4 and every ISO file (a failed set fails the open loudly). 2. The core marks IsoSourceAudio::departed for a selected Zoom ISO whose participant is missing from a known engine roster (core/IsoDeparture.h); the MF sink neither queues nor writes a departed silence-only tick, so the file ends at its real duration. 3. RecordingTrackWorker gains joinFor/abandon/exited. closeWriters joins all workers against one shared deadline (4 s, env override COREVIDEO_RECORDING_TRACK_JOIN_BOUND_MS) and abandons a worker that misses it, logging loudly and naming the file. Abandoned ISO entries are parked and reaped once they exit; an abandoned Program refuses new takes by name until it exits and is leaked by AsyncEncoderSink at teardown (IEncoderSink::mustOutliveTeardown). MFShutdown is skipped while an abandoned writer still runs. 4. Queue-overflow loss text names the cause (a blocked or slow file writer) instead of "encoder load". Tests: bounded-join worker units, IsoDeparture, and four Windows MF tests (video stops while audio continues; departed stem not silence-filled; ISO writer abandoned past the bound; Program abandoned, refused, then recovered). The first fails with throttling left on. The recording stall QA cap rises to 10 s so drills can exceed the bound. Full native suite on Windows: 1886 passed, 0 failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Blocker 1: the ISO departure roster was read with floorParticipants() inside gatherAudioOutputWork, under coreMutex, taking the engine mutex the 8.5 ms audio.gather stall came from. The audio worker now reads the roster ids before the lock (readZoomRosterIdsUnlocked via ZoomEngineRuntime::rosterIdsIfConfigured), like pollZoomAudioUnlocked. ZoomEngineRuntime::mutex_ is an EngineMutex and the gather opens a ForbidEngineMutexScope that counts any acquisition; it found configured() taking the engine mutex on every gather, so configured() is now an atomic mirror. Blocker 2: with throttling off, the fMP4 sink held audio that ran ahead of a stopped video stream (4.7 MB queued, zero bytes on disk after 30 s). Mp4Writer::tickStalledVideo sends SendStreamTick on video once per second of media while video is more than 1 s behind; the same 30 s now reaches disk before Stop (+522 KB), queued_bytes is 0 at Finalize, and a camera that resumes muxes cleanly. The writer diagnostic fires from the audio path too and the drain line reports queued_bytes. Nits: a late Program finalize settles the take as stopped with a "finalized late" warning and recovery adopts it; every abandoned ISO is named beside an abandoned Program; a MediaCore test drives a real roster drop through the gather; join timings are logged and the 8-source validate-iso-record run joined everything at 38 ms of the 4 s bound. Full native suite (COREVIDEO_CAPTURE_TESTS=1): 1887 passed, 0 failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
iamfatness
force-pushed
the
codex/rc-1030
branch
from
October 23, 2026 23:32
5265103 to
ea616ad
Compare
…am tick New test MediaFoundationIsoFrameStampedBehindTheLastHeldFrame (camera returns with a frame stamped behind the last stall marker) showed the SendStreamTick design corrupted the timeline: file PTS stayed monotonic, but the fMP4 sink lays video out by accumulated sample durations and the tick collapsed the 28 s gap, so a camera resumed at 31.0 s muxed at 2.8 s against its audio. With the tick disabled the timeline was correct. holdStalledVideo now re-submits the last real picture once per second of media while video is more than 1 s behind the audio (0.5 s trail); the VFR pending logic gives each sample its exact duration, so the gap is kept and audio still reaches disk. A resumed frame stamped at or behind the last hold takes that hold's slot (the pending held picture is discarded), so it is never dropped and PTS stay monotonic. Held frames count in the mux frame count, never in framesWritten. Measured (30 s audio-only after video stopped): +472,886 bytes on disk before Stop (~57 MB/hour), queued_bytes=0 at Finalize, sink writer queue peak 7,680 bytes per stalled file (new stall_max_queued_bytes and held_frames on the drain line), resumed camera on its capture times. Full native suite (COREVIDEO_CAPTURE_TESTS=1): 1888 passed, 0 failed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1030
On the 2026-10-23 live show (beta-2026-10-23-1072a50), a Zoom ISO participant left during a recording. His ISO kept muxing audio silence with no video. Media Foundation's
CMFSinkWriter::ThrottleStreamblocked thatWriteSampleforever. When the shell re-formed the session,closeWriters()joined everyRecordingTrackWorkerwith an unboundedthread_.join(), and theAsyncEncoderSinkfile-control lane hung for the rest of the show. A second Record press failed, and the core missed the shell's close grace. Thread 81 and thread 428 in the dump show exactly these two stacks.The four parts
Mp4Writer::createFragmentedSinkis the only sink-writer creation path. Program.mp4 and every ISO file use it. It now setsMF_SINK_WRITER_DISABLE_THROTTLING = TRUE, and a failed set fails the open loudly. The adapter already paces each file with its own bounded queue on the shared capture epoch. Throttling did nothing except turn a stalled sibling stream into a block with no end. Program is covered by the same change.zoom:ISO ids with the engine's floor roster, the same signal the shell logs asroster-drop. The comparison iscore::departedIsoSourcesincore/IsoDeparture.h, and it sets the newIsoSourceAudio::departed. The MF sink neither queues nor writes a departed tick that has no PCM, and it logs the departure once. The stem stops at its last real media, and the existing tail reconcile aligns it with the last video frame, so the file finalizes with its real duration. An empty roster counts as "unknown", never as "everyone left". If the participant returns, the stem resumes andisoAudioAdvancefills the gap with silence at that point. A participant with the camera off who is still in the meeting is not treated as departed.RecordingTrackWorkergainsjoinFor(bound),abandon()andexited():abandon()discards queued work (it may reference the sink) and detaches the thread. If the blocked write ever returns, the finalize still runs late.closeWriters()gives all joins one shared deadline: 4000 ms after the last close, matchingAsyncEncoderSink's finalize grace.COREVIDEO_RECORDING_TRACK_JOIN_BOUND_MS(50..60000) overrides it for drills and tests.[iso-writer] ABANDONEDand named in the ISO status warning and inrecordingError. Its entry is parked and released once its thread exits.MFShutdownis skipped while any abandoned writer is still running.IEncoderSink::mustOutliveTeardown()makesAsyncEncoderSink::Stateleak the sink instead of destroying it under a live thread. The same bound covers the destructor path at core shutdown.The recording stall QA knob (
COREVIDEO_QA_RECORDING_STALL_MS) now allows up to 10 s instead of 2 s, so a drill can hold a writer past the bound.validate-iso-record.mjsaccepts the same range.Tests (Windows dev core,
npm run build:native-dev, Release)COREVIDEO_CAPTURE_TESTS=1: 1888 passed, 0 failed (after the review rounds).RecordingTrackWorker.BoundedJoin*(3 tests: healthy join; blocked write abandoned and finalized late, with discarded work never run; writer stuck in finalize) andIsoDeparture.OnlyZoomSourcesMissingFromAKnownRosterHaveDeparted.!COREVIDEO_STUB && COREVIDEO_ENABLE_DEV_ADAPTERS && COREVIDEO_WITH_MF_ENCODERguard:MediaFoundationIsoWhoseVideoStopsWhileAudioContinuesClosesWithinBound: 1 s live, then 30 s of silence ticks with no video. Stop must finish in under 3.5 s and must not abandon anything. Red with the throttling attribute removed: the writer blocked in the silence write and was abandoned at 4 s.MediaFoundationDepartedIsoParticipantIsNotSilenceFilled: the stem stays under 3 s instead of 31 s.MediaFoundationStopAbandonsAnIsoWriterThatMissesTheJoinBound: the stall QA holds the ISO for 8 s against a 3 s bound. Stop returns, the file is named, the next take runs, and the late writer finalizes and is released.MediaFoundationAbandonedProgramWriterRefusesThenRecovers: Program is held past the bound. Start is refused with a named error, then recording recovers after the late finalize.Review round (fix-first)
gatherAudioOutputWork. The audio worker now reads the roster ids before taking coreMutex (MediaCore::readZoomRosterIdsUnlockedthroughZoomEngineRuntime::rosterIdsIfConfigured), the same pattern aspollZoomAudioUnlocked.ZoomEngineRuntime::mutex_is now anEngineMutex(modules/EngineMutexGuardrail.h), and the gather opens aForbidEngineMutexScopethat counts every acquisition inside it. The guardrail immediately found a violation that predates this PR:configured()took the engine mutex on every gather, viaobserveZoomGuestAvEpoch.configured()now reads a lock-free atomic mirror. The new testMediaCoreIsoDeparture.RosterDropMarksTheIsoDepartedThroughTheGatherWithoutTheEngineMutexsends a real roster drop and return through the gather and asserts the counter does not move.IMFSinkWriter::SendStreamTick: replaced. The follow-up testMediaFoundationIsoFrameStampedBehindTheLastHeldFrameshowed it corrupted the timeline. File PTS stayed monotonic, but the sink lays video out by accumulated sample durations and the tick collapsed the 28 s gap, so a camera that resumed at 31 s was muxed at 2.8 s against its audio.Mp4Writer::holdStalledVideo. While video is more than 1 s behind the audio, the writer re-submits the last real picture once per second of media, trailing the audio by 0.5 s. Held frames count in the mux frame count, never inframesWritten. The variable-frame-rate logic gives each sample its exact duration, so the gap is kept.queued_byteswas 0 at Finalize, and the sink writer's own queue peaked at 7,680 bytes for the stalled file. The Finalizedrainline now reportsheld_frames(28 to 29) andstall_max_queued_bytes.[recording-writer] queued_bytesnow also fires from the audio path (via=audio).stoppedwith a "finalized late" warning, and start() recovery adopts that state.validate-iso-record.mjs --sources 8 --seconds 20 --allow-capacity-warningpassed. Every ISO joined at 38 ms of the 4 s deadline and Program at 20 to 38 ms, because the ISOs finalize during the 8 x 100 ms close stagger. The 4 s bound was kept. Without--allow-capacity-warningthe run fails only on the known one-ISO software-spill admission note.D3DProgramBuffer.MultiviewProgramPixelsAdvanceAtFixedConsumerPhases) failed once in one full run. It passed 3/3 when rerun alone, and the next full run was clean; it failed once more in a later full run and was clean again on the run after. It does not touch recording code.Not verified
AsyncEncoderSinkgrace), so it no longer hangs. The shell's 2000 ms grace can still be shorter than a normal Program finalize; this PR does not change that.Mp4Writer::finalizedrain uses its full 2 s, because the NVENC MFT held about 20 of 51 frames after end-of-stream. That behavior predates this PR and is why the default bound is 4 s and not lower.🤖 Generated with Claude Code