Skip to content

Guard asset writer appends against unstarted and failed writers - #45

Open
tutis wants to merge 1 commit into
Mnpn:mainfrom
tutis:fix-audio-append-crash
Open

tutis wants to merge 1 commit into
Mnpn:mainfrom
tutis:fix-audio-append-crash

Conversation

@tutis

@tutis tutis commented Sep 10, 2026

Copy link
Copy Markdown

The crash

Azayaka aborts mid-recording with SIGABRT and leaves a zero byte file behind. Captured on macOS 26.6.2 with Azayaka 1.4 (67), microphone recording enabled, 25 seconds into a recording:

libc++abi.dylib   __cxa_throw
libobjc.A.dylib   objc_exception_throw
AVFCore           -[AVAssetWriterInput appendSampleBuffer:]
Azayaka           <ClassicProcessing>
ScreenCaptureKit  __59-[SCStream(SCContentSharing) startRemoteAudioReceiveQueue:]_block_invoke

asi is abort() called, thrown from the audio receive queue.

Cause

AVAssetWriterInput.append() raises an Objective-C exception when the writer has not started a session yet, or has already moved to .failed. Swift cannot catch an Objective-C exception, so either case terminates the process.

isReadyForMoreMediaData reports neither state, and in ClassicProcessing.swift it was the only guard on the .audio and .microphone appends:

if (awInput != nil) && awInput.isReadyForMoreMediaData {
    awInput.append(sampleBuffer)
}

Only the .screen branch calls startSession(atSourceTime:), and nothing anywhere inspects vW.status or vW.error after setup. That leaves two ways in:

  1. An audio buffer arriving before the first video frame has started the session.
  2. Any buffer arriving after the writer failed partway through a recording, which is the more likely path for a crash tens of seconds in.

Because the abort skips finishWriting(), the mp4 never gets its moov atom and the whole recording is lost, not just the tail.

The AVAudioEngine mic tap a few lines above already had the right idea (micInput.isReadyForMoreMediaData && startTime != nil), so this extends the same check to every append site.

The change

  • canAppend(to:) checks the writer exists, the session has started, and the status is .writing before consulting isReadyForMoreMediaData. Used by all four append sites, video included, since a failed writer breaks that one too.
  • A failed writer stops the recording rather than leaving it running while every sample is dropped. stopRecording(withError:) runs finishWriting(), so the user keeps a playable file of everything captured up to the failure instead of an empty one.

Verification

  • Builds clean from main with Xcode 26.6 (Swift 6.3.3), ad hoc signed.
  • Typechecked patched vs unpatched across the whole target: identical diagnostics, no new errors or warnings. Confirmed that check can fail by breaking the new helper deliberately and watching it go red.
  • Honest caveat: I have not found a way to force AVAssetWriter into .failed on demand, so the failure branch is reasoned from the API contract and the crash report rather than exercised in a test. The guard itself is the part the crash proves is needed.

Happy to adjust scope. I deliberately kept it to the guard and did not add a user facing alert for the mid recording failure, since alertRecordingFailure is worded for start up failures and a new string would mean touching the localisation catalogue. Glad to add one if you would prefer that.

🤖 Generated with Claude Code

@tutis
tutis force-pushed the fix-audio-append-crash branch from 8e9c38a to 540b76a Compare September 10, 2026 16:00
AVAssetWriterInput.append() raises an Objective-C exception, which Swift
cannot catch, when the writer has not yet started a session or has already
failed. isReadyForMoreMediaData reports neither of those states, so the
audio and microphone branches could append in both, terminating the app
with SIGABRT and leaving a zero byte file behind, since finishWriting()
never runs.

Two ways in: an audio buffer arriving before the first video frame has
called startSession(), and any buffer arriving after the writer has failed
partway through a recording. The AVAudioEngine mic tap already checked
startTime, so this extends the same reasoning to every append site.

A failed writer now also ends the recording instead of leaving it running
while every sample is dropped, which finalises the file written so far.

The failure is reported through os.Logger rather than print, because a bundled
app launched normally has stdout and stderr pointing at /dev/null, so neither
print nor NSLog is recoverable afterwards. Verified on macOS 26: an os.Logger
line appears under the dev.mnpn.Azayaka subsystem, the NSLog equivalent does
not appear at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tutis
tutis force-pushed the fix-audio-append-crash branch from 540b76a to 25a5a5f Compare September 11, 2026 14:18
@tutis

tutis commented Sep 11, 2026

Copy link
Copy Markdown
Author

Updated: the failure is now reported through os.Logger instead of print.

A bundled app launched from Finder or LaunchServices has stdout and stderr on /dev/null, so a print would have been unrecoverable after the fact, which defeats the point of reporting the failure at all. I checked both alternatives on macOS 26 rather than assuming: an os.Logger line shows up under the dev.mnpn.Azayaka subsystem, and the NSLog equivalent does not appear in the unified log at all (it reaches stderr only).

No change to the guard itself.

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.

1 participant