Maps KeyboardEvent.key values (e.g., "ArrowDown", " ") to their
Electron accelerator equivalents (e.g., "Down", "Space") to prevent
global shortcut registration failures.
The per-column fillRect skipped columns with amplitude < 0.5 px, causing
the waveform to disappear entirely at certain zoom levels. Revert to the
closed polygon + top-edge stroke which always renders something.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. topInset/bottomInset both set to 3 — items are center-aligned in the
36 px row (3 px gap top and bottom), so the drawing zone must match.
topInset=0 was drawing 3 px above the trim box top.
2. Replace closed polygon with per-column fillRect so silent sections
(barH < 0.5 px) produce no pixels instead of a flat green baseline.
The top-edge stroke lifts the pen across silent gaps.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the single `verticalInset` CSS prop (which tried to shrink the
canvas element) with `topInset`/`bottomInset` drawing-space offsets.
The canvas remains `absolute inset-0` (full row height); the polygon
baseline is drawn at `H - bottomInset` and the amplitude is scaled to
`drawHeight * 0.9` — so the waveform is guaranteed to stay within
[topInset, H - bottomInset] regardless of sub-pixel CSS rounding.
Trim row: topInset=0, bottomInset=6 (row minHeight 36 − item height 30)
eliminates the faint green line that was visible below the trim box.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The trim item content is 30 px tall inside a 36 px row, leaving 3 px of
vertical padding top and bottom. Previously the canvas used `inset-0` so
the waveform filled the full row height. A new `verticalInset` prop
(defaulting to 0) offsets the canvas top/bottom edges; the trim row
passes `verticalInset={3}` so the waveform top and bottom align exactly
with the trim box bounds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the bipolar ±center polygon with a rectified half-waveform:
amplitude per column is max(|min|, |max|) and the filled shape rises
from the bottom of the row upward, matching the single-sided waveform
display common in audio/video editors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch the trim-row waveform from faint white vertical strokes to a
green filled polygon (upper max edge left→right, lower min edge
right→left) with crisp edge strokes — matching the look of Final Cut
Pro / DaVinci Resolve waveform displays.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codex re-review: if openRecordingStream exists but appendRecordingChunk
does not (renderer/main version skew), the recorder would open the stream
and switch to streaming mode, but every append silently no-ops and the
save ends up empty. Require both IPC methods before streaming; otherwise
fall back to in-memory buffering. Adds a regression test.
Verified: tsc --noEmit clean; biome clean; vitest 183/183.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses the CodeRabbit + Codex re-review of the prior commit.
- Normalize a rejected append (channel/handler error, not just a
{ success: false } result) into appendError, so the write queue never
rejects and isStreaming() stays consistent after a failure (CodeRabbit).
- Handle a rejected open-stream IPC the same as a failed open: fall back
to in-memory buffering instead of leaving the recorder stuck "pending"
with an unhandled rejection (CodeRabbit).
- Discard a streamed webcam whose write failed even when the screen save
succeeds. The cleanup gate is now per-recorder, so a webcam omitted from
a successful screen-only save no longer leaks its stream and partial
file (Codex).
Adds tests for the rejected-append and rejected-open paths.
Verified: tsc --noEmit clean; biome clean; vitest 182/182.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Biome had reformatted two pre-existing VideoFrame constructor calls from
single-line to multi-line style when --write was run on the file. Restore
them to match main so the diff only reflects our intentional refactoring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Make loadLocalSourceFile and loadRemoteSourceFile static so they can be
called without an instance. loadSourceFile applies withTimeout around
each static call. loadFileAsArrayBuffer delegates to the same static
methods and extracts ArrayBuffer + contentType from the blob, eliminating
the duplicated IPC/fetch branching logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restore loadSourceFile as a thin router (local vs remote) and extract
the IPC and fetch paths into dedicated private methods, each containing
the original logic from main.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change return type to { data, contentType } so the fetched Content-Type
header is available to callers. loadSourceFile uses it to set the Blob
MIME type for https:/blob: URLs instead of always falling back to
application/octet-stream. useAudioPeaks destructures only { data }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- computePeaksInWorker now accepts an AbortSignal; terminates the worker
immediately on abort and rejects with AbortError so orphaned workers
cannot outlive the effect cleanup
- useAudioPeaks wires an AbortController into the effect; controller.abort()
fires alongside the existing cancelled flag on URL change or unmount
- loadSourceFile handles data: URLs by parsing the MIME type, deriving a
safe filename (e.g. video.mp4), and setting Blob type correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add docstrings to all undocumented functions and methods across the
files touched in this branch: getAudioCtx, Row component, and all
StreamingVideoDecoder methods (loadSourceFile, computeSegments,
getExportMetrics, splitBySpeed, getDemuxer, cancel, destroy, withTimeout).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the peaks null-check after clearRect so a previously drawn
waveform is erased when peaks transitions to null (e.g. on URL change
or decode failure) rather than lingering on screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use 剪輯 (editing/clip) instead of 修剪 (trimming) to match the term
already used in the trim section throughout zh-TW strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reset peaks to null immediately when an uncached URL is set, so the
previous video's waveform is not shown during decode. Also clear on
decode failure so stale data does not persist indefinitely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename to BackgroundWaveform to reflect its visual role rather than
structural placement. Remove the redundant wrapper div — Row already
provides relative overflow-hidden, so the canvas can sit absolute
inset-0 directly and observe itself via ResizeObserver.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add timeline.title and timeline.waveform keys to every settings.json
locale file. Replace all hardcoded "Timeline" and "Show Audio Waveform
on Trim Track" strings in SettingsPanel with t() calls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move computePeaks into audioPeaksWorker.ts — runs off the main thread
with zero-copy channel buffer transfer
- Replace module-level peaksCache Map with a useRef scoped to the hook
instance — no global mutable state, toggle off/on still instant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>