Skip to content

fix(capture): stop losing the recording when finalization is interrupted - #798

Open
iOSDevSK wants to merge 1 commit into
webadderallorg:mainfrom
iOSDevSK:fix/screencapturekit-finalize-crash
Open

fix(capture): stop losing the recording when finalization is interrupted#798
iOSDevSK wants to merge 1 commit into
webadderallorg:mainfrom
iOSDevSK:fix/screencapturekit-finalize-crash

Conversation

@iOSDevSK

@iOSDevSK iOSDevSK commented Aug 7, 2026

Copy link
Copy Markdown

Problem

The macOS capture helper crashes at stop time and takes the whole recording with it.

ScreenCaptureRecorder.finishCapture() appends a tail frame — one extra copy of the last buffer, so the final frame gets its full duration — without checking that the input can accept it:

if let originalBuffer = lastSampleBuffer, let videoInput = videoInput {
    ...
    videoInput.append(additionalSampleBuffer)   // no readiness / status check
}

-[AVAssetWriterInput appendSampleBuffer:] raises NSInvalidArgumentException when the encoder queue is still backed up, and Swift cannot catch an Objective-C exception, so the helper hits abort() in the middle of finalization: no finishWriting(), an mdat still carrying its size-0 placeholder, no moov index. The file is unplayable.

Crash report backtrace:

AVFCore    -[AVAssetWriterInput appendSampleBuffer:] + 556
helper     ScreenCaptureRecorder.finishCapture() + 460
helper     ScreenCaptureRecorder.stopCapture()
helper     closure #1 in closure #1 in RecorderService.stop()
libc++abi  __cxa_throw -> abort_message -> abort

Three of twelve recordings on one machine in a single session died this way — 45 MB, 45 MB and 69 MB, all long captures at 5120×2830. The short ones finalized fine, which is consistent with encoder backpressure at stop being the trigger. The three broken files carry the exact interrupted-writer layout (ftyp + wide + mdat size 0, no moov) and none of them got the .cursor.json sidecar that a successful stop writes.

The main process then made it worse. When the helper died, the stop handler's fallback checked only that the output file existed and passed it to the editor, which rendered a smear of undecodable bytes instead of reporting a failed recording. recoverNativeMacCaptureOutput() — reachable independently from the renderer's recovery IPC — did the same.

Fix

Helper (ScreenCaptureKitRecorder.swift)

  • The tail frame now waits up to 1 s for the encoder queue to drain and is skipped if it does not. It is cosmetic; the file is never worth one frame.
  • endSession, markAsFinished and finishWriting raise the same uncatchable way on a writer that is no longer .writing, so all three are gated on writer status.
  • The capture callbacks already checked isReadyForMoreMediaData but not writer status — they do now, which closes the same crash for a mid-recording failure such as a full disk.
  • A writer that does not reach .completed is reported as an error instead of a success.

Main process

isUnfinalizedMp4() (electron/ipc/recording/mp4Integrity.ts) walks the top-level box table — 8 bytes per header, so a multi-gigabyte capture costs a handful of reads — and identifies the interrupted-writer signature. Both recovery paths now refuse such a file and report a failed recording. The classifier is deliberately conservative: only that exact layout is unfinalized, anything unfamiliar is unknown and keeps the previous behaviour.

The renderer already surfaces a stop failure to the user, so this produces an error message rather than a garbled editor.

Testing

  • npm test — 1004 passed, including 8 new cases for the box-table walk and classifier.
  • The classifier was checked against real files: all three interrupted captures flagged, three finalized ones and a rebuilt one not flagged.
  • Patched helper driven directly from the CLI: 6 s and 95 s captures (1886 frames, 154 MB) both finalize with a proper moov.
  • tsc --noEmit and biome check clean on the changed files.

Not reproduced: the backed-up-queue state itself at stop. The guard is a code-level guarantee — append is now unreachable unless the input is ready and the writer is .writing, the two documented raise conditions — backed by the crash report.

Note on existing broken files

The capture data in an interrupted file is intact: the mdat parses to the exact byte as AVCC NAL units, and rebuilding an index produces a clean, watchable video. An in-app "recover interrupted recording" path is left as follow-up.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved recording recovery by detecting incomplete or unfinalized MP4 files.
    • Prevented damaged captures from being exposed as playable recordings.
    • Improved macOS capture finalization by validating writer readiness before processing video and audio.
    • Added clearer failure handling for incomplete recording states.

`finishCapture()` appended a tail frame — one extra copy of the last buffer so
the final frame gets its full duration — without checking that the input could
accept it. `-[AVAssetWriterInput appendSampleBuffer:]` raises an
NSInvalidArgumentException when the encoder queue is still backed up, and Swift
cannot catch an Objective-C exception, so the helper hit abort() in the middle
of finalization: no `finishWriting()`, an `mdat` still carrying its size-0
placeholder and no `moov` index. The whole recording became unplayable.

Three of twelve recordings on one machine today died this way, all of them long
captures at 5120x2830 where the encoder is most likely to be behind at stop
time; the short ones finalized fine. Crash reports point at
finishCapture() -> appendSampleBuffer -> objc_exception_throw -> abort.

The capture callback already guarded `isReadyForMoreMediaData` before
appending; finalization did not. Now the tail frame waits up to a second for
the queue to drain and is skipped if it does not — the file is never worth one
cosmetic frame. `endSession`, `markAsFinished` and `finishWriting` raise the
same way on a writer that failed mid-capture, so they are gated on
`.writing`, and the callback appends now check writer status too, which closes
the same crash for a mid-recording failure such as a full disk. A writer that
does not reach `.completed` is reported as an error instead of a success.

The main process made it worse: when the helper died, the stop handler's
fallback checked only that the output file existed and handed it to the editor,
which rendered a smear of undecodable bytes. `isUnfinalizedMp4()` detects the
interrupted-writer signature (open-ended `mdat`, no `moov`) and reports a
failed recording instead. The check is deliberately conservative — anything it
cannot positively identify keeps the previous behaviour.

Rebuilding an index for already-broken files is left as follow-up; the capture
data in those files is intact and recoverable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the Slop label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The recorder now checks writer readiness during capture finalization. New MP4 utilities classify incomplete files. macOS recovery rejects unfinalized fallback captures before audio muxing or video finalization.

Changes

MP4 Capture Recovery

Layer / File(s) Summary
Native writer readiness and finalization
electron/native/ScreenCaptureKitRecorder.swift
The recorder checks writer and input readiness before appending samples, waits during finalization, and reports incomplete writers.
MP4 box parsing and integrity classification
electron/ipc/recording/mp4Integrity.ts, electron/ipc/recording/mp4Integrity.test.ts
New utilities parse bounded top-level MP4 boxes and classify finalized, unfinalized, and unknown layouts. Tests cover box parsing, classification, temporary files, and missing files.
macOS recovery validation
electron/ipc/recording/mac.ts, electron/ipc/register/recording.ts
Recovery checks candidate MP4 files before audio handling or fallback finalization and returns failure details for unfinalized output.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScreenCaptureKitRecorder
  participant MP4File
  participant NativeRecovery
  participant isUnfinalizedMp4

  ScreenCaptureKitRecorder->>MP4File: finalize ready writers
  NativeRecovery->>MP4File: locate fallback capture
  NativeRecovery->>isUnfinalizedMp4: validate MP4 metadata
  isUnfinalizedMp4-->>NativeRecovery: return layout result
  NativeRecovery-->>NativeRecovery: reject unfinalized output
Loading

Possibly related PRs

Suggested labels: Checked

Suggested reviewers: webadderall

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing recording loss when macOS capture finalization is interrupted.
Description check ✅ Passed The description clearly explains the problem, fix, motivation, testing, and follow-up scope, although it does not use every template section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ This pull request has been flagged by Anti-Slop.
Our automated checks detected patterns commonly associated with
low-quality or automated/AI submissions (failure count reached).
No automatic closure — a maintainer will review it.
If this is legitimate work, please add more context, link issues, or ping us.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/recording/mac.ts`:
- Around line 287-290: Update the unfinalized-file branch in the recording
recovery helper around isUnfinalizedMp4 to return { success: false, message,
unfinalizedPath: candidatePath } instead of null. Preserve the existing console
error and ensure this failure result propagates to both
stop-native-screen-recording and the dedicated recovery IPC handler without
muxing or editor processing.

In `@electron/native/ScreenCaptureKitRecorder.swift`:
- Around line 408-418: Update the finalization logic around systemAudioWriter,
microphoneOnlyWriter, and finalizeFailure so every enabled writer must have
status .completed; return an error derived from microphoneOnlyWriter.error or
unfinalizedWriterError when microphoneOnlyWriter is not completed, even if
assetWriter completed. Ensure microphone-only writer failures are reported as
finalization failures rather than allowing success.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ea586b5-d4db-49df-93c6-6de935b522f9

📥 Commits

Reviewing files that changed from the base of the PR and between 54ae801 and 723a563.

📒 Files selected for processing (7)
  • electron/ipc/recording/mac.ts
  • electron/ipc/recording/mp4Integrity.test.ts
  • electron/ipc/recording/mp4Integrity.ts
  • electron/ipc/register/recording.ts
  • electron/native/ScreenCaptureKitRecorder.swift
  • electron/native/bin/darwin-arm64/recordly-screencapturekit-helper
  • electron/native/bin/darwin-x64/recordly-screencapturekit-helper

Comment on lines +287 to +290
if (await isUnfinalizedMp4(candidatePath)) {
console.error("[mac-recover] Capture file was never finalized:", candidatePath);
return null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return the interrupted-recording failure to the caller.

This branch returns null, which makes stop-native-screen-recording report “No native screen recording is active” when the helper already produced an unfinalized file. The dedicated recovery IPC handler similarly reports that no recoverable output exists.

Return { success: false, message, unfinalizedPath: candidatePath } from this branch. This preserves the failed-recording state and the raw path without passing the file to muxing or the editor.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import type { ChildProcessWithoutNullStreams } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/ipc/recording/mac.ts` around lines 287 - 290, Update the
unfinalized-file branch in the recording recovery helper around isUnfinalizedMp4
to return { success: false, message, unfinalizedPath: candidatePath } instead of
null. Preserve the existing console error and ensure this failure result
propagates to both stop-native-screen-recording and the dedicated recovery IPC
handler without muxing or editor processing.

Comment on lines +408 to +418
if let systemAudioWriter, systemAudioWriter.status == .writing {
systemAudioInput?.markAsFinished()
await systemAudioWriter.finishWriting()
}

microphoneOnlyInput?.markAsFinished()
await microphoneOnlyWriter?.finishWriting()
if let microphoneOnlyWriter, microphoneOnlyWriter.status == .writing {
microphoneOnlyInput?.markAsFinished()
await microphoneOnlyWriter.finishWriting()
}

let finalizeFailure: Error? = assetWriter.flatMap { $0.status == .completed ? nil : ($0.error ?? unfinalizedWriterError(status: $0.status)) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Report failed microphone writer completion.

When both audio sources are enabled, Lines 345-351 write microphone audio only to microphoneOnlyWriter. If that writer fails, Lines 413-416 skip finalization and Line 418 still returns success when the video writer completed. The later mux failure is non-fatal, so the completed recording can silently omit requested microphone audio.

Include every enabled writer in the completion check. Return a finalization error when microphoneOnlyWriter.status is not .completed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/native/ScreenCaptureKitRecorder.swift` around lines 408 - 418,
Update the finalization logic around systemAudioWriter, microphoneOnlyWriter,
and finalizeFailure so every enabled writer must have status .completed; return
an error derived from microphoneOnlyWriter.error or unfinalizedWriterError when
microphoneOnlyWriter is not completed, even if assetWriter completed. Ensure
microphone-only writer failures are reported as finalization failures rather
than allowing success.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant