Long recordings (35-minute screencaps were the motivating case) fail at
the 99% "Finalizing" step with a RangeError once the muxed MP4 would
exceed V8's ~2 GiB per-ArrayBuffer limit. Both export paths accumulate
the whole file in renderer memory and round-trip it through IPC, so no
output size past that point can complete:
- Legacy: src/lib/exporter/muxer.ts uses mediabunny's BufferTarget,
which holds the entire MP4 in a single ArrayBuffer. finalize() → Blob
→ blob.arrayBuffer() → ipcRenderer.invoke('write-exported-video-to-
path', arrayBuffer, path) — every step wants a ≥2 GiB contiguous
allocation.
- Lightning: native-video-export-finish did fs.readFile(finalizedPath)
and shipped the bytes back to the renderer, which re-serialized them
again. Same ceiling.
This change moves the finished MP4 across the renderer↔main boundary
via a temp file instead of an ArrayBuffer:
- New electron/ipc/export/exportStream.ts manages streaming temp files
via fh.write(buf, 0, len, position) so out-of-order writes (moov box
rewrites, etc.) stay safe. Each session lives in a 0700 mkdtemp()
directory opened with O_CREAT | O_EXCL so a hostile local user on a
shared tempdir cannot pre-plant a symlink at the predicted path.
- New renderer-facing IPCs: export-stream-open/write/close,
finalize-exported-video (renames temp to final path, copy+unlink
fallback on EXDEV/EPERM/ENOTEMPTY with console.warn on leaked bytes),
mux-exported-video-audio-from-path (FFmpeg audio fallback that takes
a path instead of an ArrayBuffer), and discard-exported-temp. Every
handler validates the caller-supplied path against an owned-export-
paths registry before touching disk, so a compromised renderer cannot
route arbitrary filesystem paths into main-process deletes/moves.
- The muxer now picks mediabunny's StreamTarget automatically when the
Electron bridge is available (BufferTarget stays for tests and any
non-Electron callers). finalize() returns { mode, tempFilePath,
bytesWritten } or { mode, blob } so the exporter can branch.
- Exporters forward tempFilePath through ExportResult. Lightning's
finish returns the ffmpeg temp path directly; the FFmpeg audio
fallback forks on the muxer result type. modernVideoExporter's
Lightning success branch now accepts tempFilePath (previously it
checked blob only, which regressed every native export).
- VideoEditor.tsx dispatches on tempFilePath: finalize via the new IPC,
keep the temp in place when the save dialog is canceled so "Save
Again" still works without re-rendering, keep the pending-save entry
alive on non-canceled save failures, and discard the temp on unmount
or explicit clear. GIF and smoke-test code paths still use the
legacy Blob path unchanged.
- app.on('before-quit') also reaps any open streaming sessions via
cleanupAllExportStreams().
Chunk size is 16 MiB — well under Electron/Mojo IPC message limits
while keeping total writes low (~160 for a 2.5 GB export).
Tested locally: exported a 35:13 source (~2.7 GiB H.264 input) at
Original 1920×1080 + Balanced. Previously failed on finalize with a
RangeError; with this patch the Legacy pipeline produced a valid 3.7
GiB MP4 whose ffmpeg -i duration/streams match the source.
Addresses #194.
On macOS versions before Sonoma (14.0), SCStreamConfiguration does not
support the captureMicrophone/microphoneCaptureDeviceID selectors. The
native helper was throwing a fatal error, blocking recording entirely
when microphone was enabled.
Now the Swift helper logs MICROPHONE_CAPTURE_UNAVAILABLE and continues
capture without microphone. The Node handler detects this signal and
returns microphoneFallbackRequired to the renderer, which starts a
browser-side MediaRecorder capturing the mic via getUserMedia. On stop,
the audio blob is saved as a .mic.webm sidecar alongside the native
recording, where the existing companion audio detection picks it up.
Fixes: 'Native microphone capture is unavailable on this macOS/Xcode
runtime' error on older macOS versions.
- Remove dead time= progress matching (no decode pass = no progress output)
- Remove stale comments about ffmpeg success/fallback behavior
- Lower timeout from 30s to 5s (header read is near-instant)
- Drop maxBuffer override (minimal stderr output now)
- Add -hide_banner to reduce stderr noise
- Remove -f null - from probeMediaDurationSeconds() ffmpeg args
so the command always exits non-zero (no output file specified)
- Without -f null -, ffmpeg exits code 1 and stderr lands in the
catch block where Duration is parsed, fixing the bug where
duration returned 0 on valid recordings
- probeMediaDurationSeconds returning 0 caused the
videoDuration > 0 gate to skip all audio sync correction,
leaving system audio misaligned in the final recording
When deleting the Whisper model failed, the frontend kept stale state
(whisperModelDownloadStatus stayed as 'downloaded'), which blocked
re-downloading because the download handler saw the model as already
present. On the backend, no progress event was sent on failure, leaving
the renderer out of sync.
Frontend: reset whisperModelDownloadStatus and progress to idle on
delete failure so re-download is unblocked.
Backend: verify whether the file was actually removed despite the error
and send appropriate progress events in either case.
Closes#152
When recording a window on a secondary monitor with a different DPI,
getNormalizedCursorPoint() used the primary display's scale factor for
both cursor and window-bounds conversion. This produced incorrect
normalised coordinates. Now resolves the display that contains the
target window and uses that display's scale factor instead.
Fixes#204
Use F_OK instead of X_OK for Windows fs.access check (X_OK is meaningless
on Windows). Wrap spawn() in try/catch to prevent uncaught exception if the
binary is missing or blocked by antivirus.
On non-macOS platforms, platform APIs (iohook, GetWindowRect, xwininfo)
return physical pixel coordinates while Electron returns DIP coordinates.
Apply scaleFactor correction so cursor normalization uses a consistent
coordinate space. Fixes ~30-40px cursor offset on Windows 11 and Linux
Mint when display scaling is >100%.
Also fixes winget-releaser workflow (use v2 tag instead of invalid @latest).
- Remove AppleScript 'front window' bounds query which returned wrong window
for multi-window apps (e.g. Arc highlighting invisible auxiliary window)
- Use resolveMacWindowBounds() which queries by exact CGWindowID
- Keep AppleScript 'activate' call for bringing the app to front
Catch errors from desktopCapturer.getSources() in the get-sources IPC
handler and return an empty array instead of crashing the handler.
Prevents repeated 'Failed to get sources' errors when screen recording
permission is not granted (common in dev mode on macOS 26).
- finalizeStoredVideo now catches validateRecordedVideo errors instead of
propagating them, so the editor opens even if ffmpeg validation fails
(restores v1.1.3/v1.1.5 resilient behavior)
- Use a single Date.now() call for all native capture filenames on macOS
to prevent timestamp drift between video/audio files
- Add CGPreflightScreenCaptureAccess pre-check in Swift helper
- Add microphone TCC pre-flight when mic capture is requested
- Warm up TCC via desktopCapturer.getSources before spawning helper
- Request mic access from Electron before helper spawn
- Remove unreliable systemPreferences.getMediaAccessStatus pre-checks
- Add camera entitlements to plist files for webcam support
- Guard getScreen() calls behind app.isReady()
- Make tray icons lazy to avoid early screen access
- Suppress duplicate permission error alerts in renderer
- Hardened IPC with safety guards for all webContents.send calls
- Scrubbed sensitive absolute paths from caption logs
- Realigned Windows artifact naming logic