isAllowedLocalReadPath now resolves the candidate via fs.realpathSync and requires both the lexical and canonical paths to satisfy the policy, so a symlink placed under an allowed prefix that points outside the allowlist is rejected. The redundant 'either resolved or realResolved is allowed' check in read-local-file and generate-wallpaper-thumbnail is removed since the function canonicalizes internally. Adds a regression test that creates such a symlink (skipping when Windows refuses to create it without Developer Mode).
build-whisper-runtime now only soft-fails when invoked from postinstall, in CI, or with WHISPER_RUNTIME_ALLOW_MISSING=1. Direct 'npm run build*' invocations fail loudly when CMake is missing and no bundled runtime is staged so we don't ship release builds with broken auto-captioning.
isAllowedLocalReadPath previously returned true for any existing path because of an existsSync fast-path, which made the read-local-file IPC handler and the local media URL policy effectively allow reading arbitrary files on disk. Drop the existsSync bypass so only paths under app-managed directories or paths that have been explicitly approved (via dialogs, exports, recording sessions, etc.) are accepted. Adjust the local media path policy tests to cover the new behaviour.
Also make build-whisper-runtime fall back to bundled artifacts when CMake is missing (mirroring build-windows-capture) so npm ci does not fail on machines without a C++ toolchain.
Exports of recordings whose muxed output exceeds 2 GiB failed with
RangeError [ERR_FS_FILE_TOO_LARGE]: Node's fs.readFile rejects files
larger than kIoMaxLength (2 ** 31 - 1). The legacy export pipeline hit
this in muxExportedVideoAudioBuffer, which called
await fs.readFile(finalized.outputPath)
to ship the muxed bytes back to the renderer.
Mirror the path-based contract that mux-exported-video-audio-from-path
already uses:
- muxExportedVideoAudioBuffer now returns { outputPath, metrics } and
collects byte size via fs.stat instead of fs.readFile. The unmuxed
intermediate is still cleaned up; the muxed output is left for the
IPC handler to register and the renderer to finalize.
- The mux-exported-video-audio IPC handler registers the muxed output
via registerOwnedExportPath and returns { tempPath, metrics }.
- preload.ts and electron-env.d.ts: tempPath replaces data in the
renderer-facing return type.
- videoExporter.ts and modernVideoExporter.ts (the
finalizeExportWithFfmpegAudio fallback paths) now return
{ tempFilePath } so VideoEditor's existing finalize-exported-video
flow handles the move — the same path the modern stream-mode export
already takes.
The renderer already preferred tempFilePath over blob in
VideoEditor.tsx for MP4 saves (with the explicit comment "avoids ever
allocating a multi-GiB ArrayBuffer in the renderer"), so this just
removes the buffer-mode regression for large legacy exports.
Adds electron/ipc/export/native-video.test.ts asserting the new
contract: muxExportedVideoAudioBuffer returns a path, never calls
fs.readFile, and still records muxedVideoBytes via stat.
Closes#380
On macOS, the screen highlight overlay's right and bottom edges were
running off-screen. macOS clamps window positions below the menu bar,
so the outward padding only appeared on the left/top while the
right/bottom extended beyond the visible area.
Use display workArea instead of full bounds for screen highlights on
macOS, and keep the border/glow within the overlay window.
Previously the smoke-export harness only accepted a raw MP4 input path.
Validating the export pipeline on a real editor project (with its own
zoom regions, wallpaper, annotations, and webcam state) required
opening the editor by hand and clicking through the GUI.
This adds three new environment variables that are picked up by the
existing smoke-export query string plumbing:
- RECORDLY_SMOKE_EXPORT_PROJECT — path to a .recordly project file;
when set, the editor opens it via openProjectFileAtPath and applies
the saved state before the auto-export fires.
- RECORDLY_SMOKE_EXPORT_QUALITY — overrides the hard-coded "good"
quality used by the auto-export trigger; accepts medium / good /
high / source.
- RECORDLY_SMOKE_EXPORT_FPS — overrides the frame rate; accepts
24 / 30 / 60.
No behavior change for existing RECORDLY_SMOKE_EXPORT_INPUT runs;
the new inputs are strictly additive and optional. The startup log
line prefers the project path over the raw-input path when both are
populated.
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.
BrowserWindow.setBounds() with x/y is silently ignored on Wayland (the
compositor owns window placement), so the IPC-driven HUD drag never
moved the window \u2014 it stayed put even though pointer events fired and
the cursor changed to grabbing.
Mark the drag handle as -webkit-app-region: drag on Linux (only in the
IPC/overlay mode; webcam-preview mode still uses the in-window CSS
transform) so the OS performs a real xdg_toplevel.move. Mirror the
resulting bounds into hudUserPosition via the window 'moved' event so
commit a78f7d4's in-place resize behavior keeps working on Linux.