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>
- Implement native bridge for Windows cursor capture via PowerShell/C#
- Add cursor-free capture using getDisplayMedia with setDisplayMediaRequestHandler
- Update video player and exporters to support native cursor telemetry
- Enable system audio capture on Windows via WASAPI loopback
- Add interpolation for smoother cursor movement in playback and export
- Improve cursor scaling and visibility handling in editor and playback
Microphone permission is checked at startup via getMediaAccessStatus, and
camera has a dedicated request-camera-access IPC handler, but screen
recording relied entirely on desktopCapturer.getSources() to implicitly
trigger the TCC prompt — causing the permission dialog to reappear on
every launch (issue #558).
Note: askForMediaAccess() only accepts "microphone" | "camera"; screen
recording TCC is triggered via desktopCapturer.getSources() instead.
Fix:
- Import desktopCapturer in main.ts
- Call getMediaAccessStatus("screen") in app.whenReady(); trigger the
TCC prompt via getSources when status is "not-determined"
- Add request-screen-access IPC handler symmetric to request-camera-access
discardLatestPending() popped whichever batch happened to be at the
back of the queue. With a Stop → Record → Discard sequence, the
pending queue can have recording B's batch sitting in front of A's by
the time A's finalize callback resolves (because finalizeRecording
awaits fixWebmDuration), so the discard targets the wrong recording.
Tag each completed batch with the recording id supplied at
startSession() time and replace discardLatestPending() with
discardBatch(recordingId). takeNextBatch() now returns the full
{recordingId, samples} shape so prependBatch() can re-queue it on
write-failure without losing the id. The renderer already owns a
stable recordingId (Date.now() in useScreenRecorder) and the IPC
surface threads it through set-recording-state and
discard-cursor-telemetry.
Adds a regression test that mirrors FabLrc's scenario in PR #457:
two recordings finalize, A is discarded after B has already been
queued, and the buffer must drop A while keeping B intact.
Every consumer of /wallpapers/*.jpg — SettingsPanel, VideoPlayback,
frameRenderer — was doing async IPC round trips, useEffect dances, and
Promise.all for a value that is a build-time constant per process. Each
consumer showed briefly-empty or briefly-404ing state on first paint
until the handler's reply resolved.
The asset base URL depends only on process.defaultApp and
process.resourcesPath / __dirname — all available in preload at
context-bridge time. Compute once there, expose as a sync string.
- preload.ts resolves baseDir (process.resourcesPath packaged,
<appRoot>/public unpackaged) and emits assetBaseUrl synchronously.
- get-asset-base-path IPC handler + main-process branching deleted.
- getAssetPath() is now sync. Returns string, not Promise<string>.
Throws AssetBaseUnavailableError (new) when electronAPI.assetBaseUrl
is missing — catastrophic preload failure, not silent 404.
- resolveImageWallpaperUrl() sync; same sync throw semantics.
- SettingsPanel: Promise.all + useState + useEffect collapse to one
useMemo. First paint has real URLs, no 18× ERR_FILE_NOT_FOUND, no
flicker.
- VideoPlayback: wallpaper-resolve useEffect collapses to useMemo.
- frameRenderer.setupBackground: drops the await.
- electronAPI type decls updated in both .d.ts files.
- 35 unit tests updated to reflect sync signature + new
AssetBaseUnavailableError contract.
Silent-fallback behavior from getAssetPath (returning /relative when
electronAPI failed) is gone. Renderers now surface preload failures
instead of rendering 404s.
Three independent defects plus one SSOT violation caused reported symptom
of image wallpapers rendering solid black in exported MP4/GIF while
appearing correctly in the editor preview.
Bug A — Dev-mode IPC handler returned <appPath>/public/assets/, but
wallpapers live at public/wallpapers/. No assets/ subdirectory exists in
source.
Bug B — FrameRenderer.setupBackground bypassed getAssetPath and did
window.location.origin + wallpaper, producing file:///wallpapers/*.jpg
404s in packaged Electron.
Bug C — setupBackground silently caught any background-load error and
filled black. Masked Bug B from the export pipeline; why the bug shipped.
Smell D — Asset layout asymmetric: public/wallpapers/ (dev) vs
resources/assets/wallpapers/ (packaged). assets/ subdirectory had no
other consumers.
Fixes:
- Unify asset layout. electron-builder extraResources now copies to
resources/wallpapers/ (no assets/). Main handler returns
<resourcesPath>/ packaged and <appPath>/public/ unpackaged. Same
convention in both modes: /wallpapers/x.jpg maps to <base>/wallpapers/x.jpg.
Nix package.nix mirror updated.
- New src/lib/wallpaper.ts module owns the wallpaper contract:
DEFAULT_WALLPAPER, classifyWallpaper (color/gradient/image), and
resolveImageWallpaperUrl (pure URL resolver, wraps getAssetPath).
BackgroundLoadError typed error for short-circuit detection.
- FrameRenderer.setupBackground uses the new helpers. Silent black
fallback removed; rethrows as BackgroundLoadError. Export pipeline
(VideoExporter + GifExporter) short-circuits encoder-retry loop on
BackgroundLoadError. VideoEditor catch site dispatches to translated
exportBackgroundLoadFailed toast.
- VideoPlayback editor preview consolidated onto the same helpers.
Three default-wallpaper path literals (useEditorHistory,
projectPersistence, VideoPlayback) collapsed onto DEFAULT_WALLPAPER.
- i18n: new errors.exportBackgroundLoadFailed key added to all seven
locales (en, zh-CN, zh-TW, es, fr, tr, ko-KR).
- Tests: 20 unit tests for wallpaper module (classifyWallpaper +
resolveImageWallpaperUrl branches + BackgroundLoadError).
videoExporter.browser.test.ts and gifExporter.browser.test.ts extended
with image-wallpaper happy path and BackgroundLoadError failure path.
Migration note: packaged users upgrading in place may retain an empty
resources/assets/ directory from the prior layout. Unreferenced at
runtime; cosmetic only. DMG/AppImage fresh installs get the new layout
directly.
- Add buildDialogOptions helper function to safely attach parent window only when valid and not destroyed
- Update all dialog calls (save-exported-video, open-video-file-picker, save-project-file, load-project-file) to use the helper
- Fix supportsWindowOpacity logic by removing || isWayland so Linux always follows no-opacity codepath
- Change incorrect Chromium feature name 'PipeWire' to 'WebRTCPipeWireCapturer' in main.ts
- Remove unused isWayland variable in handlers.ts
- Add pacman package build target for Arch Linux in electron-builder.json5
- Update build:linux script in package.json to include pacman target
- Fix dialog window issues on Wayland/Hyprland:
* Pass mainWindow reference to dialog.showSaveDialog and dialog.showOpenDialog in electron/ipc/handlers.ts
* Required for proper dialog functionality on Wayland compositors
* Previously dialogs opened without parent window attachment causing issues on Hyprland
Changes ensure:
- Correct video export on Arch Linux + Hyprland systems
- Ability to install via pacman package manager
- Improved compatibility with Wayland compositors