From 46f384d4d02a4ce5b8c5438bf9716335b1ffe2e9 Mon Sep 17 00:00:00 2001 From: Wiii <156531153+meiiie@users.noreply.github.com> Date: Sat, 25 Apr 2026 17:47:39 +0700 Subject: [PATCH 1/5] fix(recording): use native audio start timestamps for Windows sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit  --- electron/ipc/ffmpeg/filters.test.ts | 62 ++++++++++++++++- electron/ipc/ffmpeg/filters.ts | 40 +++++++++++ electron/ipc/recording/diagnostics.test.ts | 12 ++++ electron/ipc/recording/diagnostics.ts | 21 ++++-- electron/ipc/recording/windows.ts | 40 ++++++++--- electron/ipc/types.ts | 2 +- .../bin/win32-x64/helpers-manifest.json | 38 +++++------ electron/native/bin/win32-x64/wgc-capture.exe | Bin 92160 -> 95744 bytes electron/native/wgc-capture/src/main.cpp | 64 ++++++++++++++++++ .../wgc-capture/src/wasapi_loopback.cpp | 29 +++++++- .../native/wgc-capture/src/wasapi_loopback.h | 6 ++ 11 files changed, 277 insertions(+), 37 deletions(-) diff --git a/electron/ipc/ffmpeg/filters.test.ts b/electron/ipc/ffmpeg/filters.test.ts index deab8017..bc28a3ca 100644 --- a/electron/ipc/ffmpeg/filters.test.ts +++ b/electron/ipc/ffmpeg/filters.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from "vitest"; -import { appendSyncedAudioFilter, getAudioSyncAdjustment } from "./filters"; +import { + appendSyncedAudioFilter, + applyRecordedAudioStartDelay, + getAudioSyncAdjustment, +} from "./filters"; describe("getAudioSyncAdjustment", () => { it("does not speed up longer audio tracks that would advance speech", () => { @@ -30,6 +34,15 @@ describe("getAudioSyncAdjustment", () => { }); }); + it("pads trailing silence instead of prepending extreme delay for very short audio tracks", () => { + expect(getAudioSyncAdjustment(600, 480)).toEqual({ + mode: "pad", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: 120000, + }); + }); + it("does not inject atempo when longer audio stays on the anchored path", () => { const filterParts: string[] = []; appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(120, 122.5)); @@ -47,4 +60,51 @@ describe("getAudioSyncAdjustment", () => { "[1:a]atempo=0.975000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", ]); }); + + it("pads the tail for very short audio tracks", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(600, 480)); + + expect(filterParts).toEqual([ + "[1:a]apad=pad_dur=120.000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); +}); + +describe("applyRecordedAudioStartDelay", () => { + it("pads the tail when recorded metadata says audio started on time", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 110), 0)).toEqual({ + mode: "pad", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: 10000, + }); + }); + + it("prefers a measured start delay over a tempo-only heuristic", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 119.8), 275)).toEqual({ + mode: "delay", + delayMs: 275, + tempoRatio: 1, + durationDeltaMs: 200, + }); + }); + + it("applies a measured start delay even when durations already match", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 120), 275)).toEqual({ + mode: "delay", + delayMs: 275, + tempoRatio: 1, + durationDeltaMs: 0, + }); + }); + + it("leaves tempo correction alone when recorded metadata says there was no late start", () => { + expect(applyRecordedAudioStartDelay(getAudioSyncAdjustment(120, 117), 0)).toEqual({ + mode: "tempo", + delayMs: 0, + tempoRatio: 0.975, + durationDeltaMs: 3000, + }); + }); }); diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts index ce973f16..735e3c66 100644 --- a/electron/ipc/ffmpeg/filters.ts +++ b/electron/ipc/ffmpeg/filters.ts @@ -1,5 +1,7 @@ import type { AudioSyncAdjustment, PauseSegment } from "../types"; +const MAX_AUDIO_SYNC_DELAY_MS = 15000; + export function buildAtempoFilters(tempoRatio: number): string[] { if (!Number.isFinite(tempoRatio) || tempoRatio <= 0) { return []; @@ -58,9 +60,43 @@ export function getAudioSyncAdjustment( return { mode: "tempo", delayMs: 0, tempoRatio, durationDeltaMs }; } + if (durationDeltaMs > MAX_AUDIO_SYNC_DELAY_MS) { + return { mode: "pad", delayMs: 0, tempoRatio: 1, durationDeltaMs }; + } + return { mode: "delay", delayMs: durationDeltaMs, tempoRatio: 1, durationDeltaMs }; } +export function applyRecordedAudioStartDelay( + adjustment: AudioSyncAdjustment, + recordedStartDelayMs?: number | null, +): AudioSyncAdjustment { + if (!Number.isFinite(recordedStartDelayMs) || (recordedStartDelayMs ?? 0) < 0) { + return adjustment; + } + + const delayMs = Math.max(0, Math.round(recordedStartDelayMs ?? 0)); + if (delayMs > 20) { + return { + mode: "delay", + delayMs, + tempoRatio: 1, + durationDeltaMs: adjustment.durationDeltaMs, + }; + } + + if (adjustment.mode !== "delay" && adjustment.mode !== "pad") { + return adjustment; + } + + return { + mode: "pad", + delayMs: 0, + tempoRatio: 1, + durationDeltaMs: adjustment.durationDeltaMs, + }; +} + export function appendSyncedAudioFilter( filterParts: string[], inputLabel: string, @@ -77,6 +113,10 @@ export function appendSyncedAudioFilter( filters.push(...buildAtempoFilters(adjustment.tempoRatio)); } + if (adjustment.mode === "pad" && adjustment.durationDeltaMs > 0) { + filters.push(`apad=pad_dur=${formatFfmpegSeconds(adjustment.durationDeltaMs)}`); + } + filters.push("aresample=async=1:first_pts=0", "asetpts=PTS-STARTPTS"); filterParts.push(`${inputLabel}${filters.join(",")}[${outputLabel}]`); } diff --git a/electron/ipc/recording/diagnostics.test.ts b/electron/ipc/recording/diagnostics.test.ts index 9a775339..dca8f3fc 100644 --- a/electron/ipc/recording/diagnostics.test.ts +++ b/electron/ipc/recording/diagnostics.test.ts @@ -166,6 +166,18 @@ describe("getCompanionAudioFallbackPaths", () => { }); }); + it("ignores invalid sidecar timing metadata values", async () => { + const micPath = path.join(tempRoot, "recording.mic.wav"); + await Promise.all([ + fs.writeFile(micPath, "mic"), + fs.writeFile(`${micPath}.json`, JSON.stringify({ startDelayMs: -250 })), + ]); + + const { getCompanionAudioStartDelayMs } = await import("./diagnostics"); + + await expect(getCompanionAudioStartDelayMs(micPath)).resolves.toBeNull(); + }); + it("rejects tiny MP4 container-only outputs before they reach the editor", async () => { const videoPath = path.join(tempRoot, "recording-123.mp4"); await fs.writeFile(videoPath, Buffer.alloc(261)); diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts index 4588631e..fb7b6c4a 100644 --- a/electron/ipc/recording/diagnostics.ts +++ b/electron/ipc/recording/diagnostics.ts @@ -119,6 +119,16 @@ async function readCompanionAudioTimingMetadata( } } +export async function getCompanionAudioStartDelayMs(companionPath: string) { + const metadata = await readCompanionAudioTimingMetadata(companionPath); + const startDelayMs = metadata?.startDelayMs; + if (!Number.isFinite(startDelayMs) || (startDelayMs ?? 0) < 0) { + return null; + } + + return Math.round(startDelayMs ?? 0); +} + export async function hasEmbeddedAudioStream(videoPath: string) { const ffmpegPath = getFfmpegBinaryPath(); let stderr = ""; @@ -172,22 +182,19 @@ export async function getCompanionAudioFallbackInfo(videoPath: string) { const metadataEntries = await Promise.all( paths.map(async (audioPath) => { - const metadata = await readCompanionAudioTimingMetadata(audioPath); - const startDelayMs = metadata?.startDelayMs; - if (!Number.isFinite(startDelayMs) || (startDelayMs ?? 0) < 0) { + const startDelayMs = await getCompanionAudioStartDelayMs(audioPath); + if (!Number.isFinite(startDelayMs)) { return null; } - return [audioPath, Math.round(startDelayMs ?? 0)] as const; + return [audioPath, startDelayMs] as const; }), ); return { paths, startDelayMsByPath: Object.fromEntries( - metadataEntries.filter( - (entry): entry is readonly [string, number] => entry !== null, - ), + metadataEntries.filter((entry): entry is readonly [string, number] => entry !== null), ), }; } diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index 4a32f86c..612cc9f0 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -7,24 +7,29 @@ import { BrowserWindow } from "electron"; import { getFfmpegBinaryPath } from "../ffmpeg/binary"; import { appendSyncedAudioFilter, + applyRecordedAudioStartDelay, buildPausedAudioFilter, getAudioSyncAdjustment, normalizePauseSegments, } from "../ffmpeg/filters"; import { getWindowsCaptureExePath } from "../paths/binaries"; import { + selectedSource, setWindowsCaptureProcess, + setWindowsCaptureStopRequested, + setWindowsNativeCaptureActive, windowsCaptureOutputBuffer, + windowsCaptureStopRequested, windowsCaptureTargetPath, windowsNativeCaptureActive, - setWindowsNativeCaptureActive, - windowsCaptureStopRequested, - setWindowsCaptureStopRequested, - selectedSource, } from "../state"; import type { AudioSyncAdjustment, PauseSegment } from "../types"; import { moveFileWithOverwrite } from "../utils"; -import { probeMediaDurationSeconds, validateRecordedVideo } from "./diagnostics"; +import { + getCompanionAudioStartDelayMs, + probeMediaDurationSeconds, + validateRecordedVideo, +} from "./diagnostics"; import { emitRecordingInterrupted } from "./events"; const execFileAsync = promisify(execFile); @@ -191,9 +196,21 @@ export async function muxNativeWindowsVideoWithAudio( if (videoDuration > 0) { for (let i = 0; i < audioFilePaths.length; i++) { const audioDuration = await probeMediaDurationSeconds(audioFilePaths[i]); - const adjustment = getAudioSyncAdjustment(videoDuration, audioDuration); + const recordedStartDelayMs = await getCompanionAudioStartDelayMs(audioFilePaths[i]); + const adjustment = applyRecordedAudioStartDelay( + getAudioSyncAdjustment(videoDuration, audioDuration), + recordedStartDelayMs, + ); audioAdjustments.set(audioInputs[i], adjustment); - if (adjustment.mode === "tempo") { + if (Number.isFinite(recordedStartDelayMs) && adjustment.mode === "delay") { + console.log( + `[mux-win] ${audioInputs[i]} audio recorded a start delay of ${adjustment.delayMs}ms`, + ); + } else if (Number.isFinite(recordedStartDelayMs) && adjustment.mode === "pad") { + console.log( + `[mux-win] ${audioInputs[i]} audio started on time but ends ${adjustment.durationDeltaMs}ms early — padding trailing silence`, + ); + } else if (adjustment.mode === "tempo") { console.log( `[mux-win] ${audioInputs[i]} audio differs from video by ${adjustment.durationDeltaMs}ms — applying tempo ratio ${adjustment.tempoRatio.toFixed(6)}`, ); @@ -201,6 +218,10 @@ export async function muxNativeWindowsVideoWithAudio( console.log( `[mux-win] ${audioInputs[i]} audio appears to start late by ${adjustment.delayMs}ms — adding leading silence`, ); + } else if (adjustment.mode === "pad" && adjustment.durationDeltaMs > 0) { + console.log( + `[mux-win] ${audioInputs[i]} audio is much shorter than video by ${adjustment.durationDeltaMs}ms — padding trailing silence`, + ); } } } @@ -326,7 +347,10 @@ export async function muxNativeWindowsVideoWithAudio( for (const audioPath of [systemAudioPath, micAudioPath]) { if (audioPath) { - await fs.rm(audioPath, { force: true }).catch(() => undefined); + await Promise.all([ + fs.rm(audioPath, { force: true }).catch(() => undefined), + fs.rm(`${audioPath}.json`, { force: true }).catch(() => undefined), + ]); } } } diff --git a/electron/ipc/types.ts b/electron/ipc/types.ts index 5922fb0e..7c7de9c1 100644 --- a/electron/ipc/types.ts +++ b/electron/ipc/types.ts @@ -156,7 +156,7 @@ export type UiohookModuleNamespace = { }; export type AudioSyncAdjustment = { - mode: "none" | "tempo" | "delay"; + mode: "none" | "tempo" | "delay" | "pad"; delayMs: number; tempoRatio: number; durationDeltaMs: number; diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index dbae2bae..0ae7bb77 100644 --- a/electron/native/bin/win32-x64/helpers-manifest.json +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -1,21 +1,21 @@ { - "version": 1, - "platform": "win32", - "arch": "x64", - "helpers": { - "wgc-capture": { - "binaryName": "wgc-capture.exe", - "binarySha256": "013164c0a1391d334e5aa2fe0ff2e47b507407e36c7e44e7333a70a84b944643", - "sourceDir": "electron/native/wgc-capture", - "sourceFingerprint": "9e9bce082266ca5968cf5f0b535b469d47c4ec3a775a93726171c0dfdbcdaa44", - "updatedAt": "2026-03-29T02:15:34.516Z" - }, - "cursor-monitor": { - "binaryName": "cursor-monitor.exe", - "binarySha256": "b0732abc06998a40c3e95078465ad750a6169901944571c39cfd7996effe39c0", - "sourceDir": "electron/native/cursor-monitor", - "sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e", - "updatedAt": "2026-03-29T02:15:38.286Z" - } - } + "version": 1, + "platform": "win32", + "arch": "x64", + "helpers": { + "wgc-capture": { + "binaryName": "wgc-capture.exe", + "binarySha256": "bb4c2aa4141e81e1a05b54bd49dbb114eb3a5ff4a666bf7d26525a9da585128b", + "sourceDir": "electron/native/wgc-capture", + "sourceFingerprint": "5bb02a69049a02909d38188cae1dfdfb94fd49465b51ed6ab78a98092b7520cc", + "updatedAt": "2026-04-25T09:17:16.237Z" + }, + "cursor-monitor": { + "binaryName": "cursor-monitor.exe", + "binarySha256": "b0732abc06998a40c3e95078465ad750a6169901944571c39cfd7996effe39c0", + "sourceDir": "electron/native/cursor-monitor", + "sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e", + "updatedAt": "2026-03-29T02:15:38.286Z" + } + } } diff --git a/electron/native/bin/win32-x64/wgc-capture.exe b/electron/native/bin/win32-x64/wgc-capture.exe index e5d0df8f950c17f9e636425c14859c4abdadf1e7..d16a07249454d55cb8b491009e83fcd084f78b66 100644 GIT binary patch literal 95744 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 M!i2fd|1H1&1;R3X5dZ)H literal 92160 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%0DA?9T#Gn^I=$4 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 E9}3l_aR2}S diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index c949e8a2..7fca95b8 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include static std::atomic g_stopRequested{false}; static std::atomic g_pauseRequested{false}; @@ -156,6 +157,43 @@ static std::wstring utf8ToWide(const std::string& str) { return wstr; } +static void writeCompanionAudioTimingMetadata( + const std::string& audioPath, + int64_t firstVideoTimestampHns, + const WasapiCapture& capture +) { + if (audioPath.empty() || firstVideoTimestampHns < 0) { + return; + } + + const int64_t firstPacketQpcHns = capture.firstPacketQpcHns(); + if (firstPacketQpcHns < 0) { + return; + } + + int64_t startDelayMs = (firstPacketQpcHns - firstVideoTimestampHns + 5000) / 10000; + if (startDelayMs < 0) { + startDelayMs = 0; + } + + std::ofstream metadataFile(audioPath + ".json", std::ios::trunc); + if (!metadataFile) { + std::cerr << "WARNING: Failed to write audio timing metadata for " << audioPath << std::endl; + return; + } + + metadataFile << "{\"startDelayMs\":" << startDelayMs; + const uint32_t discontinuityCount = capture.dataDiscontinuityCount(); + if (discontinuityCount > 0) { + metadataFile << ",\"dataDiscontinuityCount\":" << discontinuityCount; + } + const uint32_t timestampErrorCount = capture.timestampErrorCount(); + if (timestampErrorCount > 0) { + metadataFile << ",\"timestampErrorCount\":" << timestampErrorCount; + } + metadataFile << "}"; +} + static void stdinListenerThread() { std::string line; while (std::getline(std::cin, line)) { @@ -250,9 +288,12 @@ int main(int argc, char* argv[]) { // Set up frame callback std::atomic frameCount{0}; + std::atomic firstVideoTimestampHns{-1}; std::atomic recordingStartedAnnounced{false}; session.setFrameCallback([&](ID3D11Texture2D* texture, int64_t timestampHns) { g_lastFrameTimestampHns = timestampHns; + int64_t expectedFirstVideoTimestampHns = -1; + firstVideoTimestampHns.compare_exchange_strong(expectedFirstVideoTimestampHns, timestampHns); if (g_stopRequested) return; if (g_pauseRequested) return; @@ -337,6 +378,19 @@ int main(int argc, char* argv[]) { if (audioActive) loopback.stop(); if (micActive) micCapture.stop(); + if (audioActive) { + writeCompanionAudioTimingMetadata( + config.audioOutputPath, + firstVideoTimestampHns.load(), + loopback); + } + if (micActive) { + writeCompanionAudioTimingMetadata( + config.micOutputPath, + firstVideoTimestampHns.load(), + micCapture); + } + if (frameCount.load() <= 0) { std::cerr << "ERROR: No video frames were captured before stop" << std::endl; DeleteFileW(outputPathW.c_str()); @@ -350,9 +404,19 @@ int main(int argc, char* argv[]) { std::cout << "Recording stopped. Output path: " << config.outputPath << std::endl; if (audioActive) { + if (loopback.dataDiscontinuityCount() > 0 || loopback.timestampErrorCount() > 0) { + std::cerr << "WARNING: System audio timing metadata includes discontinuities=" + << loopback.dataDiscontinuityCount() + << " timestampErrors=" << loopback.timestampErrorCount() << std::endl; + } std::cout << "Audio path: " << config.audioOutputPath << std::endl; } if (micActive) { + if (micCapture.dataDiscontinuityCount() > 0 || micCapture.timestampErrorCount() > 0) { + std::cerr << "WARNING: Microphone timing metadata includes discontinuities=" + << micCapture.dataDiscontinuityCount() + << " timestampErrors=" << micCapture.timestampErrorCount() << std::endl; + } std::cout << "Mic path: " << config.micOutputPath << std::endl; } std::cout.flush(); diff --git a/electron/native/wgc-capture/src/wasapi_loopback.cpp b/electron/native/wgc-capture/src/wasapi_loopback.cpp index 1659d013..e4008def 100644 --- a/electron/native/wgc-capture/src/wasapi_loopback.cpp +++ b/electron/native/wgc-capture/src/wasapi_loopback.cpp @@ -137,6 +137,9 @@ bool WasapiCapture::start() { } totalDataBytes_ = 0; + firstPacketQpcHns_ = -1; + dataDiscontinuityCount_ = 0; + timestampErrorCount_ = 0; writeWavHeader(outputFile_, 0); HRESULT hr = audioClient_->Start(); @@ -250,13 +253,37 @@ void WasapiCapture::captureThread() { BYTE* data = nullptr; UINT32 numFrames = 0; DWORD flags = 0; + UINT64 devicePosition = 0; + UINT64 qpcPosition = 0; - hr = captureClient_->GetBuffer(&data, &numFrames, &flags, nullptr, nullptr); + hr = captureClient_->GetBuffer( + &data, + &numFrames, + &flags, + &devicePosition, + &qpcPosition); if (FAILED(hr)) { std::cerr << "WASAPI: GetBuffer failed hr=0x" << std::hex << hr << std::dec << std::endl; break; } + if ((flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) { + dataDiscontinuityCount_.fetch_add(1); + } + if ((flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) != 0) { + timestampErrorCount_.fetch_add(1); + } + if ( + numFrames > 0 && + qpcPosition > 0 && + (flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) == 0 + ) { + int64_t expected = -1; + firstPacketQpcHns_.compare_exchange_strong( + expected, + static_cast(qpcPosition)); + } + UINT32 totalSamples = numFrames * channels; if (flags & AUDCLNT_BUFFERFLAGS_SILENT) { diff --git a/electron/native/wgc-capture/src/wasapi_loopback.h b/electron/native/wgc-capture/src/wasapi_loopback.h index a4facf13..1792a863 100644 --- a/electron/native/wgc-capture/src/wasapi_loopback.h +++ b/electron/native/wgc-capture/src/wasapi_loopback.h @@ -19,6 +19,9 @@ public: bool pause(); bool resume(); void stop(); + int64_t firstPacketQpcHns() const { return firstPacketQpcHns_.load(); } + uint32_t dataDiscontinuityCount() const { return dataDiscontinuityCount_.load(); } + uint32_t timestampErrorCount() const { return timestampErrorCount_.load(); } private: bool initializeCommon(); @@ -41,4 +44,7 @@ private: DWORD streamFlags_ = 0; UINT32 bufferFrameCount_ = 0; + std::atomic firstPacketQpcHns_{-1}; + std::atomic dataDiscontinuityCount_{0}; + std::atomic timestampErrorCount_{0}; }; From 97ba2a020c69a31c6c5a48541ba36bb679a925ba Mon Sep 17 00:00:00 2001 From: Wiii <156531153+meiiie@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:15:51 +0700 Subject: [PATCH 2/5] fix(recording): normalize native Windows mic loudness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit  --- electron/ipc/ffmpeg/filters.test.ts | 26 ++++++++++++++++++++++++++ electron/ipc/ffmpeg/filters.ts | 14 +++++++++++++- electron/ipc/recording/windows.ts | 15 +++++++++++++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/electron/ipc/ffmpeg/filters.test.ts b/electron/ipc/ffmpeg/filters.test.ts index bc28a3ca..3569d729 100644 --- a/electron/ipc/ffmpeg/filters.test.ts +++ b/electron/ipc/ffmpeg/filters.test.ts @@ -69,6 +69,32 @@ describe("getAudioSyncAdjustment", () => { "[1:a]apad=pad_dur=120.000,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", ]); }); + + it("can add a small gain boost before resampling", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter( + filterParts, + "[1:a]", + "aout", + getAudioSyncAdjustment(120, 120), + 1.4, + ); + + expect(filterParts).toEqual([ + "[1:a]volume=1.400,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); + + it("can prepend mic normalization filters before sync handling", () => { + const filterParts: string[] = []; + appendSyncedAudioFilter(filterParts, "[1:a]", "aout", getAudioSyncAdjustment(120, 120), { + preFilters: ["loudnorm=I=-16:TP=-1.5:LRA=11"], + }); + + expect(filterParts).toEqual([ + "[1:a]loudnorm=I=-16:TP=-1.5:LRA=11,aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[aout]", + ]); + }); }); describe("applyRecordedAudioStartDelay", () => { diff --git a/electron/ipc/ffmpeg/filters.ts b/electron/ipc/ffmpeg/filters.ts index 735e3c66..e6fa1253 100644 --- a/electron/ipc/ffmpeg/filters.ts +++ b/electron/ipc/ffmpeg/filters.ts @@ -102,8 +102,12 @@ export function appendSyncedAudioFilter( inputLabel: string, outputLabel: string, adjustment: AudioSyncAdjustment, + options: number | { volumeMultiplier?: number; preFilters?: string[] } = 1, ) { - const filters: string[] = []; + const volumeMultiplier = + typeof options === "number" ? options : (options.volumeMultiplier ?? 1); + const preFilters = typeof options === "number" ? [] : (options.preFilters ?? []); + const filters: string[] = [...preFilters]; if (adjustment.mode === "delay" && adjustment.delayMs > 0) { filters.push(`adelay=${adjustment.delayMs}|${adjustment.delayMs}`); @@ -117,6 +121,14 @@ export function appendSyncedAudioFilter( filters.push(`apad=pad_dur=${formatFfmpegSeconds(adjustment.durationDeltaMs)}`); } + if ( + Number.isFinite(volumeMultiplier) && + volumeMultiplier > 0 && + Math.abs(volumeMultiplier - 1) > 0.0005 + ) { + filters.push(`volume=${volumeMultiplier.toFixed(3)}`); + } + filters.push("aresample=async=1:first_pts=0", "asetpts=PTS-STARTPTS"); filterParts.push(`${inputLabel}${filters.join(",")}[${outputLabel}]`); } diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index 612cc9f0..e0336413 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -33,6 +33,9 @@ import { import { emitRecordingInterrupted } from "./events"; const execFileAsync = promisify(execFile); +// Match the browser path's "usable speech level" intent with a standard +// loudness pass on native Windows mic audio instead of a fixed tiny boost. +const WINDOWS_NATIVE_MIC_PRE_FILTERS = ["loudnorm=I=-16:TP=-1.5:LRA=11"]; export async function isNativeWindowsCaptureAvailable(): Promise { if (process.platform !== "win32") return false; @@ -266,7 +269,9 @@ export async function muxNativeWindowsVideoWithAudio( const micLabel = micPauseFilter ? "[mic_trimmed]" : "[2:a]"; appendSyncedAudioFilter(filterParts, systemLabel, "s", systemAdjustment); - appendSyncedAudioFilter(filterParts, micLabel, "m", micAdjustment); + appendSyncedAudioFilter(filterParts, micLabel, "m", micAdjustment, { + preFilters: WINDOWS_NATIVE_MIC_PRE_FILTERS, + }); filterParts.push("[s][m]amix=inputs=2:duration=longest:normalize=0[aout]"); await execFileAsync( @@ -312,7 +317,13 @@ export async function muxNativeWindowsVideoWithAudio( filterParts.push(pauseFilter); } const srcLabel = pauseFilter ? "[trimmed_audio]" : "[1:a]"; - appendSyncedAudioFilter(filterParts, srcLabel, "aout", singleAdjustment); + appendSyncedAudioFilter( + filterParts, + srcLabel, + "aout", + singleAdjustment, + audioInputs[0] === "mic" ? { preFilters: WINDOWS_NATIVE_MIC_PRE_FILTERS } : 1, + ); await execFileAsync( ffmpegPath, From 624c3047beb3656b8127b8ab821e85bd88636a27 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:34:55 +1000 Subject: [PATCH 3/5] Improve HUD and update prompt UI --- electron/electron-env.d.ts | 14 +- electron/main.ts | 28 +- electron/preload.ts | 16 +- electron/updater.ts | 143 +++-- electron/windows.ts | 4 +- src/components/launch/LaunchWindow.module.css | 103 ++-- src/components/launch/LaunchWindow.tsx | 147 +---- src/components/launch/UpdateToastWindow.tsx | 528 ++++++++---------- src/components/video-editor/types.ts | 8 +- src/hooks/useScreenRecorder.ts | 46 +- 10 files changed, 471 insertions(+), 566 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index cc013c59..5c5bfb47 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -50,7 +50,11 @@ interface UpdateToastState { delayMs: number; isPreview?: boolean; progressPercent?: number; - primaryAction?: "download-update" | "install-update" | "retry-check"; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; } interface UpdateStatusSummary { @@ -286,9 +290,7 @@ interface Window { error?: string; }>; discardExportedTemp: (tempPath: string) => Promise<{ success: boolean; error?: string }>; - getVideoAudioFallbackPaths: ( - videoPath: string, - ) => Promise<{ + getVideoAudioFallbackPaths: (videoPath: string) => Promise<{ success: boolean; paths: string[]; startDelayMsByPath?: Record; @@ -486,7 +488,9 @@ interface Window { error?: string; }>; installDownloadedUpdate: () => Promise<{ success: boolean }>; - downloadAvailableUpdate: () => Promise<{ success: boolean; message?: string }>; + downloadAvailableUpdate: ( + installAfterDownload?: boolean, + ) => Promise<{ success: boolean; message?: string }>; deferDownloadedUpdate: (delayMs?: number) => Promise<{ success: boolean; message?: string; diff --git a/electron/main.ts b/electron/main.ts index c792ba3e..7607913b 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -496,13 +496,15 @@ function getUpdateNotificationTitle(payload: UpdateToastPayload) { function getUpdateNotificationBody(payload: UpdateToastPayload) { switch (payload.phase) { case "available": - return "Click to download the update."; + return "Click to install the update and restart Recordly."; case "downloading": - return "Recordly is downloading the update in the foreground."; + return "Recordly is downloading the update and will restart when it is ready."; case "ready": - return "Click to install the downloaded update."; + return "Click to install the downloaded update and restart."; case "error": - return "Click to retry checking for updates."; + return payload.primaryAction === "install-and-restart" + ? "Click to try the install again." + : "Click to retry checking for updates."; } } @@ -551,13 +553,21 @@ function sendUpdateToastToWindows(channel: "update-toast-state", payload: unknow focusOrCreateMainWindow(); switch (updatePayload.phase) { case "available": - void downloadAvailableUpdate(sendUpdateToastToWindows); + void downloadAvailableUpdate(sendUpdateToastToWindows, { + installAfterDownload: true, + }); break; case "ready": installDownloadedUpdateNow(sendUpdateToastToWindows); break; case "error": - void checkForAppUpdates(getUpdateDialogWindow, { manual: true }); + if (updatePayload.primaryAction === "install-and-restart") { + void downloadAvailableUpdate(sendUpdateToastToWindows, { + installAfterDownload: true, + }); + } else { + void checkForAppUpdates(getUpdateDialogWindow, { manual: true }); + } break; default: break; @@ -625,8 +635,10 @@ ipcMain.handle("install-downloaded-update", () => { return { success: true }; }); -ipcMain.handle("download-available-update", () => { - return downloadAvailableUpdate(sendUpdateToastToWindows); +ipcMain.handle("download-available-update", (_event, installAfterDownload?: boolean) => { + return downloadAvailableUpdate(sendUpdateToastToWindows, { + installAfterDownload: Boolean(installAfterDownload), + }); }); ipcMain.handle("defer-downloaded-update", (_event, delayMs?: number) => { diff --git a/electron/preload.ts b/electron/preload.ts index 937e00d0..e6537ec4 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -504,8 +504,8 @@ contextBridge.exposeInMainWorld("electronAPI", { installDownloadedUpdate: () => { return ipcRenderer.invoke("install-downloaded-update"); }, - downloadAvailableUpdate: () => { - return ipcRenderer.invoke("download-available-update"); + downloadAvailableUpdate: (installAfterDownload?: boolean) => { + return ipcRenderer.invoke("download-available-update", installAfterDownload); }, deferDownloadedUpdate: (delayMs?: number) => { return ipcRenderer.invoke("defer-downloaded-update", delayMs); @@ -537,7 +537,11 @@ contextBridge.exposeInMainWorld("electronAPI", { delayMs: number; isPreview?: boolean; progressPercent?: number; - primaryAction?: "download-update" | "install-update" | "retry-check"; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; } | null, ) => void, ) => { @@ -550,7 +554,11 @@ contextBridge.exposeInMainWorld("electronAPI", { delayMs: number; isPreview?: boolean; progressPercent?: number; - primaryAction?: "download-update" | "install-update" | "retry-check"; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; } | null, ) => callback(payload); ipcRenderer.on("update-toast-state", listener); diff --git a/electron/updater.ts b/electron/updater.ts index c81ba7bb..23eb65e5 100644 --- a/electron/updater.ts +++ b/electron/updater.ts @@ -17,6 +17,7 @@ const UPDATER_LOG_PATH = const DEV_UPDATE_PREVIEW_VERSION = "9.9.9"; const DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS = 300; const DEV_UPDATE_PREVIEW_PROGRESS_INCREMENT = 20; +const ONE_MEGABYTE = 1024 * 1024; export type UpdateToastPhase = "available" | "downloading" | "ready" | "error"; @@ -43,7 +44,18 @@ export interface UpdateToastPayload { delayMs: number; isPreview?: boolean; progressPercent?: number; - primaryAction?: "download-update" | "install-update" | "retry-check"; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; +} + +interface DownloadProgressSnapshot { + progressPercent?: number; + transferredBytes?: number; + totalBytes?: number; + bytesPerSecond?: number; } type UpdateToastSender = ( @@ -64,6 +76,7 @@ let downloadInProgress = false; let downloadToastDismissed = false; let skippedVersion: string | null = null; let updateCheckErrorHandled = false; +let installAfterDownloadRequested = false; let activeUpdateToastSender: UpdateToastSender | undefined; let updateStatusSummary: UpdateStatusSummary = { status: "idle", @@ -180,26 +193,54 @@ function createAvailableUpdateToastPayload(version: string): UpdateToastPayload return { version, phase: "available", - detail: "A new version is available. Download it now, or wait and we will remind you again in 3 hours.", + detail: "Install the latest version now, or remind yourself to come back to it later.", delayMs: UPDATE_REMINDER_DELAY_MS, - primaryAction: "download-update", + primaryAction: "install-and-restart", }; } function createDownloadingUpdateToastPayload( version: string, - progressPercent = 0, + progress: DownloadProgressSnapshot = {}, ): UpdateToastPayload { - const normalizedProgress = Math.max(0, Math.min(100, progressPercent)); + const normalizedProgress = Math.max( + 0, + Math.min(100, Math.round(progress.progressPercent ?? 0)), + ); + const transferredBytes = + typeof progress.transferredBytes === "number" && Number.isFinite(progress.transferredBytes) + ? Math.max(0, progress.transferredBytes) + : undefined; + const totalBytes = + typeof progress.totalBytes === "number" && Number.isFinite(progress.totalBytes) + ? Math.max(0, progress.totalBytes) + : undefined; + const remainingBytes = + totalBytes !== undefined && transferredBytes !== undefined + ? Math.max(totalBytes - transferredBytes, 0) + : undefined; + const bytesPerSecond = + typeof progress.bytesPerSecond === "number" && Number.isFinite(progress.bytesPerSecond) + ? Math.max(0, progress.bytesPerSecond) + : undefined; + const remainingMb = + remainingBytes !== undefined ? Math.max(0, remainingBytes / ONE_MEGABYTE) : null; return { version, phase: "downloading", detail: normalizedProgress >= 100 - ? "Finishing the update download. You can keep using Recordly while this completes." - : `Downloading the update in the foreground: ${normalizedProgress.toFixed(0)}% complete.`, + ? "Finishing the update download. Recordly will restart as soon as the installer is ready." + : remainingMb !== null + ? `${remainingMb.toFixed(1)} MB left before Recordly restarts.` + : "Downloading the update now. Recordly will restart when it finishes.", delayMs: UPDATE_REMINDER_DELAY_MS, progressPercent: normalizedProgress, + transferredBytes, + totalBytes, + remainingBytes, + bytesPerSecond, + primaryAction: "install-and-restart", }; } @@ -207,9 +248,9 @@ function createDownloadedUpdateToastPayload(version: string): UpdateToastPayload return { version, phase: "ready", - detail: "Install now to restart into the new version, or wait and we will remind you again in 3 hours.", + detail: "The update is ready. Install and restart now, or remind yourself later.", delayMs: UPDATE_REMINDER_DELAY_MS, - primaryAction: "install-update", + primaryAction: "install-and-restart", }; } @@ -217,9 +258,9 @@ function createUpdateErrorToastPayload(version: string, error: unknown): UpdateT return { version, phase: "error", - detail: `The update download failed. ${String(error)}`, + detail: `The update could not be downloaded. ${String(error)}`, delayMs: UPDATE_REMINDER_DELAY_MS, - primaryAction: "download-update", + primaryAction: "install-and-restart", }; } @@ -276,6 +317,7 @@ function resetDevPreviewState(sendToRenderer?: UpdateToastSender) { downloadInProgress = false; downloadToastDismissed = false; skippedVersion = null; + installAfterDownloadRequested = false; clearVisibleUpdateToast(sendToRenderer); } @@ -289,7 +331,12 @@ function simulateDevPreviewDownload(sendToRenderer?: UpdateToastSender) { let progressPercent = 0; emitUpdateToastState(sendToRenderer, { - ...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, progressPercent), + ...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, { + progressPercent, + transferredBytes: 0, + totalBytes: 20 * ONE_MEGABYTE, + bytesPerSecond: 5 * ONE_MEGABYTE, + }), isPreview: true, }); @@ -313,7 +360,12 @@ function simulateDevPreviewDownload(sendToRenderer?: UpdateToastSender) { } emitUpdateToastState(sendToRenderer, { - ...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, progressPercent), + ...createDownloadingUpdateToastPayload(DEV_UPDATE_PREVIEW_VERSION, { + progressPercent, + transferredBytes: (progressPercent / 100) * 20 * ONE_MEGABYTE, + totalBytes: 20 * ONE_MEGABYTE, + bytesPerSecond: 5 * ONE_MEGABYTE, + }), isPreview: true, }); }, DEV_UPDATE_PREVIEW_PROGRESS_STEP_MS); @@ -331,6 +383,7 @@ export function dismissUpdateToast( } if (downloadInProgress) { + installAfterDownloadRequested = false; downloadToastDismissed = true; clearVisibleUpdateToast(sendToRenderer); return { success: true }; @@ -360,13 +413,17 @@ export function installDownloadedUpdateNow(sendToRenderer?: UpdateToastSender) { clearDeferredReminderTimer(); downloadToastDismissed = false; + installAfterDownloadRequested = false; clearVisibleUpdateToast(sendToRenderer); setUpdateStatusSummary({ status: "ready", availableVersion: pendingDownloadedVersion }); writeUpdaterLog("Installing downloaded update."); autoUpdater.quitAndInstall(); } -export async function downloadAvailableUpdate(sendToRenderer?: UpdateToastSender) { +export async function downloadAvailableUpdate( + sendToRenderer?: UpdateToastSender, + options?: { installAfterDownload?: boolean }, +) { if (currentToastPayload?.isPreview) { return simulateDevPreviewDownload(sendToRenderer); } @@ -386,12 +443,20 @@ export async function downloadAvailableUpdate(sendToRenderer?: UpdateToastSender clearDeferredReminderTimer(); downloadInProgress = true; downloadToastDismissed = false; + installAfterDownloadRequested = + Boolean(options?.installAfterDownload) || installAfterDownloadRequested; setUpdateStatusSummary({ status: "downloading", availableVersion, detail: `Downloading Recordly ${availableVersion}`, }); - emitUpdateToastState(sendToRenderer, createDownloadingUpdateToastPayload(availableVersion, 0)); + emitUpdateToastState( + sendToRenderer, + createDownloadingUpdateToastPayload(availableVersion, { + progressPercent: 0, + transferredBytes: 0, + }), + ); writeUpdaterLog(`Starting update download for ${availableVersion}.`); try { @@ -425,6 +490,7 @@ export function deferUpdateReminder( } clearDeferredReminderTimer(); + installAfterDownloadRequested = false; clearVisibleUpdateToast(sendToRenderer); deferredReminderTimer = setTimeout(() => { const nextPayload = getReminderPayload(); @@ -462,6 +528,7 @@ export function skipAvailableUpdateVersion(sendToRenderer?: UpdateToastSender) { } downloadInProgress = false; downloadToastDismissed = false; + installAfterDownloadRequested = false; clearDeferredReminderTimer(); clearVisibleUpdateToast(sendToRenderer); @@ -475,6 +542,7 @@ export function previewUpdateToast(sendToRenderer: UpdateToastSender) { pendingDownloadedVersion = null; downloadInProgress = false; downloadToastDismissed = false; + installAfterDownloadRequested = false; return emitUpdateToastState(sendToRenderer, { version: DEV_UPDATE_PREVIEW_VERSION, phase: "available", @@ -493,24 +561,19 @@ async function showAvailableUpdateDialog( type: "info", title: "Update Available", message: `Recordly ${version} is available.`, - detail: "Download now, remind me in 3 hours, or skip this version.", - buttons: ["Download Update", "Remind Me in 3 Hours", "Skip This Version"], + detail: "Install and restart now, or remind me later.", + buttons: ["Install & Restart", "Later"], defaultId: 0, cancelId: 1, noLink: true, }); if (result.response === 0) { - await downloadAvailableUpdate(sendToRenderer); + await downloadAvailableUpdate(sendToRenderer, { installAfterDownload: true }); return; } - if (result.response === 1) { - deferUpdateReminder(getMainWindow, sendToRenderer, UPDATE_REMINDER_DELAY_MS); - return; - } - - skipAvailableUpdateVersion(sendToRenderer); + deferUpdateReminder(getMainWindow, sendToRenderer, UPDATE_REMINDER_DELAY_MS); } async function showDownloadedUpdateDialog( @@ -527,8 +590,8 @@ async function showDownloadedUpdateDialog( : `Recordly ${version} has been downloaded.`, detail: isPreview ? "Development preview of the native update prompt. No real update will be installed." - : "Install now, remind me in 3 hours, or skip this version.", - buttons: ["Install Update", "Remind Me in 3 Hours", "Skip This Version"], + : "Install and restart now, or remind me later.", + buttons: ["Install & Restart", "Later"], defaultId: 0, cancelId: 1, noLink: true, @@ -558,14 +621,7 @@ async function showDownloadedUpdateDialog( } deferUpdateReminder(getMainWindow, undefined, UPDATE_REMINDER_DELAY_MS); - return; } - - if (isPreview) { - return; - } - - skipAvailableUpdateVersion(); } export async function checkForAppUpdates( @@ -665,6 +721,7 @@ export function setupAutoUpdates( pendingDownloadedVersion = null; downloadInProgress = false; downloadToastDismissed = false; + installAfterDownloadRequested = false; setUpdateStatusSummary({ status: "available", availableVersion: info.version, @@ -694,6 +751,7 @@ export function setupAutoUpdates( pendingDownloadedVersion = null; downloadInProgress = false; downloadToastDismissed = false; + installAfterDownloadRequested = false; setUpdateStatusSummary({ status: "up-to-date", availableVersion: null, @@ -727,7 +785,12 @@ export function setupAutoUpdates( emitUpdateToastState( sendToRenderer, - createDownloadingUpdateToastPayload(availableVersion, progress.percent), + createDownloadingUpdateToastPayload(availableVersion, { + progressPercent: progress.percent, + transferredBytes: progress.transferred, + totalBytes: progress.total, + bytesPerSecond: progress.bytesPerSecond, + }), ); }); @@ -748,6 +811,7 @@ export function setupAutoUpdates( if (downloadInProgress && availableVersion) { downloadInProgress = false; downloadToastDismissed = false; + installAfterDownloadRequested = false; emitUpdateToastState( sendToRenderer, createUpdateErrorToastPayload(availableVersion, error), @@ -767,6 +831,7 @@ export function setupAutoUpdates( downloadInProgress = false; downloadToastDismissed = false; if (skippedVersion === info.version) { + installAfterDownloadRequested = false; return; } availableVersion = info.version; @@ -778,6 +843,16 @@ export function setupAutoUpdates( }); clearDeferredReminderTimer(); + if (installAfterDownloadRequested && !currentToastPayload?.isPreview) { + installAfterDownloadRequested = false; + clearVisibleUpdateToast(sendToRenderer); + writeUpdaterLog(`Auto-installing downloaded update: version=${info.version}`); + setImmediate(() => { + installDownloadedUpdateNow(sendToRenderer); + }); + return; + } + if ( emitUpdateToastState(sendToRenderer, createDownloadedUpdateToastPayload(info.version)) ) { diff --git a/electron/windows.ts b/electron/windows.ts index b1b1ae21..6f51f742 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -34,8 +34,8 @@ const HUD_SHADOW_BLEED_DIP = 36; const HUD_MIN_WINDOW_WIDTH = 560; const HUD_COMPACT_HEIGHT = 96; const HUD_MIN_EXPANDED_HEIGHT = 520 + HUD_SHADOW_BLEED_DIP; -const UPDATE_TOAST_WIDTH = 420; -const UPDATE_TOAST_HEIGHT = 212; +const UPDATE_TOAST_WIDTH = 456; +const UPDATE_TOAST_HEIGHT = 252; const UPDATE_TOAST_GAP_DIP = 18; let hudOverlayExpanded = false; diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index 230f24b3..59ab66c1 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -35,66 +35,6 @@ padding-right: 2px; } -.updateBadge { - display: inline-flex; - align-items: center; - gap: 7px; - height: 34px; - padding: 0 12px; - border-radius: 11px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.03); - font-size: 12px; - font-weight: 700; - letter-spacing: 0.01em; - transition: all 0.15s ease; - cursor: pointer; - flex-shrink: 0; -} - -.updateBadge:disabled { - opacity: 0.72; - cursor: default; -} - -.updateBadgeQuiet { - color: #a5b4c7; - border-color: rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.035); -} - -.updateBadgeQuiet:hover:not(:disabled) { - color: #d7dee8; - background: rgba(255, 255, 255, 0.06); -} - -.updateBadgeHot { - color: #f8fbff; - border-color: rgba(125, 211, 252, 0.24); - background: linear-gradient(180deg, rgba(125, 211, 252, 0.12), rgba(125, 211, 252, 0.04)); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); -} - -.updateBadgeHot:hover:not(:disabled) { - color: #ffffff; - border-color: rgba(125, 211, 252, 0.36); - background: linear-gradient(180deg, rgba(125, 211, 252, 0.17), rgba(125, 211, 252, 0.07)); - transform: translateY(-1px); -} - -.updateBadgeSpin { - animation: updateBadgeSpin 0.9s linear infinite; -} - -@keyframes updateBadgeSpin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - .barState { display: flex; align-items: center; @@ -135,11 +75,11 @@ } .ibActive { - color: #6360f5; + color: #3d8bff; } .ibActive:hover { - color: #7b78ff; + color: #62a4ff; } .ibRed { @@ -274,7 +214,44 @@ } .ddItemSelected { - color: #6360f5; + color: #3d8bff; +} + +.finalizingState { + display: inline-flex; + align-items: center; + gap: 11px; + min-width: 238px; + color: #eeeef2; +} + +.finalizingSpin { + color: #3d8bff; + animation: finalizingSpin 0.9s linear infinite; +} + +.finalizingCopy { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 12px; + font-weight: 700; + line-height: 1.15; +} + +.finalizingCopy small { + color: #8a8a96; + font-size: 10px; + font-weight: 600; +} + +@keyframes finalizingSpin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } .recBtn { diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 33fa5740..8f48fd79 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,7 +1,5 @@ import { AppWindow, - ArrowCircleUp as ArrowUpCircle, - CheckCircle as CheckCircle2, CaretUp as ChevronUp, Eye, EyeSlash as EyeOff, @@ -165,6 +163,7 @@ export function LaunchWindow() { const { recording, paused, + finalizing, countdownActive, toggleRecording, pauseRecording, @@ -208,24 +207,6 @@ export function LaunchWindow() { >(null); const [platform, setPlatform] = useState(null); const [appVersion, setAppVersion] = useState(null); - const [updateStatus, setUpdateStatus] = useState<{ - status: - | "idle" - | "checking" - | "up-to-date" - | "available" - | "downloading" - | "ready" - | "error"; - currentVersion: string; - availableVersion: string | null; - detail?: string; - }>({ - status: "idle", - currentVersion: "", - availableVersion: null, - }); - const [updateActionPending, setUpdateActionPending] = useState(false); const dropdownRef = useRef(null); const hudContentRef = useRef(null); const hudBarRef = useRef(null); @@ -714,31 +695,6 @@ export function LaunchWindow() { void preparePermissions({ startup: true }); }, [preparePermissions]); - useEffect(() => { - let mounted = true; - - const refreshUpdateStatus = async () => { - try { - const summary = await window.electronAPI.getUpdateStatusSummary(); - if (mounted) { - setUpdateStatus(summary); - } - } catch (error) { - console.error("Failed to load update status summary:", error); - } - }; - - void refreshUpdateStatus(); - const pollTimer = window.setInterval(() => { - void refreshUpdateStatus(); - }, 2500); - - return () => { - mounted = false; - window.clearInterval(pollTimer); - }; - }, []); - useEffect(() => { let cancelled = false; const loadVersion = async () => { @@ -1009,74 +965,6 @@ export function LaunchWindow() { toggleDropdown("webcam"); }; - const updateButtonLabel = - updateStatus.status === "up-to-date" - ? t("recording.update.updated") - : t("recording.update.update"); - const updateButtonTitle = (() => { - switch (updateStatus.status) { - case "up-to-date": - return t("recording.update.upToDateTitle", "Recordly {{version}} is up to date.", { - version: updateStatus.currentVersion, - }); - case "available": - case "ready": - return updateStatus.availableVersion - ? t("recording.update.availableTitle", "Recordly {{version}} is available.", { - version: updateStatus.availableVersion, - }) - : t("recording.update.availableGenericTitle"); - case "downloading": - return updateStatus.detail ?? t("recording.update.downloadingTitle"); - case "checking": - return t("recording.update.checkingTitle"); - case "error": - return updateStatus.detail ?? t("recording.update.errorTitle"); - default: - return t("recording.update.idleTitle"); - } - })(); - const updateButtonClassName = `${styles.updateBadge} ${updateStatus.status === "up-to-date" ? styles.updateBadgeQuiet : styles.updateBadgeHot} ${styles.electronNoDrag}`; - const updateButtonIcon = (() => { - switch (updateStatus.status) { - case "up-to-date": - return ; - case "checking": - case "downloading": - return ; - default: - return ; - } - })(); - - const handleUpdateButtonClick = async () => { - if (updateActionPending || updateStatus.status === "downloading") { - return; - } - - setUpdateActionPending(true); - try { - switch (updateStatus.status) { - case "available": - await window.electronAPI.downloadAvailableUpdate(); - break; - case "ready": - await window.electronAPI.installDownloadedUpdate(); - break; - default: - await window.electronAPI.checkForAppUpdates(); - break; - } - - const summary = await window.electronAPI.getUpdateStatusSummary(); - setUpdateStatus(summary); - } catch (error) { - console.error("Failed to handle update button action:", error); - } finally { - setUpdateActionPending(false); - } - }; - const recordingControls = ( <>
@@ -1238,6 +1126,18 @@ export function LaunchWindow() { ); + const finalizingControls = ( +
+ +
+ {t("recording.preparing", "Preparing recording")} + {t("recording.preparingSubtitle", "Opening the editor in a moment")} +
+
+ ); + + const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle"; + return (
- -
- {recording ? recordingControls : idleControls} + {finalizing + ? finalizingControls + : recording + ? recordingControls + : idleControls}
diff --git a/src/components/launch/UpdateToastWindow.tsx b/src/components/launch/UpdateToastWindow.tsx index da37bfa1..2f2f89d4 100644 --- a/src/components/launch/UpdateToastWindow.tsx +++ b/src/components/launch/UpdateToastWindow.tsx @@ -4,7 +4,7 @@ import { Spinner as LoaderCircle, Rocket, } from "@phosphor-icons/react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; type UpdateToastPayload = { version: string; @@ -13,61 +13,73 @@ type UpdateToastPayload = { delayMs: number; isPreview?: boolean; progressPercent?: number; - primaryAction?: "download-update" | "install-update" | "retry-check"; + transferredBytes?: number; + totalBytes?: number; + remainingBytes?: number; + bytesPerSecond?: number; + primaryAction?: "install-and-restart" | "retry-check"; }; -const THREE_DAYS_MS = 3 * 24 * 60 * 60 * 1000; +const DEFAULT_REMINDER_DELAY_MS = 3 * 60 * 60 * 1000; +const REMINDER_OPTIONS = [ + { label: "1 hour", value: 1 * 60 * 60 * 1000 }, + { label: "3 hours", value: 3 * 60 * 60 * 1000 }, + { label: "Tomorrow", value: 24 * 60 * 60 * 1000 }, + { label: "3 days", value: 3 * 24 * 60 * 60 * 1000 }, +]; -function formatDelayHours(delayMs: number) { - const hours = Math.max(1, Math.round(delayMs / (60 * 60 * 1000))); - return `${hours}h`; +function formatBytes(value: number | undefined) { + if (value === undefined || !Number.isFinite(value) || value <= 0) { + return null; + } + + const megabytes = value / (1024 * 1024); + if (megabytes >= 1024) { + return `${(megabytes / 1024).toFixed(1)} GB`; + } + + return `${megabytes.toFixed(megabytes >= 100 ? 0 : 1)} MB`; } function getToastTitle(payload: UpdateToastPayload) { if (payload.isPreview) { - return "Update Toast Preview"; + return "Update Prompt Preview"; } switch (payload.phase) { case "available": return `Recordly ${payload.version} is available`; case "downloading": - return `Downloading Recordly ${payload.version}`; + return `Installing Recordly ${payload.version}`; case "ready": return `Recordly ${payload.version} is ready`; case "error": - return `Recordly ${payload.version} needs attention`; + return payload.primaryAction === "retry-check" + ? "Could not check for updates" + : `Recordly ${payload.version} needs attention`; } } -function getPrimaryActionLabel(payload: UpdateToastPayload) { - switch (payload.primaryAction) { - case "download-update": - return "Download Update"; - case "install-update": - return "Install Update"; - case "retry-check": - return "Retry Check"; - default: - return null; +function getPrimaryButtonLabel(payload: UpdateToastPayload) { + return payload.primaryAction === "retry-check" ? "Try Again" : "Install & Restart"; +} + +function getPhaseIcon(payload: UpdateToastPayload) { + switch (payload.phase) { + case "available": + return ; + case "downloading": + return ; + case "ready": + return ; + case "error": + return ; } } export function UpdateToastWindow() { const [payload, setPayload] = useState(null); - const [dragOffsetX, setDragOffsetX] = useState(0); - const dragResetKey = payload - ? `${payload.phase}:${payload.version}:${payload.progressPercent ?? ""}:${payload.detail}:${payload.delayMs}:${payload.isPreview ? "1" : "0"}:${payload.primaryAction ?? ""}` - : "empty"; - const dragState = useRef<{ - pointerId: number | null; - startX: number; - active: boolean; - }>({ - pointerId: null, - startX: 0, - active: false, - }); + const [reminderDelayMs, setReminderDelayMs] = useState(DEFAULT_REMINDER_DELAY_MS); useEffect(() => { let mounted = true; @@ -81,11 +93,9 @@ export function UpdateToastWindow() { pollTimer = setInterval(() => { void window.electronAPI.getCurrentUpdateToastPayload().then((nextPayload) => { - if (!mounted || !nextPayload) { - return; + if (mounted) { + setPayload(nextPayload); } - - setPayload((currentPayload) => currentPayload ?? nextPayload); }); }, 750); @@ -103,214 +113,160 @@ export function UpdateToastWindow() { }, []); useEffect(() => { - if (!dragResetKey) { + if (!payload) { return; } - setDragOffsetX(0); - dragState.current = { - pointerId: null, - startX: 0, - active: false, - }; - }, [dragResetKey]); + setReminderDelayMs(payload.delayMs || DEFAULT_REMINDER_DELAY_MS); + }, [payload]); - const cardStyle = { - background: "#0d1117", - border: "1px solid rgba(125, 211, 252, 0.22)", - boxShadow: "0 24px 48px rgba(0, 0, 0, 0.45)", - borderRadius: 24, - padding: 16, - color: "#ffffff", - width: "100%", - maxWidth: 404, - display: "flex", - gap: 12, - alignItems: "flex-start", - fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', - } as const; + const normalizedProgress = Math.max( + 0, + Math.min(100, Math.round(payload?.progressPercent ?? 0)), + ); + const downloadedLabel = formatBytes(payload?.transferredBytes); + const totalLabel = formatBytes(payload?.totalBytes); + const remainingLabel = formatBytes(payload?.remainingBytes); + const speedLabel = formatBytes(payload?.bytesPerSecond); + const phaseStats: Array<{ label: string; value: string }> = []; + if (payload?.phase === "downloading") { + if (downloadedLabel && totalLabel) { + phaseStats.push({ label: "Downloaded", value: `${downloadedLabel} / ${totalLabel}` }); + } else if (downloadedLabel) { + phaseStats.push({ label: "Downloaded", value: downloadedLabel }); + } + if (remainingLabel) { + phaseStats.push({ label: "Left", value: remainingLabel }); + } + if (speedLabel) { + phaseStats.push({ label: "Speed", value: `${speedLabel}/s` }); + } + } + + const isMacOS = /mac/i.test(navigator.platform); const wrapperStyle = { display: "flex", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", - padding: 8, + padding: 10, boxSizing: "border-box", - background: "transparent", + background: isMacOS ? "transparent" : "#0b1220", } as const; - const secondaryTextStyle = { - color: "rgba(255, 255, 255, 0.74)", - fontSize: 14, - lineHeight: 1.45, - margin: "4px 0 0 0", - } as const; - const titleStyle = { - fontSize: 14, - fontWeight: 700, - lineHeight: 1.2, - margin: 0, + const cardStyle = { + width: "100%", + maxWidth: 440, + display: "flex", + gap: 14, + alignItems: "flex-start", + padding: "18px 18px 16px", + borderRadius: 24, + background: + "linear-gradient(180deg, rgba(12, 19, 34, 0.98) 0%, rgba(10, 17, 30, 0.98) 100%)", + border: "1px solid rgba(37, 99, 235, 0.24)", + boxShadow: "0 20px 48px rgba(2, 6, 23, 0.5), inset 0 1px 0 rgba(148, 163, 184, 0.08)", color: "#ffffff", + fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', } as const; const iconBoxStyle = { - width: 40, - height: 40, - minWidth: 40, + width: 42, + height: 42, + minWidth: 42, borderRadius: 16, - background: "rgba(125, 211, 252, 0.15)", - color: "#7dd3fc", display: "flex", alignItems: "center", justifyContent: "center", - marginTop: 2, + background: "rgba(37, 99, 235, 0.16)", + color: "#60a5fa", + boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.18)", } as const; - const rowStyle = { - display: "flex", - flexWrap: "wrap" as const, - gap: 8, - marginTop: 12, + const titleStyle = { + fontSize: 15, + fontWeight: 700, + lineHeight: 1.25, + margin: 0, + color: "#f8fafc", + } as const; + const secondaryTextStyle = { + color: "rgba(226, 232, 240, 0.78)", + fontSize: 13, + lineHeight: 1.5, + margin: "6px 0 0 0", } as const; const subtleButtonStyle = { - border: "1px solid rgba(255, 255, 255, 0.1)", - background: "rgba(255, 255, 255, 0.05)", - color: "rgba(255, 255, 255, 0.92)", + height: 38, borderRadius: 12, - padding: "8px 12px", - fontSize: 12, + padding: "0 14px", + border: "1px solid rgba(148, 163, 184, 0.16)", + background: "rgba(15, 23, 42, 0.72)", + color: "#e2e8f0", + fontSize: 13, fontWeight: 600, cursor: "pointer", + transition: "all 0.15s ease", } as const; const primaryButtonStyle = { ...subtleButtonStyle, - background: "#7dd3fc", - color: "#031a2c", border: "none", + background: "linear-gradient(180deg, #3b82f6 0%, #2563eb 100%)", + color: "#ffffff", + boxShadow: "0 12px 24px rgba(37, 99, 235, 0.26)", } as const; - const ghostButtonStyle = { - ...subtleButtonStyle, - background: "transparent", - color: "rgba(255, 255, 255, 0.72)", - border: "1px solid rgba(125, 211, 252, 0.16)", + const selectStyle = { + height: 38, + borderRadius: 12, + padding: "0 34px 0 12px", + border: "1px solid rgba(37, 99, 235, 0.22)", + background: + "linear-gradient(180deg, rgba(18, 29, 51, 0.96) 0%, rgba(12, 22, 42, 0.96) 100%)", + color: "#dbeafe", + fontSize: 13, + fontWeight: 600, + outline: "none", + boxShadow: "inset 0 0 0 1px rgba(37, 99, 235, 0.06)", + cursor: "pointer", } as const; - if (!payload) { - return ( -
-
-
- -
-
-

Checking for updates

-

- Waiting for updater state from the main process. -

-
-
-
- ); - } - - const normalizedProgress = Math.max(0, Math.min(100, Math.round(payload.progressPercent ?? 0))); - const primaryActionLabel = getPrimaryActionLabel(payload); - const swipeThreshold = 96; - const handleSwipeDismiss = async () => { - setDragOffsetX(0); - dragState.current = { - pointerId: null, - startX: 0, - active: false, - }; - await window.electronAPI.dismissUpdateToast(); - }; - const handlePrimaryAction = async () => { - switch (payload.primaryAction) { - case "download-update": - await window.electronAPI.downloadAvailableUpdate(); - return; - case "install-update": - await window.electronAPI.installDownloadedUpdate(); - return; - case "retry-check": - await window.electronAPI.checkForAppUpdates(); - return; - default: - return; + if (!payload || payload.phase === "downloading") { + return; } + + if (payload.primaryAction === "retry-check") { + await window.electronAPI.checkForAppUpdates(); + return; + } + + if (payload.phase === "ready") { + await window.electronAPI.installDownloadedUpdate(); + return; + } + + await window.electronAPI.downloadAvailableUpdate(true); }; + const handleLater = async () => { + if (!payload) { + return; + } + + if (payload.isPreview) { + await window.electronAPI.dismissUpdateToast(); + return; + } + + await window.electronAPI.deferDownloadedUpdate(reminderDelayMs); + }; + + if (!payload) { + return
; + } + return (
-
{ - const target = event.target as HTMLElement | null; - if (target?.closest("button")) { - return; - } - - dragState.current = { - pointerId: event.pointerId, - startX: event.clientX, - active: true, - }; - event.currentTarget.setPointerCapture(event.pointerId); - }} - onPointerMove={(event) => { - if ( - !dragState.current.active || - dragState.current.pointerId !== event.pointerId - ) { - return; - } - - setDragOffsetX(event.clientX - dragState.current.startX); - }} - onPointerUp={async (event) => { - if ( - !dragState.current.active || - dragState.current.pointerId !== event.pointerId - ) { - return; - } - - const nextOffset = event.clientX - dragState.current.startX; - dragState.current = { - pointerId: null, - startX: 0, - active: false, - }; - - if (Math.abs(nextOffset) >= swipeThreshold) { - await handleSwipeDismiss(); - return; - } - - setDragOffsetX(0); - }} - onPointerCancel={() => { - dragState.current = { - pointerId: null, - startX: 0, - active: false, - }; - setDragOffsetX(0); - }} - > -
- {payload.phase === "available" ? : null} - {payload.phase === "downloading" ? ( - - ) : null} - {payload.phase === "ready" ? : null} - {payload.phase === "error" ? : null} -
+
+
{getPhaseIcon(payload)}

{getToastTitle(payload)}

@@ -318,14 +274,14 @@ export function UpdateToastWindow() { Dev @@ -335,104 +291,102 @@ export function UpdateToastWindow() {

{payload.detail}

{payload.phase === "downloading" ? ( -
+
-

- {normalizedProgress}% downloaded -

+ + {normalizedProgress}% complete + + {phaseStats.map((stat) => ( + + {stat.label}: {stat.value} + + ))} +
) : null} -
- {primaryActionLabel ? ( - - ) : null} - - {payload.phase === "downloading" ? ( - - ) : null} - +
{payload.phase !== "downloading" ? ( - - ) : null} - - {payload.phase !== "downloading" ? ( - - ) : null} - - {!payload.isPreview && payload.phase !== "downloading" ? ( - + <> + + + + ) : null}
diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index 56750da3..170d48f3 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -350,10 +350,10 @@ export interface Padding { } export const DEFAULT_PADDING: Padding = { - top: 50, - bottom: 50, - left: 50, - right: 50, + top: 20, + bottom: 20, + left: 20, + right: 20, linked: true, }; diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index cb41930e..3709fa84 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -59,6 +59,7 @@ type DesktopCaptureMediaDevices = { type UseScreenRecorderReturn = { recording: boolean; paused: boolean; + finalizing: boolean; countdownActive: boolean; toggleRecording: () => void; pauseRecording: () => void; @@ -114,6 +115,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const [recording, setRecording] = useState(false); const [paused, setPaused] = useState(false); const [starting, setStarting] = useState(false); + const [finalizing, setFinalizing] = useState(false); const [countdownActive, setCountdownActive] = useState(false); const [isMacOS, setIsMacOS] = useState(false); const [microphoneEnabled, setMicrophoneEnabled] = useState(false); @@ -149,36 +151,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const accumulatedPausedDurationMs = useRef(0); const pauseStartedAtMs = useRef(null); const pauseSegmentsRef = useRef([]); - const recordingFinalizationToastId = useRef(null); const micFallbackRecorder = useRef(null); const micFallbackChunks = useRef([]); const micFallbackStartDelayMs = useRef(null); - const showRecordingFinalizationToast = useCallback((message = "Preparing recording...") => { - recordingFinalizationToastId.current = toast.loading(message, { - id: recordingFinalizationToastId.current ?? undefined, - duration: Number.POSITIVE_INFINITY, - }); + const notifyRecordingFinalizationFailure = useCallback(async (message: string) => { + setFinalizing(false); + toast.error(message, { duration: 10000 }); }, []); - const clearRecordingFinalizationToast = useCallback(() => { - const toastId = recordingFinalizationToastId.current; - if (toastId === null) { - return; - } - - toast.dismiss(toastId); - recordingFinalizationToastId.current = null; - }, []); - - const notifyRecordingFinalizationFailure = useCallback( - async (message: string) => { - clearRecordingFinalizationToast(); - toast.error(message, { duration: 10000 }); - }, - [clearRecordingFinalizationToast], - ); - const logNativeCaptureDiagnostics = useCallback(async (context: string) => { if (typeof window.electronAPI?.getLastNativeCaptureDiagnostics !== "function") { return; @@ -436,10 +417,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } - clearRecordingFinalizationToast(); + setFinalizing(false); await window.electronAPI.switchToEditor(); }, - [clearRecordingFinalizationToast], + [], ); const stopMicFallbackRecorder = useCallback((): Promise => { @@ -690,9 +671,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (nativeScreenRecording.current) { nativeScreenRecording.current = false; setRecording(false); + setFinalizing(true); void (async () => { - showRecordingFinalizationToast(); const fallbackStartDelayMs = micFallbackStartDelayMs.current; const micFallbackBlobPromise = stopMicFallbackRecorder(); const webcamPath = await stopWebcamRecorder(); @@ -787,6 +768,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { cleanupCapturedMedia(); recorder.stop(); setRecording(false); + setFinalizing(true); window.electronAPI?.setRecordingState(false); } }); @@ -1341,9 +1323,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }; recorder.onstop = async () => { cleanupCapturedMedia(); - if (chunks.current.length === 0) return; - - showRecordingFinalizationToast(); + if (chunks.current.length === 0) { + setFinalizing(false); + return; + } const duration = getRecordingDurationMs(Date.now()); const recordedChunks = chunks.current; @@ -1530,7 +1513,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }, [cleanupCapturedMedia, markRecordingResumed, recording]); const toggleRecording = async () => { - if (starting || countdownActive) { + if (starting || countdownActive || finalizing) { return; } @@ -1558,6 +1541,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return { recording, paused, + finalizing, countdownActive, toggleRecording, pauseRecording, From 51a9567c84dfb6feb98c9daf2695504753bab928 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:47:02 +1000 Subject: [PATCH 4/5] fix(build): use maintained Electron rebuild package --- package-lock.json | 790 +--------------------------------------------- package.json | 4 +- 2 files changed, 3 insertions(+), 791 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5500762b..deb22a46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@biomejs/biome": "2.3.13", + "@electron/rebuild": "^4.0.3", "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", "@pixi/filter-motion-blur": "^5.1.1", @@ -46,7 +47,6 @@ "electron": "^39.2.7", "electron-builder": "^26.7.0", "electron-icon-builder": "^2.0.1", - "electron-rebuild": "^3.2.9", "emoji-picker-react": "^4.16.1", "fast-check": "^4.5.2", "fix-webm-duration": "^1.0.6", @@ -1921,13 +1921,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2814,35 +2807,6 @@ "node": ">= 14" } }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/@phosphor-icons/react": { "version": "2.1.10", "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz", @@ -4333,16 +4297,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4806,13 +4760,6 @@ "dev": true, "license": "MIT" }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC" - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -4851,19 +4798,6 @@ "node": ">= 6.0.0" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -5283,28 +5217,6 @@ "node": ">=18" } }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "dev": true, - "license": "ISC" - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -5866,100 +5778,6 @@ "node": ">= 10.0.0" } }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -6155,16 +5973,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -6333,16 +6141,6 @@ "dev": true, "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6432,13 +6230,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6670,13 +6461,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -7118,74 +6902,6 @@ "node": ">= 10.0.0" } }, - "node_modules/electron-rebuild": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-3.2.9.tgz", - "integrity": "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw==", - "deprecated": "Please use @electron/rebuild moving forward. There is no API change, just a package name change", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "chalk": "^4.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "fs-extra": "^10.0.0", - "got": "^11.7.0", - "lzma-native": "^8.0.5", - "node-abi": "^3.0.0", - "node-api-version": "^0.1.4", - "node-gyp": "^9.0.0", - "ora": "^5.1.0", - "semver": "^7.3.5", - "tar": "^6.0.5", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/src/cli.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/electron-rebuild/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-rebuild/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-rebuild/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.234", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", @@ -7958,39 +7674,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8023,34 +7706,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8432,13 +8087,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC" - }, "node_modules/hasha": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", @@ -8506,21 +8154,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/http-response-object": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", @@ -8588,16 +8221,6 @@ "node": ">=10.17.0" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/icon-gen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/icon-gen/-/icon-gen-2.1.0.tgz", @@ -8738,13 +8361,6 @@ "node": ">=8" } }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true, - "license": "ISC" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8869,13 +8485,6 @@ "node": ">=8" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9401,32 +9010,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lzma-native": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", - "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^3.1.0", - "node-gyp-build": "^4.2.1", - "readable-stream": "^3.6.0" - }, - "bin": { - "lzmajs": "bin/lzmajs" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/lzma-native/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -9437,64 +9020,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dev": true, - "license": "ISC", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -9675,77 +9200,6 @@ "node": ">=8" } }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -9845,40 +9299,6 @@ "dev": true, "license": "ISC" }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -9993,29 +9413,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.78.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", - "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", @@ -10024,42 +9421,6 @@ "license": "MIT", "optional": true }, - "node_modules/node-api-version": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.1.4.tgz", - "integrity": "sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-gyp": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", - "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^10.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -10078,22 +9439,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -10169,23 +9514,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -11059,13 +10387,6 @@ "node": ">=0.4.0" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true, - "license": "ISC" - }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -12196,21 +11517,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -12323,39 +11629,6 @@ "node": ">=0.10.0" } }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/ssri/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ssri/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -12798,31 +12071,6 @@ "tailwindcss": ">=3.0.0 || insiders" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -13249,32 +12497,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -14243,16 +13465,6 @@ "node": ">=8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 204d494a..6fac6f9d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "lint:fix": "biome check --write .", "format": "biome format --write .", "preview": "vite preview", - "rebuild:native": "node ./node_modules/electron-rebuild/lib/src/cli.js --force --only uiohook-napi", + "rebuild:native": "node ./node_modules/@electron/rebuild/lib/cli.js --force --only uiohook-napi", "build:native-helpers": "node scripts/build-native-helpers.mjs", "build:whisper-runtime": "node scripts/build-whisper-runtime.mjs", "build:platform-native-helpers": "npm run build:native-helpers && npm run build:windows-capture && npm run build:cursor-monitor && npm run build:whisper-runtime", @@ -74,7 +74,7 @@ "electron": "^39.2.7", "electron-builder": "^26.7.0", "electron-icon-builder": "^2.0.1", - "electron-rebuild": "^3.2.9", + "@electron/rebuild": "^4.0.3", "emoji-picker-react": "^4.16.1", "fast-check": "^4.5.2", "fix-webm-duration": "^1.0.6", From f483b0a4a0cee49046f93ce7b8331a02d631de93 Mon Sep 17 00:00:00 2001 From: Wiii <156531153+meiiie@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:02:46 +0700 Subject: [PATCH 5/5] fix(editor): align preview timing with presented video frames --- src/components/video-editor/VideoEditor.tsx | 42 +++-- src/components/video-editor/VideoPlayback.tsx | 32 ++-- .../videoPlayback/videoEventHandlers.test.ts | 154 ++++++++++++++++++ .../videoPlayback/videoEventHandlers.ts | 79 +++++++-- .../videoPlayback/webcamSync.test.ts | 44 +++++ .../video-editor/videoPlayback/webcamSync.ts | 15 ++ 6 files changed, 326 insertions(+), 40 deletions(-) create mode 100644 src/components/video-editor/videoPlayback/videoEventHandlers.test.ts create mode 100644 src/components/video-editor/videoPlayback/webcamSync.test.ts create mode 100644 src/components/video-editor/videoPlayback/webcamSync.ts diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 794dba45..bc2c01ef 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -149,6 +149,7 @@ import { DEFAULT_FIGURE_DATA, DEFAULT_PLAYBACK_SPEED, DEFAULT_WEBCAM_OVERLAY, + DEFAULT_WEBCAM_TIME_OFFSET_MS, DEFAULT_ZOOM_DEPTH, DEFAULT_ZOOM_IN_DURATION_MS, DEFAULT_ZOOM_IN_EASING, @@ -1569,6 +1570,7 @@ export default function VideoEditor() { await window.electronAPI.setCurrentRecordingSession?.({ videoPath: sourcePath, webcamPath: normalizedEditor.webcam.sourcePath, + timeOffsetMs: normalizedEditor.webcam.timeOffsetMs, }); } else { await window.electronAPI.setCurrentVideoPath(sourcePath); @@ -1695,7 +1697,7 @@ export default function VideoEditor() { }, [currentPersistedEditorState, currentSourcePath, lastSavedSnapshot?.projectId]); const syncRecordingSessionWebcam = useCallback( - async (webcamPath: string | null) => { + async (webcamPath: string | null, timeOffsetMs?: number) => { if (!currentSourcePath || !window.electronAPI.setCurrentRecordingSession) { return; } @@ -1703,9 +1705,15 @@ export default function VideoEditor() { await window.electronAPI.setCurrentRecordingSession({ videoPath: currentSourcePath, webcamPath, + timeOffsetMs: + webcamPath && Number.isFinite(timeOffsetMs) + ? (timeOffsetMs ?? DEFAULT_WEBCAM_TIME_OFFSET_MS) + : webcamPath + ? webcam.timeOffsetMs + : DEFAULT_WEBCAM_TIME_OFFSET_MS, }); }, - [currentSourcePath], + [currentSourcePath, webcam.timeOffsetMs], ); const syncActiveVideoSource = useCallback( @@ -1714,13 +1722,14 @@ export default function VideoEditor() { await window.electronAPI.setCurrentRecordingSession?.({ videoPath: sourcePath, webcamPath, + timeOffsetMs: webcam.timeOffsetMs, }); return; } await window.electronAPI.setCurrentVideoPath(sourcePath); }, - [], + [webcam.timeOffsetMs], ); const handleUploadWebcam = useCallback(async () => { @@ -1733,9 +1742,10 @@ export default function VideoEditor() { ...prev, enabled: true, sourcePath: result.path ?? null, + timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS, })); - await syncRecordingSessionWebcam(result.path); + await syncRecordingSessionWebcam(result.path, DEFAULT_WEBCAM_TIME_OFFSET_MS); toast.success(t("settings.effects.webcamFootageAdded")); }, [syncRecordingSessionWebcam, t]); @@ -1744,6 +1754,7 @@ export default function VideoEditor() { ...prev, enabled: false, sourcePath: null, + timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS, })); await syncRecordingSessionWebcam(null); @@ -1838,6 +1849,7 @@ export default function VideoEditor() { ...prev, enabled: !!smokeWebcamSourcePath, sourcePath: smokeWebcamSourcePath, + timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS, shadow: smokeExportConfig.webcamShadow === undefined ? prev.shadow @@ -1896,6 +1908,8 @@ export default function VideoEditor() { ...prev, enabled: Boolean(sessionResult.session?.webcamPath), sourcePath: sessionResult.session?.webcamPath ?? null, + timeOffsetMs: + sessionResult.session?.timeOffsetMs ?? DEFAULT_WEBCAM_TIME_OFFSET_MS, })); return; } @@ -1913,6 +1927,7 @@ export default function VideoEditor() { ...prev, enabled: false, sourcePath: null, + timeOffsetMs: DEFAULT_WEBCAM_TIME_OFFSET_MS, })); } else { setError("No video to load. Please record or select a video."); @@ -3594,14 +3609,14 @@ export default function VideoEditor() { } } - for (const audioPath of previewSourceAudioFallbackPaths) { - let audio = existing.get(audioPath); - if (!audio) { - audio = new Audio(); - audio.preload = "auto"; - existing.set(audioPath, audio); - } - audio.dataset.sourceAudioPath = audioPath; + for (const audioPath of previewSourceAudioFallbackPaths) { + let audio = existing.get(audioPath); + if (!audio) { + audio = new Audio(); + audio.preload = "auto"; + existing.set(audioPath, audio); + } + audio.dataset.sourceAudioPath = audioPath; if (sourceAudioElementResourcesRef.current.get(audioPath) !== audioPath) { audio.pause(); @@ -5476,7 +5491,8 @@ export default function VideoEditor() { audioRegions.length > 0 ? Math.max( ...audioRegions.map( - (region) => region.trackIndex ?? 0, + (region) => + region.trackIndex ?? 0, ), ) + 1 : 0; diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 3cee42fb..edab1ff2 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -101,6 +101,7 @@ import { DEFAULT_CURSOR_SIZE, DEFAULT_CURSOR_SMOOTHING, DEFAULT_CURSOR_SWAY, + DEFAULT_PADDING, DEFAULT_WEBCAM_CORNER_RADIUS, DEFAULT_WEBCAM_REACT_TO_ZOOM, DEFAULT_WEBCAM_SHADOW, @@ -110,7 +111,6 @@ import { DEFAULT_ZOOM_IN_OVERLAP_MS, DEFAULT_ZOOM_OUT_DURATION_MS, DEFAULT_ZOOM_OUT_EASING, - DEFAULT_PADDING, getDefaultCaptionFontFamily, } from "./types"; import { @@ -124,6 +124,7 @@ import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focu import { layoutVideoContent as layoutVideoContentUtil } from "./videoPlayback/layoutUtils"; import { updateOverlayIndicator } from "./videoPlayback/overlayUtils"; import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers"; +import { getWebcamPreviewTargetTimeSeconds } from "./videoPlayback/webcamSync"; import { findDominantRegion } from "./videoPlayback/zoomRegionUtils"; import { applyZoomTransform, @@ -1232,10 +1233,11 @@ const VideoPlayback = forwardRef( return; } - const targetTime = clampMediaTimeToDuration( + const targetTime = getWebcamPreviewTargetTimeSeconds({ currentTime, - Number.isFinite(webcamVideo.duration) ? webcamVideo.duration : null, - ); + webcamDuration: Number.isFinite(webcamVideo.duration) ? webcamVideo.duration : null, + timeOffsetMs: webcam.timeOffsetMs, + }); const activeSpeedRegion = speedRegionsRef.current.find( (region) => targetTime * 1000 >= region.startMs && targetTime * 1000 < region.endMs, @@ -1470,7 +1472,7 @@ const VideoPlayback = forwardRef( layoutVideoContent(); video.pause(); - const { handlePlay, handlePause, handleSeeked, handleSeeking } = + const { handlePlay, handlePause, handleSeeked, handleSeeking, dispose } = createVideoEventHandlers({ video, isSeekingRef, @@ -1496,10 +1498,7 @@ const VideoPlayback = forwardRef( video.removeEventListener("ended", handlePause); video.removeEventListener("seeked", handleSeeked); video.removeEventListener("seeking", handleSeeking); - - if (timeUpdateAnimationRef.current) { - cancelAnimationFrame(timeUpdateAnimationRef.current); - } + dispose(); if (videoSprite) { videoContainer.removeChild(videoSprite); @@ -1749,9 +1748,13 @@ const VideoPlayback = forwardRef( // through an intermediate RenderTexture at renderer resolution, which // downsamples the native video and degrades preview quality. // Hysteresis prevents flickering when motionIntensity oscillates near threshold. - const filtersActive = Array.isArray(videoContainer.filters) && videoContainer.filters.length > 0; - const cameraIsMoving = filtersActive ? motionIntensity > 0.002 : motionIntensity > 0.008; - const needsFilters = zoomMotionBlurRef.current > 0 && isPlayingRef.current && cameraIsMoving; + const filtersActive = + Array.isArray(videoContainer.filters) && videoContainer.filters.length > 0; + const cameraIsMoving = filtersActive + ? motionIntensity > 0.002 + : motionIntensity > 0.008; + const needsFilters = + zoomMotionBlurRef.current > 0 && isPlayingRef.current && cameraIsMoving; if (needsFilters && !filtersActive && motionBlurFilterRef.current) { videoContainer.filters = [motionBlurFilterRef.current]; } else if (!needsFilters && filtersActive) { @@ -2451,12 +2454,15 @@ const VideoPlayback = forwardRef( })()}
)} + {/* Keep the source video off-screen instead of display:none so the + browser continues producing presented frames for Pixi and preview sync. */}