Skip to content

fix(macos): recover the take a dead writer or a killed helper left on disk - #662

Merged
EtienneLescot merged 2 commits into
mainfrom
fix/macos-salvage-dead-writer-take
Sep 14, 2026
Merged

fix(macos): recover the take a dead writer or a killed helper left on disk#662
EtienneLescot merged 2 commits into
mainfrom
fix/macos-salvage-dead-writer-take

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

When a macOS stop fails, the file left on disk is still a playable fragmented MP4. The stop fails when the writer died (#655 then stops the take) or when the helper was killed or crashed. The take was reported as lost; it is now recovered and opens in the editor.

What a failed take leaves on disk. A clean stop rewrites the file flat (ftyp mdat moov). A failed one keeps AVAssetWriter's fragmented shape:

ftyp  mdat  moov[… mvex]  (mdat moof)*  wide  mdat
  • Fragments. Each fragment's mdat comes before the moof that indexes it (absolute tfhd base offset). The final mdat is the fragment that was still open, and it may declare size 0.
  • Readers. ffmpeg, libavformat, Chromium and the real editor all open this as it is, with the right duration.

Changes

  • electron/recording/nativeMacCaptureSalvage.ts (new).
    • Box walk. It walks the top-level boxes by their own offsets and counts the video frames whose bytes are entirely in the file: the flat moov sample table for the first second, then every fragment's trun with tfhd/trex defaults.
    • No mp4box. fix(mac): 【openscreen】録画終了時の保存失敗を救済する #571 fed mp4box in 1 MiB chunks. mp4box aborted whenever a chunk boundary fell inside that size-0 tail, so a take was kept or lost depending on its byte length. mp4box also accepted a file cut inside a moof.
    • Torn tail. A torn tail (a moof or header cut short) makes a file that opens nowhere. It is truncated to where it starts and inspected again, so success always means the file on disk opens as it is.
    • Rejected files are not touched.
  • Stop handler.
    • When. It salvages only when the stop failed and the helper has exited (a running helper may still be writing).
    • Save path. The normal path follows: cursor telemetry, session manifest, media links.
    • Result and warning. The result carries recovered: true. The warning keeps the helper's sentence and says how much was kept, e.g. "Recording stopped after 0:35: the video file could not be written (Disk Full). The part recorded until then was saved." It reaches the editor and the CLI through the hand-off from fix(macos): settle the native stop on the helper's exit, not its first error #661.
    • Side writes. For a recovered take, the cursor telemetry and session manifest writes cannot undo the recovery. A recovered take most often follows a full disk, so a failed side write is logged and its partial file removed.

Related issue

Refs #621. Replaces the salvage half of #571 (closed).

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

None. The only UI change is the existing warning toast, now also shown for a recovered take.

Testing

End to end with the real helper. Mac mini M1, macOS 26.5. The helper was built from main, recording 3840×2160@60 at 76.5 Mbps with system audio. The take went through the real stop and salvage modules with the stop handler's glue.

Scenario Stop result Salvage ffmpeg on the recovered file
Helper SIGKILL 12 s in helper-failed, exited 626 frames, 11.0 s, no truncation, 5 ms 626 video packets, 11.01 s, decodes clean
Writer died, disk full (40 MB image), 36 s in helper-failed, exited 1992 frames, 34.98 s, no truncation, 16 ms 1993 video packets, 35.01 s, decodes clean

The one-frame gap in the disk-full take is the last frame. The last moof indexes 12 samples in the tail mdat that the full disk cut short, and the 57th sample's bytes are only partly on disk. ffmpeg lists that packet; the salvage counts only complete frames.

Fixtures. Five real takes from the helper with every mdat payload zeroed and gzipped (3–44 KB), kept in electron/recording/__fixtures__/macos-fmp4/:

  • two writer deaths, one of them inside the first second, with no moof;
  • two killed helpers;
  • one clean flat take.

Zeroing moves no box. The frame counts the tests expect are ffmpeg's packet counts on the original files.

Unit tests (nativeMacCaptureSalvage.test.ts). The inspection matches ffmpeg on all five takes, and every case below comes from a real file:

Case Expected result
Size-0 tail mdat accepted
Final mdat cut short accepted
Cut inside the last moof truncated, then 1596 frames (ffmpeg reads exactly this cut)
Cut inside the first moof 58 frames
Half-written trailing header truncated
Torn moov rejected, file untouched
Cut before moov rejected
Not an MP4 rejected

More unit tests.

  • Salvage gate: salvage is refused unless the helper exited.
  • Warning wording: checked on the helper's verbatim disk-full message, its -16364 message and a bare SCStream NSError.

Suite. Full vitest passes (2,825 tests); both tsc projects and Biome are clean.

Adversarial review. A four-lens, read-only review ran on the first commit (box parsing, truncation safety, stop integration, tests and claims), with two independent skeptics per finding.

  • Truncation safety held, measured on copies. Across 36 torn and trailing-garbage variants, truncation never lowered ffmpeg's packet count on a file that already opened. Every file it cut, ffmpeg read afterwards, and a rejected file was never modified.
  • Two findings survived, both fixed in the second commit:
    • After a successful salvage, a full disk could fail the cursor/manifest writes and turn the recovery back into an ENOSPC error.
    • The warning dropped "the video file could not be written" for the -16364 writer death, whose NSError description is generic.

Not tested:

  • a torn moof produced by a real crash (only synthetic cuts of real files);
  • Intel Macs;
  • recovery with a webcam clip attached.

Summary by CodeRabbit

  • Bug Fixes
    • Improved macOS recording recovery when stopping a capture fails, helping preserve playable recordings from incomplete files.
    • Added clearer recovery status and warning messages, including recovered recording duration.
    • Reduced the chance that secondary metadata or cursor-data write failures discard an otherwise usable recording.
  • Tests
    • Added coverage for recording inspection, recovery, malformed files, truncated captures, and user-facing recovery messages.

… disk

When a macOS stop fails — the writer died, or the helper was killed — the file
on disk is a fragmented MP4 that ffmpeg, libavformat, Chromium and the editor
all open as it is. The take was still reported as lost. This keeps it.

electron/recording/nativeMacCaptureSalvage.ts walks the file's top-level boxes
by their own offsets and counts the video frames whose bytes are actually in the
file: the flat sample table the moov holds for the first second, then every
fragment's trun, with tfhd/trex defaults. It does not use mp4box. PR #571 did, in
1 MiB chunks, and AVAssetWriter's last mdat carries a size field of 0, so mp4box
aborted whenever a chunk boundary fell inside it and a take was kept or lost
depending on its byte length; it also accepted a file cut inside a moof, which
nothing can open. A torn tail — a moof, or a header, cut short — is truncated to
where it starts and the file inspected again, so success always means the file
opens as it is on disk. A final mdat cut short is the fragment still being
written and is left alone.

The stop handler only salvages when the helper has exited (a running helper may
still be writing), then takes the normal save path: cursor telemetry, session
manifest and media links are written, the result carries `recovered`, and the
warning names how much was kept ("Recording stopped after 0:35: Disk Full. The
part recorded until then was saved."). The warning reaches the editor and the CLI
through the session hand-off from #661.

Fixtures are five real takes from the helper with every mdat payload zeroed and
gzipped (3-44 KB): two writer deaths, two killed helpers, one clean flat take.
The frame counts the tests expect are ffmpeg's on the original files, and the
torn cuts reproduce layouts ffmpeg refuses (cut inside a moof) and accepts once
cut back.
…y what failed

Review of the salvage found two defects.

A recovered take most often follows a disk that filled up, and the cursor
telemetry and session manifest are written to the same volume right after. Either
write throwing ENOSPC reached the stop handler's catch, so a take whose video had
just been recovered was reported lost with a raw errno. For a recovered take those
writes are now secondary: a failure is logged and its partial file removed, and
the stop still succeeds with the warning.

The warning replaced the helper's sentence with the NSError's localized
description. For the -16364 writer death that description is AVFoundation's
generic "The operation could not be completed", so the user was never told the
video file could not be written. The sentence is now kept, and a description is
added only when it says something: "Recording stopped after 0:35: the video file
could not be written (Disk Full)." The tests use the helper's verbatim messages
instead of an invented one, and the length is rounded rather than floored (a 35 s
take's frame durations sum to 34.98 s, which printed "0:34").

Also: whether to salvage is `nativeMacSalvageTarget`, tested, instead of an inline
condition in the handler — salvage truncates, and the helper-exited check is what
keeps it off a file still being written. The module doc no longer says every torn
box breaks a file: a torn moof does; a half-written trailing header is cut as a
precaution.
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 07712524-73da-41b6-a3b8-a9f06d31389a

📥 Commits

Reviewing files that changed from the base of the PR and between 42115ad and 746503f.

⛔ Files ignored due to path filters (5)
  • electron/recording/__fixtures__/macos-fmp4/clean-flat-8s.mp4.gz is excluded by !**/*.gz
  • electron/recording/__fixtures__/macos-fmp4/helper-killed-29s.mp4.gz is excluded by !**/*.gz
  • electron/recording/__fixtures__/macos-fmp4/helper-killed-9s-main.mp4.gz is excluded by !**/*.gz
  • electron/recording/__fixtures__/macos-fmp4/writer-died-1s-no-moof.mp4.gz is excluded by !**/*.gz
  • electron/recording/__fixtures__/macos-fmp4/writer-died-4s.mp4.gz is excluded by !**/*.gz
📒 Files selected for processing (4)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/recording/nativeMacCaptureSalvage.test.ts
  • electron/recording/nativeMacCaptureSalvage.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Native macOS recording recovery

Layer / File(s) Summary
MP4 inspection and salvage
electron/recording/nativeMacCaptureSalvage.ts
Adds MP4 structure inspection, flat and fragmented sample counting, torn-tail truncation, duration formatting, salvage target selection, and recovery message generation.
Failed-stop recovery integration
electron/ipc/handlers.ts, electron/electron-env.d.ts
The stop handler salvages eligible failed captures, tolerates recovered side-write failures, and returns recovery status and warnings through the preload contract.
Salvage behavior validation
electron/recording/nativeMacCaptureSalvage.test.ts
Adds coverage for valid, truncated, malformed, and unrecoverable captures, target selection, and recovery descriptions.

Priority: ➖ Normal

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant NativeMacRecorder
  participant StopHandler
  participant CaptureSalvage
  participant RecordingFiles
  NativeMacRecorder->>StopHandler: stop recording
  StopHandler->>CaptureSalvage: inspect failed capture
  CaptureSalvage-->>StopHandler: repaired capture and duration
  StopHandler->>RecordingFiles: write recovered sidecar files
  StopHandler-->>NativeMacRecorder: recovered status and warning
Loading

Suggested reviewers: olamide226

Merge Risk: ⚪ Minimal · up to 74650

The recovery path correctly preserves fragment offsets and contains sidecar cleanup failures, with no identified issue blocking merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and matches the repository template. It explains the recovery behavior, related issues, change type, release impact, platform impact, testing, limitations, and review findi…
Title check ✅ Passed The title clearly summarizes the main change: recovering macOS recordings left on disk after a dead writer or killed helper.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/macos-salvage-dead-writer-take

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.

@EtienneLescot
EtienneLescot merged commit 527b27e into main Sep 14, 2026
21 of 22 checks passed
@EtienneLescot
EtienneLescot deleted the fix/macos-salvage-dead-writer-take branch September 14, 2026 15:37
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