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.
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.
Address two issues raised during review:
P1 – When a recording is cancelled or restarted, setRecordingState(false)
enqueues its cursor batch but store-recorded-session is never called,
leaving a stale batch that contaminates the next recording's telemetry.
Add discardLatestPending() to the buffer and a discard-cursor-telemetry
IPC handler; the renderer now calls it on the discard path.
P2 – takeNextBatch() dequeued the batch before fs.writeFile, so a write
failure would permanently lose the telemetry. Wrap the write in
try/catch and re-insert the batch via prependBatch() on failure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- LaunchWindow: render sr-only <select> when webcamExpanded but
cameraDevices.length === 0 (loading/error/empty), so keyboard users
always have a focusable element even in no-camera states
- useCameraDevices.test: add error-branch test asserting error message,
empty devices array and isLoading=false when enumerateDevices rejects
- useCameraDevices: remove getUserMedia label probe to avoid conflict with
useScreenRecorder acquiring the real stream; use enumerateDevices only and
fall back to 'Camera <id>' for unlabeled devices; gate effect on enabled flag
- LaunchWindow: fix selectedCameraLabel to reflect loading/error/empty states
in the collapsed view (was always showing 'Default Camera')
- LaunchWindow: unify webcam <select> to a single always-mounted element
(sr-only when unavailable); mirrors the mic selector pattern
- useCameraDevices.test.ts: re-seed mockGetUserMedia in beforeEach after
vi.resetAllMocks(); update permission test to assert fallback label behavior
- useCameraDevices: remove selectedDeviceId from useEffect deps (use ref instead)
- useCameraDevices: fall back to first available device when selected device is unplugged
- i18n: add missing keys (audio.defaultMicrophone, webcam.defaultCamera, webcam.searching) to en/es/zh-CN
- LaunchWindow: replace hardcoded strings with t() i18n calls
- tests: add afterEach(vi.resetAllMocks()), improve permission test assertions, add stale device fallback test
- Add useCameraDevices hook to enumerate video input devices
- Update useScreenRecorder to support webcamDeviceId selection
- Add device selector UI above HUD bar (mic + webcam, hover-to-expand)
- All selectors and HUD bar are absolute-positioned to prevent layout shifts
- Increase HUD window to 600x200px to accommodate device panels
- Add unit tests for useCameraDevices hook