From 5d3da14671388af955935489bdb38668cf39d422 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 25 May 2026 02:50:09 +0700 Subject: [PATCH 1/2] fix: stabilize Windows recording preview --- electron/ipc/recording/windows.test.ts | 59 +++++++++++++++ electron/ipc/recording/windows.ts | 67 ++++++++++++------ .../bin/win32-x64/helpers-manifest.json | 6 +- electron/native/bin/win32-x64/wgc-capture.exe | Bin 110080 -> 110080 bytes .../native/wgc-capture/src/mf_encoder.cpp | 25 ++++++- src/components/video-editor/VideoEditor.tsx | 31 ++++++++ .../audio/useSourceAudioFallback.ts | 23 ++++-- 7 files changed, 180 insertions(+), 31 deletions(-) create mode 100644 electron/ipc/recording/windows.test.ts diff --git a/electron/ipc/recording/windows.test.ts b/electron/ipc/recording/windows.test.ts new file mode 100644 index 000000000..025f5a166 --- /dev/null +++ b/electron/ipc/recording/windows.test.ts @@ -0,0 +1,59 @@ +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { setWindowsCaptureOutputBuffer, setWindowsCaptureTargetPath } from "../state"; +import { waitForWindowsCaptureStop } from "./windows"; + +vi.mock("electron", () => ({ + app: { + getPath: () => "C:\\RecordlyTest", + }, + BrowserWindow: { + getAllWindows: () => [], + }, +})); + +class FakeCaptureProcess extends EventEmitter { + stdout = new PassThrough(); + stderr = new PassThrough(); + stdin = new PassThrough(); + killed = false; + + kill = vi.fn(() => { + this.killed = true; + return true; + }); +} + +describe("waitForWindowsCaptureStop", () => { + beforeEach(() => { + setWindowsCaptureOutputBuffer(""); + setWindowsCaptureTargetPath(null); + }); + + it("resolves the helper output path when the process closes cleanly", async () => { + const proc = new FakeCaptureProcess(); + setWindowsCaptureOutputBuffer("Recording stopped. Output path: C:\\Recordly\\capture.mp4"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("close", 0); + + await expect(stopped).resolves.toBe("C:\\Recordly\\capture.mp4"); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("kills the helper and rejects when stop never completes", async () => { + const proc = new FakeCaptureProcess(); + + await expect( + waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 5, + ), + ).rejects.toThrow("Timed out waiting for native Windows capture to stop"); + expect(proc.kill).toHaveBeenCalledTimes(1); + }); +}); diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index f3e43db59..262d26daa 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -16,8 +16,10 @@ import { import { AudioSyncAdjustment, } from "../types"; -import { emitRecordingInterrupted } from "./events"; import { moveFileWithOverwrite } from "../utils"; +import { emitRecordingInterrupted } from "./events"; + +const WINDOWS_CAPTURE_STOP_TIMEOUT_MS = 45_000; export type NativeWindowsVideoPaddingResult = { padded: boolean; @@ -107,33 +109,58 @@ export function waitForWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) }); } -export function waitForWindowsCaptureStop(proc: ChildProcessWithoutNullStreams) { +export function waitForWindowsCaptureStop( + proc: ChildProcessWithoutNullStreams, + timeoutMs = WINDOWS_CAPTURE_STOP_TIMEOUT_MS, +) { return new Promise((resolve, reject) => { - const onClose = (code: number | null) => { + let settled = false; + const finish = (callback: () => void) => { + if (settled) return; + settled = true; cleanup(); - const match = windowsCaptureOutputBuffer.match(/Recording stopped\. Output path: (.+)/); - if (match?.[1]) { - resolve(match[1].trim()); - return; - } - if (code === 0 && windowsCaptureTargetPath) { - resolve(windowsCaptureTargetPath); - return; - } - reject( - new Error( - windowsCaptureOutputBuffer.trim() || - `Native Windows capture exited with code ${code ?? "unknown"}`, - ), - ); + callback(); + }; + + const timer = setTimeout(() => { + finish(() => { + try { + if (!proc.killed) proc.kill(); + } catch { + // The process may already be gone; the caller only needs the timeout error. + } + reject(new Error("Timed out waiting for native Windows capture to stop")); + }); + }, timeoutMs); + + const onClose = (code: number | null) => { + finish(() => { + const match = windowsCaptureOutputBuffer.match(/Recording stopped\. Output path: (.+)/); + if (match?.[1]) { + resolve(match[1].trim()); + return; + } + if (code === 0 && windowsCaptureTargetPath) { + resolve(windowsCaptureTargetPath); + return; + } + reject( + new Error( + windowsCaptureOutputBuffer.trim() || + `Native Windows capture exited with code ${code ?? "unknown"}`, + ), + ); + }); }; const onError = (error: Error) => { - cleanup(); - reject(error); + finish(() => { + reject(error); + }); }; const cleanup = () => { + clearTimeout(timer); proc.off("close", onClose); proc.off("error", onError); }; diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index cf1ec1b69..2df0f4e5b 100644 --- a/electron/native/bin/win32-x64/helpers-manifest.json +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -5,10 +5,10 @@ "helpers": { "wgc-capture": { "binaryName": "wgc-capture.exe", - "binarySha256": "47a67765a0b7fdd9936e41a2f7e62f6408bd426774f7854e6378194ebe3ae827", + "binarySha256": "298b41f371c3881046061048b466e12ed70dd93fa761bade2fd57d1ccddf3cb9", "sourceDir": "electron/native/wgc-capture", - "sourceFingerprint": "696c9f56da75105941063b7940eb91c231e781e9a3a4ba885e47bcfdf798f13f", - "updatedAt": "2026-05-22T10:43:18.337Z" + "sourceFingerprint": "6ee457080c27dc939ff4b61965f86b6d73995e40200440a1dc44865f9708d39f", + "updatedAt": "2026-05-24T19:49:15.077Z" }, "cursor-monitor": { "binaryName": "cursor-monitor.exe", diff --git a/electron/native/bin/win32-x64/wgc-capture.exe b/electron/native/bin/win32-x64/wgc-capture.exe index 032169fa0086bbb41cc229425cd3eb180417f19c..8180edd00bf651f29b790dab9cdef9da52c5ddef 100644 GIT binary patch delta 24436 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 delta 24049 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 WEhXDaib{%0N=ptM{Imp*zW)!kjIMbA diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/wgc-capture/src/mf_encoder.cpp index 4dfbb2f81..4875d0956 100644 --- a/electron/native/wgc-capture/src/mf_encoder.cpp +++ b/electron/native/wgc-capture/src/mf_encoder.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,25 @@ static int clampByte(int v) { return v < 0 ? 0 : (v > 255 ? 255 : v); } +static UINT32 calculateScreenRecordingBitrate(int width, int height, int fps) { + constexpr uint64_t kFourKPixels = 3840ULL * 2160ULL; + constexpr uint64_t kQhdPixels = 2560ULL * 1440ULL; + constexpr UINT32 kBitrate4K = 45000000; + constexpr UINT32 kBitrateQhd = 28000000; + constexpr UINT32 kBitrateBase = 18000000; + constexpr double kHighFrameRateBoost = 1.35; + + const uint64_t pixels = + static_cast((std::max)(width, 1)) * + static_cast((std::max)(height, 1)); + const UINT32 baseBitrate = + pixels >= kFourKPixels ? kBitrate4K : + pixels >= kQhdPixels ? kBitrateQhd : + kBitrateBase; + const double boost = fps >= 60 ? kHighFrameRateBoost : 1.0; + return static_cast(static_cast(baseBitrate) * boost + 0.5); +} + MFEncoder::MFEncoder() {} MFEncoder::~MFEncoder() { @@ -56,11 +76,14 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264); - outputType->SetUINT32(MF_MT_AVG_BITRATE, 20000000); + const UINT32 videoBitrate = calculateScreenRecordingBitrate(width_, height_, fps_); + outputType->SetUINT32(MF_MT_AVG_BITRATE, videoBitrate); MFSetAttributeSize(outputType.Get(), MF_MT_FRAME_SIZE, width_, height_); MFSetAttributeRatio(outputType.Get(), MF_MT_FRAME_RATE, fps_, 1); MFSetAttributeRatio(outputType.Get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); outputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + std::cerr << "Encoder bitrate: " << videoBitrate << " bps for " + << width_ << "x" << height_ << "@" << fps_ << "fps" << std::endl; // Input media type (NV12) ComPtr inputType; diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 9e80a8549..975f5aeaa 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -2064,6 +2064,32 @@ export default function VideoEditor() { [currentProjectPath, webcam.timeOffsetMs], ); + const resetSourceScopedEditorState = useCallback(() => { + setZoomRegions([]); + setTrimRegions([]); + setClipRegions([]); + clipInitializedRef.current = false; + autoFullTrackClipIdRef.current = null; + autoFullTrackClipEndMsRef.current = null; + setSpeedRegions([]); + setAnnotationRegions([]); + setAudioRegions([]); + setSourceAudioTrackSettingsByClip({}); + setDefaultSourceAudioTrackSettings({}); + setHasClipSourceAudio(false); + setAutoCaptions([]); + setAutoCaptionSettings((prev) => ({ ...prev, enabled: false })); + setSelectedZoomId(null); + setSelectedClipId(null); + setSelectedAnnotationId(null); + setSelectedAudioId(null); + nextZoomIdRef.current = 1; + nextClipIdRef.current = 1; + nextAudioIdRef.current = 1; + nextAnnotationIdRef.current = 1; + nextAnnotationZIndexRef.current = 1; + }, []); + const handleUploadWebcam = useCallback(async () => { const result = await window.electronAPI.openVideoFilePicker(); if (!result.success || !result.path) { @@ -2152,6 +2178,7 @@ export default function VideoEditor() { setVideoPath(sourceVideoUrl); setCurrentProjectPath(null); setLastSavedSnapshot(null); + resetSourceScopedEditorState(); pendingFreshRecordingAutoZoomPathRef.current = autoApplyFreshRecordingAutoZooms ? sourceVideoUrl : null; @@ -2180,6 +2207,7 @@ export default function VideoEditor() { setVideoPath(sourceVideoUrl); setCurrentProjectPath(null); setLastSavedSnapshot(null); + resetSourceScopedEditorState(); pendingFreshRecordingAutoZoomPathRef.current = null; setWebcam((prev) => ({ ...prev, @@ -2237,6 +2265,7 @@ export default function VideoEditor() { setVideoPath(sourceVideoUrl); setCurrentProjectPath(null); setLastSavedSnapshot(null); + resetSourceScopedEditorState(); pendingFreshRecordingAutoZoomPathRef.current = autoApplyFreshRecordingAutoZooms ? sourceVideoUrl : null; @@ -2259,6 +2288,7 @@ export default function VideoEditor() { setVideoPath(sourceVideoUrl); setCurrentProjectPath(null); setLastSavedSnapshot(null); + resetSourceScopedEditorState(); pendingFreshRecordingAutoZoomPathRef.current = null; applySessionPresentation(null); setWebcam((prev) => ({ @@ -2285,6 +2315,7 @@ export default function VideoEditor() { devOpenRecordingConfig.inputPath, devOpenRecordingConfig.webcamInputPath, initialEditorPreferences, + resetSourceScopedEditorState, smokeExportConfig.enabled, smokeExportConfig.inputPath, smokeExportConfig.projectPath, diff --git a/src/components/video-editor/audio/useSourceAudioFallback.ts b/src/components/video-editor/audio/useSourceAudioFallback.ts index 2cfbbf752..bdaf05408 100644 --- a/src/components/video-editor/audio/useSourceAudioFallback.ts +++ b/src/components/video-editor/audio/useSourceAudioFallback.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { SOURCE_AUDIO_FALLBACK_TOAST_ID } from "@/components/video-editor/audio/audioTypes"; @@ -16,13 +16,18 @@ export function useSourceAudioFallback({ const [sourceAudioFallbackPaths, setSourceAudioFallbackPaths] = useState([]); const [sourceAudioFallbackStartDelayMsByPath, setSourceAudioFallbackStartDelayMsByPath] = useState>({}); + const previousSourcePathRef = useRef(null); useEffect(() => { let cancelled = false; // Refetch when late recording sidecars are finalized after the editor opens. void refreshKey; - setSourceAudioFallbackPaths([]); - setSourceAudioFallbackStartDelayMsByPath({}); + const sourceChanged = previousSourcePathRef.current !== currentSourcePath; + previousSourcePathRef.current = currentSourcePath; + if (sourceChanged) { + setSourceAudioFallbackPaths([]); + setSourceAudioFallbackStartDelayMsByPath({}); + } if (!currentSourcePath) { return () => { @@ -37,8 +42,10 @@ export function useSourceAudioFallback({ return; } if (!result.success) { - setSourceAudioFallbackPaths([]); - setSourceAudioFallbackStartDelayMsByPath({}); + if (sourceChanged) { + setSourceAudioFallbackPaths([]); + setSourceAudioFallbackStartDelayMsByPath({}); + } toast.warning( result.error ? `Could not load companion audio sources: ${summarizeErrorMessage(result.error)}` @@ -53,8 +60,10 @@ export function useSourceAudioFallback({ setSourceAudioFallbackStartDelayMsByPath(result.startDelayMsByPath ?? {}); } catch (error) { if (!cancelled) { - setSourceAudioFallbackPaths([]); - setSourceAudioFallbackStartDelayMsByPath({}); + if (sourceChanged) { + setSourceAudioFallbackPaths([]); + setSourceAudioFallbackStartDelayMsByPath({}); + } toast.warning( `Could not load companion audio sources: ${summarizeErrorMessage(String(error))}`, { id: SOURCE_AUDIO_FALLBACK_TOAST_ID, duration: 10000 }, From df95723635ed5b8126219a89e61f2a45579f1e77 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Mon, 25 May 2026 02:57:09 +0700 Subject: [PATCH 2/2] fix: clear source history on recording load --- electron/ipc/recording/windows.test.ts | 43 +++++++++++++++++++++ src/components/video-editor/VideoEditor.tsx | 5 ++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/electron/ipc/recording/windows.test.ts b/electron/ipc/recording/windows.test.ts index 025f5a166..73a1e0c24 100644 --- a/electron/ipc/recording/windows.test.ts +++ b/electron/ipc/recording/windows.test.ts @@ -45,6 +45,49 @@ describe("waitForWindowsCaptureStop", () => { expect(proc.kill).not.toHaveBeenCalled(); }); + it("resolves the fallback target path when the helper closes cleanly without output path", async () => { + const proc = new FakeCaptureProcess(); + setWindowsCaptureOutputBuffer("Recording stopped without output path"); + setWindowsCaptureTargetPath("C:\\Recordly\\fallback.mp4"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("close", 0); + + await expect(stopped).resolves.toBe("C:\\Recordly\\fallback.mp4"); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("rejects with helper output when the helper exits with a non-zero code", async () => { + const proc = new FakeCaptureProcess(); + setWindowsCaptureOutputBuffer("Encoder error: insufficient memory"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("close", 1); + + await expect(stopped).rejects.toThrow("Encoder error: insufficient memory"); + expect(proc.kill).not.toHaveBeenCalled(); + }); + + it("rejects when the helper emits an error", async () => { + const proc = new FakeCaptureProcess(); + const error = new Error("spawn failed"); + + const stopped = waitForWindowsCaptureStop( + proc as unknown as Parameters[0], + 1000, + ); + proc.emit("error", error); + + await expect(stopped).rejects.toBe(error); + expect(proc.kill).not.toHaveBeenCalled(); + }); + it("kills the helper and rejects when stop never completes", async () => { const proc = new FakeCaptureProcess(); diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 975f5aeaa..40d40dd96 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -2088,7 +2088,10 @@ export default function VideoEditor() { nextAudioIdRef.current = 1; nextAnnotationIdRef.current = 1; nextAnnotationZIndexRef.current = 1; - }, []); + resetEditorHistoryStack(editorHistoryRef.current); + applyingHistoryRef.current = false; + syncHistoryButtons(); + }, [syncHistoryButtons]); const handleUploadWebcam = useCallback(async () => { const result = await window.electronAPI.openVideoFilePicker();