Repository navigation
fix(export): realtime writer inputs — offline AV export ran at ~0.2 fps and dropped frames - #374
Merged
Merged
Conversation
….2 fps and dropped frames The deterministic offline export (ADR-0032 Phase C) put its AVAssetWriter inputs in non-realtime mode on the assumption that is the faster-than-realtime setting. Measured, it is the opposite for OUR producer: in non-realtime mode Apple's H.264 encoder holds the video input NOT-ready until it has a batch of ~16 frames (or a ~1.5 s internal timeout flushes one). Vivid appends one frame per frame-loop tick, so every frame waited on that timeout: ~0.2 fps, and when wait_ready gave up the frame was DROPPED without a word — a 116-frame export took ~10 minutes and the file held 49 frames. Reproduced standalone (Swift, same settings): a one-frame-per-tick producer does 37 frames in 20 s non-realtime vs 120 frames in ~1 s realtime; back-to-back appends are fast in both modes, which is why the export looked fine when first validated. - Both inputs are now `expectsMediaDataInRealTime = YES` in every mode. Offline determinism never came from that flag — it comes from the explicit PTS in write_video_frame / write_audio_samples, unchanged. - wait_ready polls with usleep instead of pumping a nested NSRunLoop: readiness is flipped from the writer's queue, not the main run loop, and the pump was running hosted plugins' GUI timers mid-export with the audio device paused. - A refused frame is no longer silent (ADR-0019): AvExportJob counts it, AvBounceResult.dropped_frames carries it, av_export_status reports it, the completion toast says "N frames DROPPED" at Warning level, and the MCP docstring tells agents to treat such an export as incomplete. Verified: the same 2-bar export on the plugin-heavy song-sketch copy now completes in ~2 s with 116/116 frames in the file and dropped_frames=0. test_av_bounce gains a refusing mock (5 of 15 refused => dropped_frames 5, audio complete) and a healthy-encoder zero check. Full headless ctest green; MCP parity + layering guards green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jeffcrouse
enabled auto-merge (squash)
September 19, 2026 13:54
Contributor
Production gate (core): ✅ PASS
|
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.
Summary
The deterministic offline export (
export_av, ADR-0032 Phase C) was crawling at ~0.2 fps and silently dropping frames: a 116-frame export took ~10 minutes and the file held 49 frames. Found while generating real media for the gate-1 Review workspace.Root cause
The offline path set the
AVAssetWriterinputs toexpectsMediaDataInRealTime = NO, assuming that's the faster-than-realtime mode. Measured, it's the opposite for our producer: in non-realtime mode Apple's H.264 encoder holds the video input not-ready until it has a batch of ~16 frames (or a ~1.5 s internal timeout flushes one). Vivid appends one frame per frame-loop tick, so every frame ate that timeout, and whenwait_readygave up the frame was dropped without a word.Reproduced standalone in Swift with the exporter's exact settings, one frame per 120 Hz timer tick:
Back-to-back appends are fast in both modes, which is why the export looked fine when first validated (#325–#327).
Changes
expectsMediaDataInRealTime = YESin every mode. Determinism never came from that flag — it comes from the explicit PTS inwrite_video_frame/write_audio_samples, unchanged.wait_readypolls withusleepinstead of pumping a nestedNSRunLoop— readiness is flipped from the writer's queue, and the pump was running hosted plugins' GUI timers mid-export with the audio device paused.AvBounceResult.dropped_frames, reported byav_export_status, the completion toast ("N frames DROPPED", Warning), and the MCP docstring.Verified
Same 2-bar export on the plugin-heavy song-sketch copy: ~2 s, 116/116 frames in the file,
dropped_frames: 0.Test plan
test_av_bounce— refusing mock: 5 of 15 refused ⇒dropped_frames == 5, video 10, audio complete; healthy mock ⇒ 0 (279 checks)test_av_bounceis in the audio-engine tier)🤖 Generated with Claude Code