fix: prevent AVAssetWriter -16364 (InvalidTimestamp) and harden instant/pause recording paths - #2094
Open
richiemcilroy wants to merge 16 commits into
Open
fix: prevent AVAssetWriter -16364 (InvalidTimestamp) and harden instant/pause recording paths#2094richiemcilroy wants to merge 16 commits into
richiemcilroy wants to merge 16 commits into
Conversation
… tie correction AVAssetWriter receives PTS as whole microseconds (1MHz SampleTimingInfo), but remapped capture timestamps carry nanosecond precision. During stall-recovery bursts two frames can land inside the same microsecond: they pass the nanosecond-space monotonicity guard yet collapse into duplicate writer timestamps, which AVAssetWriter reports asynchronously a few frames later as -11800/-16364 (InvalidTimestamp), aborting the whole recording. Field logs from 0.5.8 (studio mode + camera, the non-fragmented AVFoundation muxer path) show exactly this failure at 285s and 103s. Truncate the PTS to whole microseconds before the monotonic tie correction so the guard operates in the units the writer sees, and surface the NSError code/domain/underlying error in WriterFailed messages and append-site logs so future reports are diagnosable. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…mple extents disjoint Flushing at pause wrote the pending frame with the full nominal duration, so the first post-resume frame (tie-corrected +1us) landed inside that sample's extent. Overlapping extents are the sporadic AVAssetWriter failure shape reproduced by the overlapping-extents tests. Holding the frame until resume writes it with the real clamped forward gap instead; stop-while-paused still flushes it via finish_start. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…t the segmented encoder The 0.5.8 field-failure timeline (nanosecond-precision timestamps, a multi-second stall, then backlogged frames landing hundreds of nanoseconds apart, plus an exact duplicate and a backwards blip) must encode with strictly monotonic PTS, no dropped frames, and survive the production remux + decode probe. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…pause/resume The harness drives cidre/ShareableContent and the macOS builder signature, so it never compiled on Linux and broke the whole cap-recording test suite there. Gate it to macOS and extend the real recording flow with a mid-recording pause/resume cycle, with duration bounds tight enough to fail if the pause leaks into either timeline. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…nt pipeline SharedPauseState gets direct unit coverage (excision, no-frame pauses, accumulated cycles, backwards resume timestamps), and the instant-mode scenario harness gains two full-pipeline cases: a paused-and-resumed recording whose output must excise the pause identically on both tracks and stay uploadable, and a stall-recovery burst with same-microsecond timestamps that must keep A/V aligned and uploadable. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…ncoders Four scenarios rotted because they never ran in CI: the DASH muxer only cuts segments at keyframes and the encoder pins a 2s GOP (libx264 honors keyint_min strictly, hardware encoders emit extra IDRs), so sub-GOP segment durations produced platform-dependent segment counts. Mark source I-frames at the segment cadence so the counts are deterministic everywhere, and compare assembled media durations instead of manifest bookkeeping totals: a tail that ends between keyframes is appended into the previous segment file, so the manifest's estimated total under-reports while the assembled output carries the full content. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
The scenario harness (assembly, validation, pause/resume excision, stall-recovery bursts) never ran in CI, which is how four of its tests rotted unnoticed. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…umed pause gaps Holding the pending frame across pause created the first window where timestamp_offset can change (pause-gap consumption) between a deferred offset being snapshotted and applied: a tie-bumped frame held across a pause would, on append after resume, overwrite the gap-adjusted offset with its stale pre-pause snapshot and silently shift every later video and audio timestamp forward by the gap. Shift the held snapshot when either path consumes a gap so apply-on-append stays correct. Also verify by container duration that a held-frame pause leaves the muxed timeline untouched, retry writer-busy queues in the regression tests (paravirtualized CI runners have no hardware VideoToolbox, so single-shot queue calls flake), and wait for input readiness before the finish-time flush in the stop-while-paused test. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…-format writer failures The critical disk-space check only ran for instant mode, so a studio or camera recording filling the disk killed the AVAssetWriter on a failed async write and lost the moov (unrecoverable file). Check in every mode and stop while the writer is alive so finish() preserves the output. The four fatal-message sites destructured the NSError and Display-formatted it, which hides the code and NSUnderlyingError that identify failures like -11800/-16364; debug-format them so dialogs and logs carry the full error. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…eam ticks MediaFoundation stamps samples in 100ns ticks but the stream time base is ~333x coarser (1/(fps*1000)): two strictly increasing sample times can quantize onto the same output tick and the mov muxer rejects the duplicate — the same unit-mismatch class as the AVFoundation -16364 failures, currently surfacing as dropped packets in the hardware encoder path. Bump ties one tick in the writer-visible unit, like normalize_input_pts in cap-enc-ffmpeg. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…mpile all harnesses in CI The non-fragmented AVFoundation display writer (the 0.5.8 field-failure path) had no real-environment coverage: record the primary display through the real studio actor with fragmented(false), pause and resume mid-recording, and verify each segment's display.mp4 is a finalized, decodable MP4 with the expected content duration. sync-tests now also compile-checks every cap-recording test target so hardware harnesses can't rot into non-compiling again. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
richiemcilroy
marked this pull request as ready for review
August 6, 2026 15:24
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
…not composite The windows-2022 runner image update broke WARP compositing under the notch golden tests with no repo change (passing Aug 4, failing every run since Aug 5), blocking every PR that triggers sync-tests. Keep the shape assertions at full strength on any adapter that can actually render — hardware everywhere, and software rasterizers like Ubuntu's lavapipe — and skip loudly only when a software adapter fails the basic sanity of clearing to white and drawing anything at all. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
… in the sync matrix Shared runners cannot real-time-encode several hundred fps of synthetic worst-case content, and the muxer's stall budget deliberately drops frames rather than block capture, so exact frame-count equality above real-device delivery rates asserts runner throughput, not timestamp correctness — the shape behind every matrix failure on main's nightly runs. Above 240fps delivered, allow bounded drops but verify every muxed pts against the nearest sent timestamp so timestamp bugs still fail. The heavy over-delivery cases also ride on the drift tracker's designed wall-clock re-pinning (0.1s cap), leaving a 0.15s relative tolerance only 50ms of scheduler headroom at 1000 timed emissions per second — the macos-latest runner failed the curated 1000fps case at exactly 0.150s. Widen the relative tolerance to 0.25s for such cases only; the bug class this matrix guards produces errors of a second or more. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
…ission The end-of-emission lag guard misses a stall that later catches up, but the contamination is the same: frames stamped with scheduled capture times arrive late and the pipeline's designed wall-clock re-pinning moves muxed pts by roughly the stall size — macos-latest failed the plain 30fps steady case with a 0.307s error from exactly this. Measure per-frame emission lateness directly and skip loudly past 0.1s; real timestamp bugs reproduce on healthy runners, a stalled runner proves nothing either way. Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.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.
Problem
Two users on 0.5.8 hit the same mid-recording failure:
The recording aborts with an error dialog and the display track is unusable (no
moovatom is ever written).Root cause
Uploaded logs contain the full NSError:
AVFoundationErrorDomain -11800with underlyingNSOSStatusErrorDomain -16364— CoreMedia's InvalidTimestamp (duplicate/backwards PTS, reported asynchronously a few appends late).MP4Encoder::queue_video_frameenforced monotonicity on nanosecond-precisionDurations while the writer receives microsecond-truncated PTS; two frames inside the same microsecond pass the guard and collapse into duplicate writer PTS. The trigger is a stall-recovery burst (the log shows a ~15s system-wide stall right before failure). Reached by macOS studio recordings with camera active (non-fragmentedAVFoundationMp4Muxer), the camera track writer, and camera-only recordings. Not a 0.5.8 regression — the path is byte-identical since 0.4.7x-era code.Fixes
enc-avfoundation: quantize video PTS to whole microseconds before the monotonic tie correction — the guard now operates in the units the writer sees; emitted PTS are strictly increasing integral microseconds with non-overlapping extents.enc-avfoundation: hold the pending frame acrosspause()instead of flushing with nominal duration, which put the first post-resume frame inside the flushed sample's extent (the sporadic overlapping-extents writer-failure shape). Stop-while-paused still flushes viafinish_start; a container-duration assertion proves the muxed timeline is unchanged.enc-avfoundation: shift a held frame's deferred offset when a pause gap is consumed (found by adversarial review): a tie-bumped frame held across a pause carried a stale offset snapshot that, applied on append after resume, overwrote the gap-adjustedtimestamp_offsetand silently shifted all later timestamps.recording(macOS): disk-exhaustion guard for all AVAssetWriter modes, not just instant. Previously a studio or camera recording filling the disk killed the writer on a failed async write (file unrecoverable); now the encoder thread stops while the writer is alive sofinish()preserves the recording.mediafoundation-ffmpeg(Windows): strictly monotonic muxer PTS in stream ticks. A muxer-path audit found the same unit-mismatch class: MF stamps samples in 100ns ticks, the stream time base is ~333× coarser, and nothing guarded the re-quantization — surfacing as dropped packets in the hardware encoder path. One guard at the writer-visible unit closes it for every MF consumer.QueueFrameError::WriterFailedand all four fatal-message sites now debug-format the NSError, so dialogs and logs carry the code andNSUnderlyingErrorinstead of "The operation could not be completed".Audits (documented, no action needed)
A full audit of every muxer/encoder path for the unit-mismatch class found all other shipping paths safe: the ffmpeg H264 stack guards in tick space (
normalize_input_pts) with warn-and-continue containment; the OOP muxer's guard runs in the same tick unit that crosses the process boundary; all audio encoders funnel through one guarded base;win_segmented{,_camera}.rscontain the defect but are dead code. Error-propagation audit: only the display/screen pipeline is fatal to a recording; mic/camera/system-audio failures degrade.Test coverage (real encoders, real files, wired into CI)
enc-avfoundation(macOS CI, real AVAssetWriter): same-microsecond pair bumped apart, same-microsecond bursts survive, pause/resume with resume-tie keeps extents disjoint + container duration intact, stop-while-paused flushes the held frame. Queue calls use a writer-ready retry helper mirroring production so paravirtualized runners (no hardware VideoToolbox) can't flake the suite.enc-ffmpeg(all platforms): stall-recovery burst with same-microsecond timestamps, exact duplicate, and backwards blip must encode every frame with strictly monotonic PTS and survive the production remux + decode probe.cap-recordinglib (all platforms):SharedPauseStateexcision/no-frame-pause/accumulated-cycles/backwards-resume coverage.instant_mode_scenarios(all platforms, newly wired intosync-tests.yml): full-pipeline pause/resume excision on both tracks and stall-burst A/V alignment, ending invalidate_instant_recordinguploadability. Four rotted tests repaired (segments only cut at keyframes; the harness now marks I-frames at the segment cadence and compares assembled media durations, not manifest estimates).hardware_instant_recordinggated to macOS (it broke every non-macOS test build via ungated imports) and extended with a real pause/resume cycle; newhardware_studio_recordingexercises the exact field-failure path (non-fragmented AVFoundation display writer) with pause/resume and per-segment validation.sync-tests.ymlnow compile-checks all test targets so harnesses can't rot again.CI repairs (sync-tests was failing on every branch, including main's nightlies)
cap-renderingnotch golden tests: the windows-2022 runner image update broke WARP compositing with no repo change (passing Aug 4, failing since Aug 5, blocking every PR). Shape assertions stay at full strength on any adapter that renders (hardware everywhere, Ubuntu's lavapipe); a software adapter that fails render sanity skips loudly.Verification — full board green
Final run: 27 checks, 0 failed — A/V Sync Tests green on macOS, Windows, and Ubuntu (the first fully green sync-tests run on any branch since Aug 4), Clippy
-D warningsgreen on macOS and Windows, Format/Typecheck/Biome/CodeQL green. macOS rancap-enc-avfoundation44/44 against the real AVAssetWriter,instant_mode_scenarios61/61, lib 248/248; the matrix passed with one case skipped by its own pre-existing lag guard. Locally: 41-case matrix green with zero skips,cap-enc-ffmpeg50/50,cap-recording --lib237/237,instant_mode_scenarios61/61. Property check across 200 randomized stall/burst scenarios × 20k frames: pre-fix 243,989 duplicate writer-PTS pairs, post-fix 0.Follow-up candidates (out of scope)
movieFragmentIntervalor segment rotation).total_durationunder-reports a tail ending between keyframes (bookkeeping estimate only; media is complete).win_segmented{,_camera}.rs,WindowsOOPFragmentedM4SMuxer.Greptile Summary
The PR hardens macOS and Windows recording timestamp handling, preserves pending AVFoundation frames across pauses, expands low-disk protection, improves native writer diagnostics, and adds extensive regression and hardware-oriented coverage.
Confidence Score: 5/5
The PR appears safe to merge, with no concrete changed-code defect identified.
The timestamp normalization, pause-offset coordination, low-disk shutdown paths, and expanded test coverage are consistent with their production callers and lifecycle behavior.
Important Files Changed
Reviews (1): Last reviewed commit: "test(recording): add real-hardware studi..." | Re-trigger Greptile
Context used: