Resolved conflicts:
- LaunchWindow.tsx: kept upstream's removal of openVideoFile/openProjectFile (moved to Studio Dashboard)
- VideoEditor.tsx: used upstream's doLoadProject rename while preserving getProjectFolder() arg
- Replace lastEmittedAssetId with a process-wide Set<String> so each unique
cursor shape is serialised at most once even across non-adjacent repeats
(e.g. arrow → text → arrow no longer resends the arrow bitmap)
- Wrap the sampling loop body in autoreleasepool{} to prevent Cocoa objects
(NSBitmapImageRep, PNG Data, base64 String) from accumulating for the
lifetime of the helper during long recordings
- Update stale Accessibility comments: missing Accessibility only disables
text/pointer affordance detection; native bitmap capture is unaffected
Capture the real system cursor image during macOS recording so custom and
default cursors render natively instead of being mapped to bundled SVGs,
bringing macOS in line with the Windows WGC capture path.
- macOS cursor helper grabs NSCursor.currentSystem as a PNG asset (SHA256 id,
intrinsic scale factor, pixel hotspot); the bitmap payload is emitted once per
shape and referenced by assetId thereafter
- helper returns nil cursorType instead of an arrow fallback so default/custom
cursors fall through to the captured bitmap while text/pointer stay beautified
- MacNativeCursorRecordingSession collects deduped assets, tags samples with
assetId, and reports provider "native" when bitmaps are captured
Adds `projectFolder` as a key to user preferences, stores the location of
the most recently opened project, and prefills it in the next File → Open
action. Mirrors the pattern from #512 (`exportFolder`).
Closes#668
Maps KeyboardEvent.key values (e.g., "ArrowDown", " ") to their
Electron accelerator equivalents (e.g., "Down", "Space") to prevent
global shortcut registration failures.
Addresses the review feedback on #658 (CodeRabbit + Codex) and the
structural notes from the quality pass.
Correctness:
- Compute the recorder's streaming state at finalize time, not at
construction. A stream that fails to open is now reported as
not-streamed, so its buffered chunks are saved as a complete in-memory
fallback instead of being dropped (was total data loss on open failure).
- Await every in-flight chunk write before onstop resolves, so the main
process never closes the write stream while a final chunk is still in
flight (was truncating the tail of a recording under load).
- Open the disk write stream by awaiting its 'open' event, so a bad path
or permission error rejects up front instead of being acknowledged as
success and then silently dropping bytes.
- Close the stream and remove the partial file when a streamed recording
is discarded or fails, so cancelled/failed runs don't leak descriptors
or orphan partial recordings.
- Surface a mid-stream write failure as a rejected recording rather than
saving a silently truncated file.
Structure:
- Extract the streaming concern into electron/ipc/recordingStream.ts
(RecordingStreamRegistry) and src/hooks/recorderHandle.ts, out of the
2.8k-line handlers.ts and the screen-recorder hook.
- Key write streams by output file name, removing the implicit
recordingId/+1 contract that spanned the IPC boundary.
- Collapse the duplicated screen/webcam finalize blocks into one helper
and the repeated duration-validity guard into one check; patch the
screen and webcam durations in parallel.
Adds unit tests for the registry (real temp-dir fs) and the recorder
handle state machine (open-failure fallback, in-order writes awaited
before stop, mid-stream failure). Extends the vitest include glob to
collect electron-side tests.
Verified: tsc --noEmit clean; biome clean; vitest 180/180.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Recordings longer than ~10 minutes silently fail to save (#616). The
renderer buffers the whole WebM as a Blob[], then on stop makes several
in-memory copies (fixWebmDuration -> arrayBuffer -> Buffer.from) before
writing. A long 1080p recording duplicates hundreds of MB several times
in the renderer, exceeds Electron's memory limit, and the renderer
crashes silently with no file saved.
Two changes:
1. Stream chunks to disk (originally @Amanuel2x's contribution in #617).
Open an fs.WriteStream in the main process at recording start and send
each ~1s ondataavailable chunk straight to disk over two new IPC calls
(open-recording-stream, append-recording-chunk), so the renderer never
holds more than a single chunk. A full in-memory fallback is preserved
for environments where the IPC stream cannot open.
2. Patch the WebM Duration header on disk after the stream closes. Browser
MediaRecorder writes WebM with no Duration element, so streamed files
save with duration=N/A and the editor's seek bar, timeline, and any
scrub/trim break. A new electron/recording/webm-duration.ts module
rewrites the Duration element, writing to a temp file and renaming in
place so a crash mid-write cannot corrupt the recording.
Streaming is opt-in: the screen recorder and the browser-only webcam
recorder stream to disk; native-capture webcam sidecars (Windows, macOS)
keep buffering in-memory, since their finalize path reads the recorder
blob directly to attach the webcam track.
Verified: tsc --noEmit clean; biome clean; vitest 166/166.
Closes#616
Supersedes #617
Co-Authored-By: Amanuel <amanuel@localboostnetworking.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Record browser webcam sidecar when native Windows capture is active.
Add native webcam sidecar output and DirectShow NV12/YUY2 fallback.
Sample exported webcam frames by source timestamp.
- Add nativeCursorClipRef div (outside preserve-3d) with CSS inset() clip-path that
tracks the camera-transformed video boundary, including border-radius
- Add cameraAwareMaskRect() in FrameRenderer that computes the same boundary for
Canvas 2D clip in the export path; remove stage-clamping so rounded corners match
the preview's inset() behavior when zoom/pan pushes the mask off-stage
- Cache maskBorderRadius in LayoutCache so both shadow and direct composite paths
can apply camera-aware rounded clipping
- Fix double mask.x offset introduced by nativeCursorMaskRef; replace mask div with
clip-path on the outer wrapper
- Normalize cursor size relative to maskRect.width so preview and export scale match
- Clip cursor to canvas boundary and hide on non-recorded display
- Wire cursorClipToBounds flag through FrameRenderConfig and VideoExporter