From 1670db41a833c54454e2070d0ab81c3974de450d Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Thu, 16 Apr 2026 15:53:31 +0530 Subject: [PATCH 1/9] feat:add countdown before record start --- electron/electron-env.d.ts | 4 + electron/ipc/handlers.ts | 43 +++++++++- electron/main.ts | 22 ++++- electron/preload.ts | 14 ++++ electron/windows.ts | 52 ++++++++++++ src/App.tsx | 19 +++-- src/components/launch/CountdownOverlay.tsx | 19 +++++ src/hooks/useScreenRecorder.ts | 98 +++++++++++++++++++++- src/main.tsx | 11 +++ 9 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 src/components/launch/CountdownOverlay.tsx diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index b2a37205..178313dc 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -135,6 +135,10 @@ interface Window { saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>; hudOverlayHide: () => void; hudOverlayClose: () => void; + showCountdownOverlay: (value: number) => Promise; + setCountdownOverlayValue: (value: number) => Promise; + hideCountdownOverlay: () => Promise; + onCountdownOverlayValue: (callback: (value: number) => void) => () => void; setMicrophoneExpanded: (expanded: boolean) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; onRequestSaveBeforeClose: (callback: () => Promise | boolean) => () => void; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index be20fcde..66ea746a 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -352,11 +352,50 @@ function sampleCursorPoint() { export function registerIpcHandlers( createEditorWindow: () => void, createSourceSelectorWindow: () => BrowserWindow, + createCountdownOverlayWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow | null, getSourceSelectorWindow: () => BrowserWindow | null, + getCountdownOverlayWindow: () => BrowserWindow | null, onRecordingStateChange?: (recording: boolean, sourceName: string) => void, switchToHud?: () => void, ) { + ipcMain.handle("countdown-overlay-show", async (_, value: number) => { + const win = getCountdownOverlayWindow() ?? createCountdownOverlayWindow(); + if (win.isDestroyed()) { + return; + } + + if (win.webContents.isLoading()) { + win.webContents.once("did-finish-load", () => { + if (!win.isDestroyed()) { + win.webContents.send("countdown-overlay-value", value); + win.showInactive(); + } + }); + } else { + win.webContents.send("countdown-overlay-value", value); + win.showInactive(); + } + }); + + ipcMain.handle("countdown-overlay-set-value", (_, value: number) => { + const win = getCountdownOverlayWindow(); + if (!win || win.isDestroyed()) { + return; + } + + win.webContents.send("countdown-overlay-value", value); + }); + + ipcMain.handle("countdown-overlay-hide", () => { + const win = getCountdownOverlayWindow(); + if (!win || win.isDestroyed()) { + return; + } + + win.hide(); + }); + ipcMain.handle("switch-to-hud", () => { if (switchToHud) switchToHud(); }); @@ -518,9 +557,8 @@ export function registerIpcHandlers( }); ipcMain.handle("read-binary-file", async (_, inputPath: string) => { - let normalizedPath: string | null = null; try { - normalizedPath = normalizeVideoSourcePath(inputPath); + const normalizedPath = normalizeVideoSourcePath(inputPath); if (!normalizedPath) { return { success: false, message: "Invalid file path" }; } @@ -545,7 +583,6 @@ export function registerIpcHandlers( success: false, message: "Failed to read binary file", error: String(error), - path: normalizedPath, }; } }); diff --git a/electron/main.ts b/electron/main.ts index c399fd09..6e0e0d50 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -14,7 +14,12 @@ import { } from "electron"; import { mainT, setMainLocale } from "./i18n"; import { registerIpcHandlers } from "./ipc/handlers"; -import { createEditorWindow, createHudOverlayWindow, createSourceSelectorWindow } from "./windows"; +import { + createCountdownOverlayWindow, + createEditorWindow, + createHudOverlayWindow, + createSourceSelectorWindow, +} from "./windows"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -60,6 +65,7 @@ process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL // Window references let mainWindow: BrowserWindow | null = null; let sourceSelectorWindow: BrowserWindow | null = null; +let countdownOverlayWindow: BrowserWindow | null = null; let tray: Tray | null = null; let selectedSourceName = ""; const isMac = process.platform === "darwin"; @@ -322,6 +328,18 @@ function createSourceSelectorWindowWrapper() { return sourceSelectorWindow; } +function createCountdownOverlayWindowWrapper() { + if (countdownOverlayWindow && !countdownOverlayWindow.isDestroyed()) { + return countdownOverlayWindow; + } + + countdownOverlayWindow = createCountdownOverlayWindow(); + countdownOverlayWindow.on("closed", () => { + countdownOverlayWindow = null; + }); + return countdownOverlayWindow; +} + // On macOS, applications and their menu bar stay active until the user quits // explicitly with Cmd + Q. app.on("window-all-closed", () => { @@ -386,8 +404,10 @@ app.whenReady().then(async () => { registerIpcHandlers( createEditorWindowWrapper, createSourceSelectorWindowWrapper, + createCountdownOverlayWindowWrapper, () => mainWindow, () => sourceSelectorWindow, + () => countdownOverlayWindow, (recording: boolean, sourceName: string) => { selectedSourceName = sourceName; if (!tray) createTray(); diff --git a/electron/preload.ts b/electron/preload.ts index eeca25cd..15773df4 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -130,6 +130,20 @@ contextBridge.exposeInMainWorld("electronAPI", { setHasUnsavedChanges: (hasChanges: boolean) => { ipcRenderer.send("set-has-unsaved-changes", hasChanges); }, + showCountdownOverlay: (value: number) => { + return ipcRenderer.invoke("countdown-overlay-show", value); + }, + setCountdownOverlayValue: (value: number) => { + return ipcRenderer.invoke("countdown-overlay-set-value", value); + }, + hideCountdownOverlay: () => { + return ipcRenderer.invoke("countdown-overlay-hide"); + }, + onCountdownOverlayValue: (callback: (value: number) => void) => { + const listener = (_event: unknown, value: number) => callback(value); + ipcRenderer.on("countdown-overlay-value", listener); + return () => ipcRenderer.removeListener("countdown-overlay-value", listener); + }, onRequestSaveBeforeClose: (callback: () => Promise | boolean) => { const listener = async () => { try { diff --git a/electron/windows.ts b/electron/windows.ts index dcd9f92b..a5ed36f8 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -177,3 +177,55 @@ export function createSourceSelectorWindow(): BrowserWindow { return win; } + +/** + * Creates a centered transparent countdown overlay window that sits above the + * HUD while recording pre-roll is running. + */ +export function createCountdownOverlayWindow(): BrowserWindow { + const { workArea } = screen.getPrimaryDisplay(); + const overlayWidth = 420; + const overlayHeight = 260; + + const win = new BrowserWindow({ + width: overlayWidth, + height: overlayHeight, + minWidth: overlayWidth, + maxWidth: overlayWidth, + minHeight: overlayHeight, + maxHeight: overlayHeight, + x: Math.round(workArea.x + (workArea.width - overlayWidth) / 2), + y: Math.round(workArea.y + (workArea.height - overlayHeight) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + focusable: false, + transparent: true, + backgroundColor: "#00000000", + hasShadow: false, + show: !HEADLESS, + webPreferences: { + preload: path.join(__dirname, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false, + }, + }); + + win.setIgnoreMouseEvents(true); + + if (process.platform === "darwin") { + win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); + } + + if (VITE_DEV_SERVER_URL) { + win.loadURL(VITE_DEV_SERVER_URL + "?windowType=countdown-overlay"); + } else { + win.loadFile(path.join(RENDERER_DIST, "index.html"), { + query: { windowType: "countdown-overlay" }, + }); + } + + return win; +} diff --git a/src/App.tsx b/src/App.tsx index 9772ef89..a947c9e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { CountdownOverlay } from "./components/launch/CountdownOverlay.tsx"; import { LaunchWindow } from "./components/launch/LaunchWindow"; import { SourceSelector } from "./components/launch/SourceSelector"; import { Toaster } from "./components/ui/sonner"; @@ -9,13 +10,17 @@ import { ShortcutsProvider } from "./contexts/ShortcutsContext"; import { loadAllCustomFonts } from "./lib/customFonts"; export default function App() { - const [windowType, setWindowType] = useState(""); + const [windowType, setWindowType] = useState( + () => new URLSearchParams(window.location.search).get("windowType") || "", + ); useEffect(() => { - const params = new URLSearchParams(window.location.search); - const type = params.get("windowType") || ""; - setWindowType(type); - if (type === "hud-overlay" || type === "source-selector") { + const type = new URLSearchParams(window.location.search).get("windowType") || ""; + if (type !== windowType) { + setWindowType(type); + } + + if (type === "hud-overlay" || type === "source-selector" || type === "countdown-overlay") { document.body.style.background = "transparent"; document.documentElement.style.background = "transparent"; document.getElementById("root")?.style.setProperty("background", "transparent"); @@ -25,7 +30,7 @@ export default function App() { loadAllCustomFonts().catch((error) => { console.error("Failed to load custom fonts:", error); }); - }, []); + }, [windowType]); const content = (() => { switch (windowType) { @@ -33,6 +38,8 @@ export default function App() { return ; case "source-selector": return ; + case "countdown-overlay": + return ; case "editor": return ( diff --git a/src/components/launch/CountdownOverlay.tsx b/src/components/launch/CountdownOverlay.tsx new file mode 100644 index 00000000..afe2cc93 --- /dev/null +++ b/src/components/launch/CountdownOverlay.tsx @@ -0,0 +1,19 @@ +import { useEffect, useState } from "react"; + +export function CountdownOverlay() { + const [value, setValue] = useState(3); + + useEffect(() => { + const unsubscribe = window.electronAPI.onCountdownOverlayValue((nextValue) => { + setValue(nextValue); + }); + + return () => unsubscribe(); + }, []); + + return ( +
+
{value}
+
+ ); +} diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 5cbc54a1..b1e0bb9d 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -110,6 +110,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const allowAutoFinalize = useRef(false); const discardRecordingId = useRef(null); const restarting = useRef(false); + const countdownRunId = useRef(0); + const [countdownActive, setCountdownActive] = useState(false); const getRecordingDurationMs = useCallback(() => { const segmentDuration = @@ -335,6 +337,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return () => { if (cleanup) cleanup(); + countdownRunId.current += 1; + void window.electronAPI.hideCountdownOverlay(); allowAutoFinalize.current = false; restarting.current = false; discardRecordingId.current = null; @@ -365,6 +369,88 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }; }, [teardownMedia]); + const cancelCountdown = () => { + countdownRunId.current += 1; + setCountdownActive(false); + void window.electronAPI.hideCountdownOverlay(); + }; + + const safeShowCountdownOverlay = async (value: number) => { + try { + await window.electronAPI.showCountdownOverlay(value); + return true; + } catch (error) { + console.warn("Failed to show countdown overlay:", error); + return false; + } + }; + + const safeSetCountdownOverlayValue = async (value: number) => { + try { + await window.electronAPI.setCountdownOverlayValue(value); + } catch (error) { + console.warn("Failed to update countdown overlay value:", error); + } + }; + + const safeHideCountdownOverlay = async () => { + try { + await window.electronAPI.hideCountdownOverlay(); + } catch (error) { + console.warn("Failed to hide countdown overlay:", error); + } + }; + + const startRecordCountdown = async () => { + if (countdownActive || recording) { + return; + } + + let selectedSource: ProcessedDesktopSource | null = null; + try { + selectedSource = await window.electronAPI.getSelectedSource(); + } catch (error) { + console.warn("Failed to read selected source before countdown:", error); + } + + if (!selectedSource) { + alert(t("recording.selectSource")); + return; + } + + const runId = countdownRunId.current + 1; + countdownRunId.current = runId; + setCountdownActive(true); + + try { + const values = [3, 2, 1]; + const overlayShown = await safeShowCountdownOverlay(values[0]); + + for (const value of values) { + if (countdownRunId.current !== runId) { + return; + } + + if (overlayShown && value !== values[0]) { + await safeSetCountdownOverlayValue(value); + } + + await new Promise((resolve) => window.setTimeout(resolve, 1000)); + } + + if (countdownRunId.current !== runId) { + return; + } + + await startRecording(); + } finally { + if (countdownRunId.current === runId) { + setCountdownActive(false); + } + await safeHideCountdownOverlay(); + } + }; + const startRecording = async () => { try { const selectedSource = await window.electronAPI.getSelectedSource(); @@ -635,7 +721,12 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }; const toggleRecording = () => { - recording ? stopRecording.current() : startRecording(); + if (countdownActive) { + cancelCountdown(); + return; + } + + recording ? stopRecording.current() : void startRecordCountdown(); }; const restartRecording = async () => { @@ -698,6 +789,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }, [getRecordingDurationMs, paused, recording]); const cancelRecording = () => { + if (countdownActive) { + cancelCountdown(); + return; + } + const activeScreenRecorder = screenRecorder.current; if (!activeScreenRecorder || activeScreenRecorder.recorder.state !== "recording") return; diff --git a/src/main.tsx b/src/main.tsx index 670e2b6f..365bdc7b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,17 @@ import App from "./App.tsx"; import { I18nProvider } from "./contexts/I18nContext"; import "./index.css"; +const windowType = new URLSearchParams(window.location.search).get("windowType") || ""; +if ( + windowType === "hud-overlay" || + windowType === "source-selector" || + windowType === "countdown-overlay" +) { + document.body.style.background = "transparent"; + document.documentElement.style.background = "transparent"; + document.getElementById("root")?.style.setProperty("background", "transparent"); +} + ReactDOM.createRoot(document.getElementById("root")!).render( From 6b08a0a72a6c32f3c17d80e8741df940dad6d96d Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Thu, 16 Apr 2026 18:27:00 +0530 Subject: [PATCH 2/9] fix:flickering, stale runs, macOS bugs provided by coderabbit and thread countdown token --- electron/electron-env.d.ts | 2 +- electron/ipc/handlers.ts | 46 ++++++++++++++++++---- electron/main.ts | 13 +++++- electron/preload.ts | 4 +- electron/windows.ts | 2 +- src/components/launch/CountdownOverlay.tsx | 8 +++- src/hooks/useScreenRecorder.ts | 34 ++++++++++++++-- 7 files changed, 91 insertions(+), 18 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 178313dc..4716472e 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -138,7 +138,7 @@ interface Window { showCountdownOverlay: (value: number) => Promise; setCountdownOverlayValue: (value: number) => Promise; hideCountdownOverlay: () => Promise; - onCountdownOverlayValue: (callback: (value: number) => void) => () => void; + onCountdownOverlayValue: (callback: (value: number | null) => void) => () => void; setMicrophoneExpanded: (expanded: boolean) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; onRequestSaveBeforeClose: (callback: () => Promise | boolean) => () => void; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 66ea746a..fdabf6c4 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -359,7 +359,30 @@ export function registerIpcHandlers( onRecordingStateChange?: (recording: boolean, sourceName: string) => void, switchToHud?: () => void, ) { - ipcMain.handle("countdown-overlay-show", async (_, value: number) => { + const countdownOverlayState = { + visible: false, + value: null as number | null, + }; + + const flushCountdownOverlayState = (win: BrowserWindow) => { + if (win.isDestroyed()) { + return; + } + + win.webContents.send("countdown-overlay-value", countdownOverlayState.value); + if (countdownOverlayState.visible && !win.isVisible()) { + setTimeout(() => { + if (!win.isDestroyed() && countdownOverlayState.visible && !win.isVisible()) { + win.showInactive(); + } + }, 16); + } + }; + + ipcMain.handle("countdown-overlay-show", (_, value: number) => { + countdownOverlayState.visible = true; + countdownOverlayState.value = value; + const win = getCountdownOverlayWindow() ?? createCountdownOverlayWindow(); if (win.isDestroyed()) { return; @@ -368,38 +391,47 @@ export function registerIpcHandlers( if (win.webContents.isLoading()) { win.webContents.once("did-finish-load", () => { if (!win.isDestroyed()) { - win.webContents.send("countdown-overlay-value", value); - win.showInactive(); + flushCountdownOverlayState(win); } }); } else { - win.webContents.send("countdown-overlay-value", value); - win.showInactive(); + flushCountdownOverlayState(win); } }); ipcMain.handle("countdown-overlay-set-value", (_, value: number) => { + countdownOverlayState.value = value; + const win = getCountdownOverlayWindow(); if (!win || win.isDestroyed()) { return; } + if (win.webContents.isLoading()) { + return; + } + win.webContents.send("countdown-overlay-value", value); }); ipcMain.handle("countdown-overlay-hide", () => { + countdownOverlayState.visible = false; + countdownOverlayState.value = null; + const win = getCountdownOverlayWindow(); if (!win || win.isDestroyed()) { return; } - win.hide(); + if (!win.webContents.isLoading()) { + win.webContents.send("countdown-overlay-value", countdownOverlayState.value); + } }); ipcMain.handle("switch-to-hud", () => { if (switchToHud) switchToHud(); }); - ipcMain.handle("start-new-recording", async () => { + ipcMain.handle("start-new-recording", () => { try { setCurrentRecordingSessionState(null); if (switchToHud) { diff --git a/electron/main.ts b/electron/main.ts index 6e0e0d50..ad0a33fc 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -349,8 +349,17 @@ app.on("window-all-closed", () => { app.on("activate", () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); + const hasVisibleWindow = BrowserWindow.getAllWindows().some((window) => { + if (window.isDestroyed() || !window.isVisible()) { + return false; + } + + const url = window.webContents.getURL(); + const isCountdownOverlayWindow = url.includes("windowType=countdown-overlay"); + return !isCountdownOverlayWindow; + }); + if (!hasVisibleWindow) { + showMainWindow(); } }); diff --git a/electron/preload.ts b/electron/preload.ts index 15773df4..77b7d99d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -139,8 +139,8 @@ contextBridge.exposeInMainWorld("electronAPI", { hideCountdownOverlay: () => { return ipcRenderer.invoke("countdown-overlay-hide"); }, - onCountdownOverlayValue: (callback: (value: number) => void) => { - const listener = (_event: unknown, value: number) => callback(value); + onCountdownOverlayValue: (callback: (value: number | null) => void) => { + const listener = (_event: unknown, value: number | null) => callback(value); ipcRenderer.on("countdown-overlay-value", listener); return () => ipcRenderer.removeListener("countdown-overlay-value", listener); }, diff --git a/electron/windows.ts b/electron/windows.ts index a5ed36f8..daa6e5e5 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -204,7 +204,7 @@ export function createCountdownOverlayWindow(): BrowserWindow { transparent: true, backgroundColor: "#00000000", hasShadow: false, - show: !HEADLESS, + show: false, webPreferences: { preload: path.join(__dirname, "preload.mjs"), nodeIntegration: false, diff --git a/src/components/launch/CountdownOverlay.tsx b/src/components/launch/CountdownOverlay.tsx index afe2cc93..a3a149dd 100644 --- a/src/components/launch/CountdownOverlay.tsx +++ b/src/components/launch/CountdownOverlay.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; export function CountdownOverlay() { - const [value, setValue] = useState(3); + const [value, setValue] = useState(null); useEffect(() => { const unsubscribe = window.electronAPI.onCountdownOverlayValue((nextValue) => { @@ -13,7 +13,11 @@ export function CountdownOverlay() { return (
-
{value}
+ {value === null ? null : ( +
+ {value} +
+ )}
); } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index b1e0bb9d..6a17c1e8 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -401,6 +401,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; + const isCountdownRunActive = (runId?: number) => + runId === undefined || countdownRunId.current === runId; + const startRecordCountdown = async () => { if (countdownActive || recording) { return; @@ -442,16 +445,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } - await startRecording(); + await startRecording(runId); } finally { if (countdownRunId.current === runId) { setCountdownActive(false); + await safeHideCountdownOverlay(); } - await safeHideCountdownOverlay(); } }; - const startRecording = async () => { + const startRecording = async (countdownRunToken?: number) => { try { const selectedSource = await window.electronAPI.getSelectedSource(); if (!selectedSource) { @@ -459,6 +462,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } + if (!isCountdownRunActive(countdownRunToken)) { + teardownMedia(); + return; + } + let screenMediaStream: MediaStream; const videoConstraints = { @@ -499,6 +507,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } screenStream.current = screenMediaStream; + if (!isCountdownRunActive(countdownRunToken)) { + teardownMedia(); + return; + } + if (microphoneEnabled) { try { microphoneStream.current = await navigator.mediaDevices.getUserMedia({ @@ -523,6 +536,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } + if (!isCountdownRunActive(countdownRunToken)) { + teardownMedia(); + return; + } + if (webcamEnabled) { try { webcamStream.current = await navigator.mediaDevices.getUserMedia({ @@ -551,6 +569,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } + if (!isCountdownRunActive(countdownRunToken)) { + teardownMedia(); + return; + } + stream.current = new MediaStream(); const videoTrack = screenMediaStream.getVideoTracks()[0]; if (!videoTrack) { @@ -610,6 +633,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); const hasAudio = stream.current.getAudioTracks().length > 0; + if (!isCountdownRunActive(countdownRunToken)) { + teardownMedia(); + return; + } + screenRecorder.current = createRecorderHandle(stream.current, { mimeType, videoBitsPerSecond, From ea68e4cfc3b979afe109367f6ab7405c2783edde Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Thu, 16 Apr 2026 19:22:57 +0530 Subject: [PATCH 3/9] fix:prevent stale countdown IPC updates from repainting overlay --- electron/electron-env.d.ts | 6 ++-- electron/ipc/handlers.ts | 16 +++++++++-- electron/preload.ts | 12 ++++---- src/components/launch/CountdownOverlay.tsx | 5 +++- src/hooks/useScreenRecorder.ts | 32 ++++++++++++++-------- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 4716472e..e20cf7ff 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -135,9 +135,9 @@ interface Window { saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>; hudOverlayHide: () => void; hudOverlayClose: () => void; - showCountdownOverlay: (value: number) => Promise; - setCountdownOverlayValue: (value: number) => Promise; - hideCountdownOverlay: () => Promise; + showCountdownOverlay: (value: number, runId: number) => Promise; + setCountdownOverlayValue: (value: number, runId: number) => Promise; + hideCountdownOverlay: (runId: number) => Promise; onCountdownOverlayValue: (callback: (value: number | null) => void) => () => void; setMicrophoneExpanded: (expanded: boolean) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index fdabf6c4..729408f1 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -362,6 +362,7 @@ export function registerIpcHandlers( const countdownOverlayState = { visible: false, value: null as number | null, + activeRunId: null as number | null, }; const flushCountdownOverlayState = (win: BrowserWindow) => { @@ -379,7 +380,8 @@ export function registerIpcHandlers( } }; - ipcMain.handle("countdown-overlay-show", (_, value: number) => { + ipcMain.handle("countdown-overlay-show", (_, value: number, runId: number) => { + countdownOverlayState.activeRunId = runId; countdownOverlayState.visible = true; countdownOverlayState.value = value; @@ -399,7 +401,11 @@ export function registerIpcHandlers( } }); - ipcMain.handle("countdown-overlay-set-value", (_, value: number) => { + ipcMain.handle("countdown-overlay-set-value", (_, value: number, runId: number) => { + if (countdownOverlayState.activeRunId !== runId || !countdownOverlayState.visible) { + return; + } + countdownOverlayState.value = value; const win = getCountdownOverlayWindow(); @@ -414,7 +420,11 @@ export function registerIpcHandlers( win.webContents.send("countdown-overlay-value", value); }); - ipcMain.handle("countdown-overlay-hide", () => { + ipcMain.handle("countdown-overlay-hide", (_, runId: number) => { + if (countdownOverlayState.activeRunId !== runId) { + return; + } + countdownOverlayState.visible = false; countdownOverlayState.value = null; diff --git a/electron/preload.ts b/electron/preload.ts index 77b7d99d..6aa066fb 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -130,14 +130,14 @@ contextBridge.exposeInMainWorld("electronAPI", { setHasUnsavedChanges: (hasChanges: boolean) => { ipcRenderer.send("set-has-unsaved-changes", hasChanges); }, - showCountdownOverlay: (value: number) => { - return ipcRenderer.invoke("countdown-overlay-show", value); + showCountdownOverlay: (value: number, runId: number) => { + return ipcRenderer.invoke("countdown-overlay-show", value, runId); }, - setCountdownOverlayValue: (value: number) => { - return ipcRenderer.invoke("countdown-overlay-set-value", value); + setCountdownOverlayValue: (value: number, runId: number) => { + return ipcRenderer.invoke("countdown-overlay-set-value", value, runId); }, - hideCountdownOverlay: () => { - return ipcRenderer.invoke("countdown-overlay-hide"); + hideCountdownOverlay: (runId: number) => { + return ipcRenderer.invoke("countdown-overlay-hide", runId); }, onCountdownOverlayValue: (callback: (value: number | null) => void) => { const listener = (_event: unknown, value: number | null) => callback(value); diff --git a/src/components/launch/CountdownOverlay.tsx b/src/components/launch/CountdownOverlay.tsx index a3a149dd..1ee5c975 100644 --- a/src/components/launch/CountdownOverlay.tsx +++ b/src/components/launch/CountdownOverlay.tsx @@ -14,7 +14,10 @@ export function CountdownOverlay() { return (
{value === null ? null : ( -
+
{value}
)} diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 6a17c1e8..51c57dd9 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -336,9 +336,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } return () => { + const activeRunId = countdownRunId.current; if (cleanup) cleanup(); countdownRunId.current += 1; - void window.electronAPI.hideCountdownOverlay(); + void window.electronAPI.hideCountdownOverlay(activeRunId); allowAutoFinalize.current = false; restarting.current = false; discardRecordingId.current = null; @@ -370,14 +371,15 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }, [teardownMedia]); const cancelCountdown = () => { + const activeRunId = countdownRunId.current; countdownRunId.current += 1; setCountdownActive(false); - void window.electronAPI.hideCountdownOverlay(); + void window.electronAPI.hideCountdownOverlay(activeRunId); }; - const safeShowCountdownOverlay = async (value: number) => { + const safeShowCountdownOverlay = async (value: number, runId: number) => { try { - await window.electronAPI.showCountdownOverlay(value); + await window.electronAPI.showCountdownOverlay(value, runId); return true; } catch (error) { console.warn("Failed to show countdown overlay:", error); @@ -385,17 +387,17 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; - const safeSetCountdownOverlayValue = async (value: number) => { + const safeSetCountdownOverlayValue = async (value: number, runId: number) => { try { - await window.electronAPI.setCountdownOverlayValue(value); + await window.electronAPI.setCountdownOverlayValue(value, runId); } catch (error) { console.warn("Failed to update countdown overlay value:", error); } }; - const safeHideCountdownOverlay = async () => { + const safeHideCountdownOverlay = async (runId: number) => { try { - await window.electronAPI.hideCountdownOverlay(); + await window.electronAPI.hideCountdownOverlay(runId); } catch (error) { console.warn("Failed to hide countdown overlay:", error); } @@ -427,7 +429,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { try { const values = [3, 2, 1]; - const overlayShown = await safeShowCountdownOverlay(values[0]); + const overlayShown = await safeShowCountdownOverlay(values[0], runId); + + if (countdownRunId.current !== runId) { + return; + } for (const value of values) { if (countdownRunId.current !== runId) { @@ -435,7 +441,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } if (overlayShown && value !== values[0]) { - await safeSetCountdownOverlayValue(value); + await safeSetCountdownOverlayValue(value, runId); + + if (countdownRunId.current !== runId) { + return; + } } await new Promise((resolve) => window.setTimeout(resolve, 1000)); @@ -449,7 +459,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } finally { if (countdownRunId.current === runId) { setCountdownActive(false); - await safeHideCountdownOverlay(); + await safeHideCountdownOverlay(runId); } } }; From d04bab732bac5bb41f5764acc077d4ba03d12eff Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Thu, 16 Apr 2026 19:44:51 +0530 Subject: [PATCH 4/9] prioritize recording stop over countdown cancel --- src/hooks/useScreenRecorder.ts | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 51c57dd9..44dece06 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -759,12 +759,17 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }; const toggleRecording = () => { + if (recording) { + stopRecording.current(); + return; + } + if (countdownActive) { cancelCountdown(); return; } - recording ? stopRecording.current() : void startRecordCountdown(); + void startRecordCountdown(); }; const restartRecording = async () => { @@ -827,19 +832,23 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }, [getRecordingDurationMs, paused, recording]); const cancelRecording = () => { + const activeScreenRecorder = screenRecorder.current; + if ( + activeScreenRecorder?.recorder.state === "recording" || + activeScreenRecorder?.recorder.state === "paused" + ) { + const activeRecordingId = recordingId.current; + discardRecordingId.current = activeRecordingId; + allowAutoFinalize.current = false; + + stopRecording.current(); + return; + } + if (countdownActive) { cancelCountdown(); return; } - - const activeScreenRecorder = screenRecorder.current; - if (!activeScreenRecorder || activeScreenRecorder.recorder.state !== "recording") return; - - const activeRecordingId = recordingId.current; - discardRecordingId.current = activeRecordingId; - allowAutoFinalize.current = false; - - stopRecording.current(); }; return { From 331e126d3c4b37d9c0f1948805b9439d72b2e1b9 Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Thu, 16 Apr 2026 20:10:56 +0530 Subject: [PATCH 5/9] fix:handle hideCountdownOverlay rejections in cleanup/cancel paths. --- src/hooks/useScreenRecorder.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 44dece06..f208013a 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -339,7 +339,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const activeRunId = countdownRunId.current; if (cleanup) cleanup(); countdownRunId.current += 1; - void window.electronAPI.hideCountdownOverlay(activeRunId); + void window.electronAPI.hideCountdownOverlay(activeRunId).catch((error) => { + console.warn("Failed to hide countdown overlay during cleanup:", error); + }); allowAutoFinalize.current = false; restarting.current = false; discardRecordingId.current = null; @@ -374,7 +376,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const activeRunId = countdownRunId.current; countdownRunId.current += 1; setCountdownActive(false); - void window.electronAPI.hideCountdownOverlay(activeRunId); + void window.electronAPI.hideCountdownOverlay(activeRunId).catch((error) => { + console.warn("Failed to hide countdown overlay during cancel:", error); + }); }; const safeShowCountdownOverlay = async (value: number, runId: number) => { From 3ba9e901c9f17e16638c3cf542e28e26ca3305ba Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Thu, 16 Apr 2026 20:36:25 +0530 Subject: [PATCH 6/9] fix:Claim the countdown run before the first await. --- src/hooks/useScreenRecorder.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index f208013a..7c9112da 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -415,6 +415,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } + const runId = countdownRunId.current + 1; + countdownRunId.current = runId; + setCountdownActive(true); + let selectedSource: ProcessedDesktopSource | null = null; try { selectedSource = await window.electronAPI.getSelectedSource(); @@ -422,15 +426,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn { console.warn("Failed to read selected source before countdown:", error); } + if (!isCountdownRunActive(runId)) { + return; + } + if (!selectedSource) { + if (countdownRunId.current === runId) { + setCountdownActive(false); + } alert(t("recording.selectSource")); return; } - const runId = countdownRunId.current + 1; - countdownRunId.current = runId; - setCountdownActive(true); - try { const values = [3, 2, 1]; const overlayShown = await safeShowCountdownOverlay(values[0], runId); From 65b9d189e83998b01ac52f6f5468c5c2c84ea346 Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Fri, 17 Apr 2026 10:55:03 +0530 Subject: [PATCH 7/9] fix:improve ui of the countdown by adding a low opacity circle background --- src/components/launch/CountdownOverlay.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/launch/CountdownOverlay.tsx b/src/components/launch/CountdownOverlay.tsx index 1ee5c975..54ec8e85 100644 --- a/src/components/launch/CountdownOverlay.tsx +++ b/src/components/launch/CountdownOverlay.tsx @@ -14,11 +14,13 @@ export function CountdownOverlay() { return (
{value === null ? null : ( -
- {value} +
+
+ {value} +
)}
From 7e02856836ed52b490f9b07eb031b3eae750e59a Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Sun, 19 Apr 2026 12:35:19 +0530 Subject: [PATCH 8/9] fix:hide handler actually hides window instead of just clearing value --- electron/ipc/handlers.ts | 81 ++++++++++++++++++++-- src/components/launch/CountdownOverlay.tsx | 20 +++--- src/hooks/useScreenRecorder.ts | 9 ++- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 729408f1..261d93fc 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -359,10 +359,37 @@ export function registerIpcHandlers( onRecordingStateChange?: (recording: boolean, sourceName: string) => void, switchToHud?: () => void, ) { + const supportsWindowOpacity = process.platform !== "linux"; const countdownOverlayState = { visible: false, value: null as number | null, activeRunId: null as number | null, + hideCommitId: 0, + hideCommitTimer: null as ReturnType | null, + }; + const COUNTDOWN_OVERLAY_HIDE_DEBOUNCE_MS = 1200; + + const clearCountdownOverlayHideCommit = () => { + if (countdownOverlayState.hideCommitTimer) { + clearTimeout(countdownOverlayState.hideCommitTimer); + countdownOverlayState.hideCommitTimer = null; + } + }; + + const commitCountdownOverlayHide = (win: BrowserWindow, hideCommitId: number) => { + if (win.isDestroyed()) { + return; + } + + if (countdownOverlayState.visible || countdownOverlayState.hideCommitId !== hideCommitId) { + return; + } + + win.hide(); + if (supportsWindowOpacity) { + // Reset baseline opacity for the next show cycle. + win.setOpacity(1); + } }; const flushCountdownOverlayState = (win: BrowserWindow) => { @@ -370,14 +397,35 @@ export function registerIpcHandlers( return; } + clearCountdownOverlayHideCommit(); win.webContents.send("countdown-overlay-value", countdownOverlayState.value); - if (countdownOverlayState.visible && !win.isVisible()) { - setTimeout(() => { - if (!win.isDestroyed() && countdownOverlayState.visible && !win.isVisible()) { - win.showInactive(); - } - }, 16); + if (!countdownOverlayState.visible) { + return; } + + if (win.isVisible()) { + if (supportsWindowOpacity) { + win.setOpacity(1); + } + return; + } + + setTimeout(() => { + if (!win.isDestroyed() && countdownOverlayState.visible && !win.isVisible()) { + if (supportsWindowOpacity) { + win.setOpacity(0); + } + win.showInactive(); + + if (supportsWindowOpacity) { + setTimeout(() => { + if (!win.isDestroyed() && countdownOverlayState.visible && win.isVisible()) { + win.setOpacity(1); + } + }, 0); + } + } + }, 16); }; ipcMain.handle("countdown-overlay-show", (_, value: number, runId: number) => { @@ -426,16 +474,35 @@ export function registerIpcHandlers( } countdownOverlayState.visible = false; - countdownOverlayState.value = null; + countdownOverlayState.hideCommitId += 1; + const hideCommitId = countdownOverlayState.hideCommitId; + clearCountdownOverlayHideCommit(); const win = getCountdownOverlayWindow(); if (!win || win.isDestroyed()) { + countdownOverlayState.value = null; return; } + if (supportsWindowOpacity) { + // Hide visually immediately to avoid hide/show compositor flashes on rapid restart. + win.setOpacity(0); + } + + countdownOverlayState.value = null; if (!win.webContents.isLoading()) { win.webContents.send("countdown-overlay-value", countdownOverlayState.value); } + + if (!supportsWindowOpacity) { + win.hide(); + return; + } + + countdownOverlayState.hideCommitTimer = setTimeout(() => { + countdownOverlayState.hideCommitTimer = null; + commitCountdownOverlayHide(win, hideCommitId); + }, COUNTDOWN_OVERLAY_HIDE_DEBOUNCE_MS); }); ipcMain.handle("switch-to-hud", () => { diff --git a/src/components/launch/CountdownOverlay.tsx b/src/components/launch/CountdownOverlay.tsx index 54ec8e85..71d12c50 100644 --- a/src/components/launch/CountdownOverlay.tsx +++ b/src/components/launch/CountdownOverlay.tsx @@ -11,18 +11,20 @@ export function CountdownOverlay() { return () => unsubscribe(); }, []); + if (value === null) { + return null; + } + return (
- {value === null ? null : ( -
-
- {value} -
+
+
+ {value}
- )} +
); } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 7c9112da..6644f42f 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -339,9 +339,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const activeRunId = countdownRunId.current; if (cleanup) cleanup(); countdownRunId.current += 1; - void window.electronAPI.hideCountdownOverlay(activeRunId).catch((error) => { - console.warn("Failed to hide countdown overlay during cleanup:", error); - }); + void safeHideCountdownOverlay(activeRunId); allowAutoFinalize.current = false; restarting.current = false; discardRecordingId.current = null; @@ -635,6 +633,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); } + if (!isCountdownRunActive(countdownRunToken)) { + teardownMedia(); + return; + } + let { width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT, From 4a65ab81711a12f637245bc249edab4636568a2c Mon Sep 17 00:00:00 2001 From: Galactic99 Date: Sun, 19 Apr 2026 12:57:17 +0530 Subject: [PATCH 9/9] chore:safewrapper consistency and hide countdown overlay before starting recording setup. --- src/App.tsx | 4 +++- src/hooks/useScreenRecorder.ts | 27 +++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a947c9e3..4045b5dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,12 +25,14 @@ export default function App() { document.documentElement.style.background = "transparent"; document.getElementById("root")?.style.setProperty("background", "transparent"); } + }, [windowType]); + useEffect(() => { // Load custom fonts on app initialization loadAllCustomFonts().catch((error) => { console.error("Failed to load custom fonts:", error); }); - }, [windowType]); + }, []); const content = (() => { switch (windowType) { diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 6644f42f..ca14dfe8 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -370,15 +370,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { }; }, [teardownMedia]); - const cancelCountdown = () => { - const activeRunId = countdownRunId.current; - countdownRunId.current += 1; - setCountdownActive(false); - void window.electronAPI.hideCountdownOverlay(activeRunId).catch((error) => { - console.warn("Failed to hide countdown overlay during cancel:", error); - }); - }; - const safeShowCountdownOverlay = async (value: number, runId: number) => { try { await window.electronAPI.showCountdownOverlay(value, runId); @@ -389,6 +380,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; + const cancelCountdown = () => { + const activeRunId = countdownRunId.current; + countdownRunId.current += 1; + setCountdownActive(false); + void safeHideCountdownOverlay(activeRunId); + }; + const safeSetCountdownOverlayValue = async (value: number, runId: number) => { try { await window.electronAPI.setCountdownOverlayValue(value, runId); @@ -436,6 +434,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } + let overlayHiddenBeforeStart = false; try { const values = [3, 2, 1]; const overlayShown = await safeShowCountdownOverlay(values[0], runId); @@ -464,9 +463,17 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } + setCountdownActive(false); + await safeHideCountdownOverlay(runId); + overlayHiddenBeforeStart = true; + + if (countdownRunId.current !== runId) { + return; + } + await startRecording(runId); } finally { - if (countdownRunId.current === runId) { + if (!overlayHiddenBeforeStart && countdownRunId.current === runId) { setCountdownActive(false); await safeHideCountdownOverlay(runId); }