Repository navigation
[Bug]: macOS — the writer dies 75 s into a take, ScreenCaptureKit keeps delivering frames for 22 more minutes, and the HUD keeps counting #621
Description
Search existing issues
- I have searched the existing issues
Describe the bug
A take that ran 23 min 20 s produced a 74-second file.
The write path died about 75 seconds in. ScreenCaptureKit kept capturing and kept delivering healthy frames for another 22 minutes. The HUD kept counting up the whole time. Nothing told the user the recording had effectively already ended — the loss was completely silent and was only discoverable afterwards, from the filesystem and the unified log.
I believe v1.10.0 is affected too: nothing in that release or on main since looks like it addresses this, and #363 explicitly scoped macOS out.
Environment: OpenScreen 1.9.6 (com.etiennelescot.openscreen), macOS 26.6.2 (25G83), Apple M4, arm64. Screen + USB_MIC capture, 1920×1080, no camera.
Note on timestamps. Absolute dates and clock times are redacted throughout. Times are given as offsets from the take's start (T0). In the quoted log lines only the leading timestamp has been substituted with its T0 offset — everything after it is verbatim from
log show --predicate 'process == "replayd"'. Filenames are redacted where they embed a timestamp (see §1).
1. The output file stops at 74 s but is not truncated
The take's recording-<redacted>.mp4 under ~/Library/Application Support/openscreen/recordings/. The numeric suffix is redacted because it is an epoch-milliseconds start timestamp, which is how T0 is known:
- fragmented MP4:
ftyp,wide,moovat offset 1410192, then 73moofand 75mdatboxes — roughly one fragment per second of the 74 s take - the box lengths sum to exactly the file size (74953463 bytes) → the writer stopped cleanly at a fragment boundary; this is not a partial or torn write
ffprobe: duration 74.0 s, 1920×1080, h264r_frame_rate=54, + aac 48 kHz stereoffmpeg -v warning -i f -f null -→ exit 0, zero errorsffprobe -count_frames→ 3998 frames = 54.0 fps across 74 s, consistent end to end- mtime froze at T0+75 s and never advanced again
- there is no
.session.jsonbeside it → the save path never ran for this take. The immediately preceding take has both a.session.jsonand a project file, so this is specific to the failed take, not to the build.
2. replayd reports the session healthy for the entire 23 minutes
266 Health: heartbeats between T0+71 s and T0+23:16, one every ~5 s, all from the same session:
screenframeCount251–297 per 5 s window (≈50–59 fps) throughout — no gap, no degradationaudioFrameCount245–255 per window throughout_screenTimeDriftSeconds=0.00000000000000000000on all 266
First heartbeat after the file stopped growing (T0+71 s):
[T0+71s] Df replayd[1177:d86ca6] [INFO] -[SCCaptureSession setupHealthMonitor]_block_invoke:1197 Health: captureSession=0x883624000 streamID=<private> screenframeCount=278 _audioFrameCount=245 _screenTimeSeconds=1369618.893588 _audioTimeSeconds=1369618.826602 _screenTimeDriftSeconds=0.00000000000000000000 compositeSessionMode=0 cameraframeCount=0 compositeFrameCount=0
Last one, 22 minutes later (T0+23:16):
[T0+23:16] Df replayd[1177:d87a2a] [INFO] -[SCCaptureSession setupHealthMonitor]_block_invoke:1197 Health: captureSession=0x883624000 streamID=<private> screenframeCount=280 _audioFrameCount=255 _screenTimeSeconds=1370943.993019 _audioTimeSeconds=1370943.926603 _screenTimeDriftSeconds=0.00000000000000000000 compositeSessionMode=0 cameraframeCount=0 compositeFrameCount=0
3. The session was only torn down when the client process exited
At T0+23:20, four seconds after the last heartbeat:
[T0+23:20] Df replayd[1177:d87a2a] [INFO] -[RPClient stop:streamID=<redacted>] Success stopping stream
[T0+23:20] Df replayd[1177:d87a2a] [INFO] -[SCScreenCaptureSession dealloc] self=0x8823b86e0
[T0+23:20] Df replayd[1177:d87a2a] [INFO] -[SCCaptureSession dealloc] session=0x883624000
[T0+23:20] Df replayd[1177:d87a2a] [com.apple.ReplayKit:RPDaemon] -[AVCaptureDeviceInput dealloc] <AVCaptureDeviceInput: 0x881a1c720 [USB_MIC]>
[T0+23:20] Df replayd[1177:d87a2a] [com.apple.xpc:connection] [0x8830fca80] invalidated because the client process (pid 54137) either cancelled the connection or exited
The producer side was alive and healthy from T0 to T0+23:20. The consumer stopped persisting at T0+75 s.
4. Ruled out: this was not storage- or OS-level loss
I checked every avenue by which the missing 22 minutes could still have existed somewhere:
- no file larger than 1 MB written anywhere on the data volume between T0+75 s and T0+23:20 (only unrelated third-party apps)
/private/var/vm/contains no swapfiles at all — nothing was paged out- no core dumps in
/cores/ tmutil listlocalsnapshots /→ no local APFS snapshots, so no filesystem-level restore pointlsof +L1→ no deleted-but-still-open handles, so nothing was readable off a live fd- the
recordings/directory's own mtime never advanced after the take began → no file was created there and later removed
So the frames produced after T0+75 s were delivered to the app and dropped. They were never written to any medium. This is not a data-recovery problem; it is a silent-failure problem.
Why this is not #252 / #292 / #327
Those three are Windows, and the symptom is a hang on stop ("still hangs on stop", "timed out waiting for native windows capture to stop", "video is not saving").
This is macOS, and the failure is mid-recording and silent: capture stays healthy, the writer dies, and the UI keeps counting for 22 minutes. The user gets no signal that the take ended. I could not find a macOS report for this, which is why I am filing one.
Relation to #338 and #363
#338 is the only reason any bytes survived — thank you. Without fragmented MP4 this take would have been an unreadable 0-second file instead of a clean, fully decodable 74-second one. But #338 says of itself: "What it does not change: the freeze. The user still sees a recording that stops on its own. That stays open." This is that open case, on macOS, at 23 minutes.
#363 scoped macOS out: "macOS fragments too and needs the same treatment, but it has no already-exited fast path and an unguarded stdin write, so it is its own change." It also listed under Deliberately not in this PR: "Detecting the mid-recording exit / stopping the HUD timer. The HUD does keep counting past a dead helper (observed: 02:12)." The observation there was 2 min 12 s; mine is 23 min 20 s. This report is the macOS evidence for both halves of that deferral — the salvage path and the dead-helper detection.
Impact
22 of 23 minutes of an unreproducible recording were lost. Because the HUD looked healthy throughout, there was no opportunity to notice and restart.
Expected behavior
One of:
- the writer recovers and the take is complete; or
- capture is stopped as soon as the writer dies, and the user is told the take ended — with whatever was salvaged opened in the editor rather than discarded (the fix(recording): open the recording a failed stop left playable #363 fall-through, applied to macOS).
What should not happen is 22 minutes of HUD counting over a dead writer with no user-visible signal, followed by a take that silently does not exist.
Ideally the two outcomes are separable in the field: #363 notes the helper already emits a container field nobody read, which is what makes "was this file supposed to survive a kill?" answerable. A recovered: true flag on macOS would let a bug report distinguish a clean stop from a salvaged one here too.
To Reproduce
I cannot reproduce this on demand — it happened once, in a single 23-minute take, and I have no reliable trigger. This report is a forensic reconstruction from the artefacts that survived, not a reproduction.
What the artefacts pin down is the failure shape, which should be enough to look for it directly:
- Start a macOS screen + microphone recording.
- Make the write path fail while capture continues (whatever fix(recording): write fragmented MP4 so a killed helper keeps its file #338 was defending against — the helper's writer dying rather than being killed).
- Observe: the
.mp4stops growing,replaydkeeps logging healthyHealth:heartbeats at ~55 fps, the HUD keeps counting, and no.session.jsonis ever written.
The cheap instrumentation to confirm it in the field: compare the output file's mtime against elapsed recording time. A file whose mtime is minutes behind the HUD is a dead writer, and today nothing performs that comparison.
OS
macOS
OS Version
26.6.2 (build 25G83)
Device Type
Desktop
Other Device
Mac mini (Apple M4, arm64)
Additional context
- App version at the time of the failure: 1.9.6. Since upgraded to 1.10.0; not re-tested, as there is no reproduction.
- The 74-second salvage is intact and fully decodable, so fix(recording): write fragmented MP4 so a killed helper keeps its file #338 did its job. The gap is purely detection + the macOS salvage path.
- Everything quoted above comes from
log show --predicate 'process == "replayd"'scoped to the take's window; the 266-heartbeat count is over that whole window, not a sample. - Related: fix(recording): write fragmented MP4 so a killed helper keeps its file #338, fix(recording): open the recording a failed stop left playable #363, [Bug]: v1.8.0 native Windows recorder still hangs on stop; next attempt says capture is not running #252, [Bug]: timed out waiting for native windows capture to stop. Record could not save #292, [Bug]: The video is not saving from the page, and this error appears. #327.
Activity
makewheels commentedon Sep 7, 2026
Follow-up with a code-level finding. Two parts: (1) confirmation from the shipped build that the macOS gap described above is real and unchanged in v1.10.0, and (2) a tested defect in the salvage predicate of #571 — the fix that would close this — which rejects the artefact from this report.
1. The macOS stop path in the shipped v1.10.0 build
Read out of the installed
dist-electronmain bundle (2.7 MB) and cross-referenced againstelectron/ipc/handlers.ts.The
stop-native-mac-recordinghandler:catchreturns{ success: false, error }immediately..session.jsonmanifest inside thetry, after the stop acknowledgement is awaited — so a failed stop means the manifest is never written. That is precisely the artefact state in this report: a complete, fully decodable MP4 on disk with no manifest beside it.stdin.write("stop\n")against the helper, with no already-exited fast path. This is the "unguarded stdin write" that fix(recording): open the recording a failed stop left playable #363 named when it scoped macOS out.The Windows handler immediately above it has all three of the missing pieces: a playability check that falls through into the save path, a
recoveredflag on the IPC result, and distinct success/recovered messages.On detection of a dead writer: string search over the whole main bundle finds
watchdog0 times,isWriterAlive0,helperExited0 (exited:appears 5 times, all in the Windows stop path). Bundling renames locals, so treat the counts as supporting rather than conclusive — but the handler bodies above are conclusive on their own: nothing anywhere compares output-file growth against elapsed recording time, which is why the HUD in this report counted to T0+23:20 over a writer that stopped at T0+75 s.2. #571's salvage predicate rejects the artefact from this report
#571 is the right shape of fix and its CI is green, so this is offered as review input, not a competing change.
The predicate in
electron/recording/nativeMacCaptureStop.tsis:I ran that logic verbatim (mp4box 2.3.0, the version
package-lock.jsonpins; I also confirmed 2.4.1 behaves identically) against five files:ftyp free mdat moovempty_moovftyp moov mdat moof …empty_moovftyp moov moof mdat …movie.duration === 0empty_moovparseFailed, mp4boxonErroron the final chunkmoovat endTwo independent failure modes:
(a)
empty_moovmakesmovie.duration === 0. That is by design — withempty_moovthe timing lives in the fragments andmvhd.durationis 0. SoparsedMovie.duration > 0rejects a perfect, fully finalized fragmented file. Rows 2 and 3 above come from a single ffmpeg invocation with identical content and duration; the only difference is-movflags +empty_moov. One passes, one is rejected.(b) A never-finalized fragmented file makes mp4box emit an error. On the artefact from this report, mp4box logs
Invalid box typewhile appending the final chunk (72 of 72 at 1 MiB each),onErrorfires, and theparseFailedguard rejects the file outright.Mode (b) is the one that matters here, because a truncated fragmented file is exactly what this salvage path exists for — #338's stated outcome is that "the same kill leaves a file that plays up to the last complete fragment". The predicate rejects its own target case.
The artefact is not corrupt
To rule out "the file is genuinely broken", here is the structural verification:
ftyp,wide,moovat offset 1410192, then 73moof+ 75mdat), and the box lengths sum to exactly the file size, 74953463 bytesmoovsubtree fully valid: twotraks each with completemdia/minf/stbl(stsd stts stss sdtp stsc stsz stco), plusmvexwith twotrex— i.e. a real sample table for the head, and a fragmentation declarationmoofsubtree valid:mfhd, twotrafs withtfhd/trunffmpeg -v warning -i f -f null -→ exit 0, zero errorsffprobe -count_frames→ 3998 frames = 54.0 fps across the 74 s durationSo the container is well-formed and every frame decodes. mp4box's chunked parser is what gives up.
Scoping this honestly
Two caveats, so this is not overstated:
empty_moov— the artefact'smoovcarries a realstblwith 57 samples. So mode (a) is likely latent rather than active for OpenScreen's own output. Mode (b) is the one that actually bit.moovat the end (3 top-level boxes) and passes the predicate. A verification sample that happens to be a degraded plain MP4 would therefore pass and give false confidence — which may be what the 18 MB sample in fix(mac): 【openscreen】録画終了時の保存失敗を救済する #571's testing notes was.Suggested direction
#363 decided readability with ffprobe, not mp4box — its ablation table reports seconds and packet counts. v1.10.0 does not ship the ffmpeg CLI on macOS (only
libavcodec.62.dylib/libavformat.62.dylib/libavutil.60.dylibbeside the helper), butmaingained it in "fix(build): ship the ffmpeg CLI in the macOS .app so transcription works without a system ffmpeg" (committed 2026-09-06). So either the bundled ffprobe or the already-linked libavformat is available, and both tolerate a truncated fragmented file where mp4box'sparseFailedguard does not.If it helps, I can supply the full box dump of the artefact, or a reduced fixture: the mode-(a) pair is two files from one ffmpeg command differing only in
-movflags, and mode (b) reproduces by taking such a file and truncating it at a fragment boundary.Refs #571, #363, #338.
EtienneLescot commentedon Sep 27, 2026
Thanks again for this detailed report. In the current release, 1.13.0, when the writer dies mid-take the app stops the take and tells you, instead of letting the HUD count on (#655). Recovering the frames already written is not implemented yet (#571 was closed).
Could you retry with 1.13.0 and tell us whether a take still stops early? If it does, the unified log lines you pulled last time would help again.
EtienneLescot commentedon Oct 10, 2026
Fixed by #655 and #662 (the dead writer is detected, the take stops and is recovered), shipped in 1.13.0. Please reopen if it happens again on the current release.