diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 178313d..4716472 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 66ea746..fdabf6c 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 6e0e0d5..ad0a33f 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 15773df..77b7d99 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 a5ed36f..daa6e5e 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 afe2cc9..a3a149d 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 b1e0bb9..6a17c1e 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,