Merge pull request #460 from Galactic99/feat/countdown-before-record-start
feat:add countdown before record start
This commit is contained in:
Vendored
+4
@@ -135,6 +135,10 @@ interface Window {
|
||||
saveShortcuts: (shortcuts: unknown) => Promise<{ success: boolean; error?: string }>;
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
showCountdownOverlay: (value: number, runId: number) => Promise<void>;
|
||||
setCountdownOverlayValue: (value: number, runId: number) => Promise<void>;
|
||||
hideCountdownOverlay: (runId: number) => Promise<void>;
|
||||
onCountdownOverlayValue: (callback: (value: number | null) => void) => () => void;
|
||||
setMicrophoneExpanded: (expanded: boolean) => void;
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => () => void;
|
||||
|
||||
+150
-4
@@ -352,15 +352,163 @@ 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,
|
||||
) {
|
||||
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<typeof setTimeout> | 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) => {
|
||||
if (win.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCountdownOverlayHideCommit();
|
||||
win.webContents.send("countdown-overlay-value", countdownOverlayState.value);
|
||||
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) => {
|
||||
countdownOverlayState.activeRunId = runId;
|
||||
countdownOverlayState.visible = true;
|
||||
countdownOverlayState.value = value;
|
||||
|
||||
const win = getCountdownOverlayWindow() ?? createCountdownOverlayWindow();
|
||||
if (win.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (win.webContents.isLoading()) {
|
||||
win.webContents.once("did-finish-load", () => {
|
||||
if (!win.isDestroyed()) {
|
||||
flushCountdownOverlayState(win);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
flushCountdownOverlayState(win);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("countdown-overlay-set-value", (_, value: number, runId: number) => {
|
||||
if (countdownOverlayState.activeRunId !== runId || !countdownOverlayState.visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
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", (_, runId: number) => {
|
||||
if (countdownOverlayState.activeRunId !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
countdownOverlayState.visible = false;
|
||||
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", () => {
|
||||
if (switchToHud) switchToHud();
|
||||
});
|
||||
ipcMain.handle("start-new-recording", async () => {
|
||||
ipcMain.handle("start-new-recording", () => {
|
||||
try {
|
||||
setCurrentRecordingSessionState(null);
|
||||
if (switchToHud) {
|
||||
@@ -518,9 +666,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 +692,6 @@ export function registerIpcHandlers(
|
||||
success: false,
|
||||
message: "Failed to read binary file",
|
||||
error: String(error),
|
||||
path: normalizedPath,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
+32
-3
@@ -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", () => {
|
||||
@@ -331,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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -386,8 +413,10 @@ app.whenReady().then(async () => {
|
||||
registerIpcHandlers(
|
||||
createEditorWindowWrapper,
|
||||
createSourceSelectorWindowWrapper,
|
||||
createCountdownOverlayWindowWrapper,
|
||||
() => mainWindow,
|
||||
() => sourceSelectorWindow,
|
||||
() => countdownOverlayWindow,
|
||||
(recording: boolean, sourceName: string) => {
|
||||
selectedSourceName = sourceName;
|
||||
if (!tray) createTray();
|
||||
|
||||
@@ -130,6 +130,20 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => {
|
||||
ipcRenderer.send("set-has-unsaved-changes", hasChanges);
|
||||
},
|
||||
showCountdownOverlay: (value: number, runId: number) => {
|
||||
return ipcRenderer.invoke("countdown-overlay-show", value, runId);
|
||||
},
|
||||
setCountdownOverlayValue: (value: number, runId: number) => {
|
||||
return ipcRenderer.invoke("countdown-overlay-set-value", value, runId);
|
||||
},
|
||||
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);
|
||||
ipcRenderer.on("countdown-overlay-value", listener);
|
||||
return () => ipcRenderer.removeListener("countdown-overlay-value", listener);
|
||||
},
|
||||
onRequestSaveBeforeClose: (callback: () => Promise<boolean> | boolean) => {
|
||||
const listener = async () => {
|
||||
try {
|
||||
|
||||
@@ -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: false,
|
||||
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;
|
||||
}
|
||||
|
||||
+14
-5
@@ -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,18 +10,24 @@ 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");
|
||||
}
|
||||
}, [windowType]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load custom fonts on app initialization
|
||||
loadAllCustomFonts().catch((error) => {
|
||||
console.error("Failed to load custom fonts:", error);
|
||||
@@ -33,6 +40,8 @@ export default function App() {
|
||||
return <LaunchWindow />;
|
||||
case "source-selector":
|
||||
return <SourceSelector />;
|
||||
case "countdown-overlay":
|
||||
return <CountdownOverlay />;
|
||||
case "editor":
|
||||
return (
|
||||
<ShortcutsProvider>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function CountdownOverlay() {
|
||||
const [value, setValue] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.electronAPI.onCountdownOverlayValue((nextValue) => {
|
||||
setValue(nextValue);
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen bg-transparent flex items-center justify-center pointer-events-none select-none">
|
||||
<div className="flex items-center justify-center w-40 h-40 rounded-full bg-black/50">
|
||||
<div
|
||||
className="text-white/90 text-[80px] font-bold leading-none tabular-nums"
|
||||
style={{ textShadow: "0 4px 24px rgba(0, 0, 0, 0.65)" }}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -110,6 +110,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
const allowAutoFinalize = useRef(false);
|
||||
const discardRecordingId = useRef<number | null>(null);
|
||||
const restarting = useRef(false);
|
||||
const countdownRunId = useRef(0);
|
||||
const [countdownActive, setCountdownActive] = useState(false);
|
||||
const webcamReady = useRef(false);
|
||||
const webcamAcquireId = useRef(0);
|
||||
|
||||
@@ -411,7 +413,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
|
||||
return () => {
|
||||
const activeRunId = countdownRunId.current;
|
||||
if (cleanup) cleanup();
|
||||
countdownRunId.current += 1;
|
||||
void safeHideCountdownOverlay(activeRunId);
|
||||
allowAutoFinalize.current = false;
|
||||
restarting.current = false;
|
||||
discardRecordingId.current = null;
|
||||
@@ -442,7 +447,117 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
};
|
||||
}, [teardownMedia]);
|
||||
|
||||
const startRecording = async () => {
|
||||
const safeShowCountdownOverlay = async (value: number, runId: number) => {
|
||||
try {
|
||||
await window.electronAPI.showCountdownOverlay(value, runId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("Failed to show countdown overlay:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
console.warn("Failed to update countdown overlay value:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const safeHideCountdownOverlay = async (runId: number) => {
|
||||
try {
|
||||
await window.electronAPI.hideCountdownOverlay(runId);
|
||||
} catch (error) {
|
||||
console.warn("Failed to hide countdown overlay:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const isCountdownRunActive = (runId?: number) =>
|
||||
runId === undefined || countdownRunId.current === runId;
|
||||
|
||||
const startRecordCountdown = async () => {
|
||||
if (countdownActive || recording) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runId = countdownRunId.current + 1;
|
||||
countdownRunId.current = runId;
|
||||
setCountdownActive(true);
|
||||
|
||||
let selectedSource: ProcessedDesktopSource | null = null;
|
||||
try {
|
||||
selectedSource = await window.electronAPI.getSelectedSource();
|
||||
} catch (error) {
|
||||
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;
|
||||
}
|
||||
|
||||
let overlayHiddenBeforeStart = false;
|
||||
try {
|
||||
const values = [3, 2, 1];
|
||||
const overlayShown = await safeShowCountdownOverlay(values[0], runId);
|
||||
|
||||
if (countdownRunId.current !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
if (countdownRunId.current !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (overlayShown && value !== values[0]) {
|
||||
await safeSetCountdownOverlayValue(value, runId);
|
||||
|
||||
if (countdownRunId.current !== runId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
if (countdownRunId.current !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCountdownActive(false);
|
||||
await safeHideCountdownOverlay(runId);
|
||||
overlayHiddenBeforeStart = true;
|
||||
|
||||
if (countdownRunId.current !== runId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await startRecording(runId);
|
||||
} finally {
|
||||
if (!overlayHiddenBeforeStart && countdownRunId.current === runId) {
|
||||
setCountdownActive(false);
|
||||
await safeHideCountdownOverlay(runId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startRecording = async (countdownRunToken?: number) => {
|
||||
try {
|
||||
const selectedSource = await window.electronAPI.getSelectedSource();
|
||||
if (!selectedSource) {
|
||||
@@ -450,6 +565,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCountdownRunActive(countdownRunToken)) {
|
||||
teardownMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
let screenMediaStream: MediaStream;
|
||||
|
||||
const videoConstraints = {
|
||||
@@ -490,6 +610,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
screenStream.current = screenMediaStream;
|
||||
|
||||
if (!isCountdownRunActive(countdownRunToken)) {
|
||||
teardownMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
if (microphoneEnabled) {
|
||||
try {
|
||||
microphoneStream.current = await navigator.mediaDevices.getUserMedia({
|
||||
@@ -514,6 +639,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCountdownRunActive(countdownRunToken)) {
|
||||
teardownMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
if (webcamEnabled) {
|
||||
if (!webcamReady.current) {
|
||||
await new Promise<void>((resolve) => {
|
||||
@@ -535,6 +665,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCountdownRunActive(countdownRunToken)) {
|
||||
teardownMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
stream.current = new MediaStream();
|
||||
const videoTrack = screenMediaStream.getVideoTracks()[0];
|
||||
if (!videoTrack) {
|
||||
@@ -575,6 +710,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isCountdownRunActive(countdownRunToken)) {
|
||||
teardownMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
width = DEFAULT_WIDTH,
|
||||
height = DEFAULT_HEIGHT,
|
||||
@@ -594,6 +734,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
);
|
||||
|
||||
const hasAudio = stream.current.getAudioTracks().length > 0;
|
||||
if (!isCountdownRunActive(countdownRunToken)) {
|
||||
teardownMedia();
|
||||
return;
|
||||
}
|
||||
|
||||
screenRecorder.current = createRecorderHandle(stream.current, {
|
||||
mimeType,
|
||||
videoBitsPerSecond,
|
||||
@@ -705,7 +850,17 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
};
|
||||
|
||||
const toggleRecording = () => {
|
||||
recording ? stopRecording.current() : startRecording();
|
||||
if (recording) {
|
||||
stopRecording.current();
|
||||
return;
|
||||
}
|
||||
|
||||
if (countdownActive) {
|
||||
cancelCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
void startRecordCountdown();
|
||||
};
|
||||
|
||||
const restartRecording = async () => {
|
||||
@@ -769,13 +924,22 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
const cancelRecording = () => {
|
||||
const activeScreenRecorder = screenRecorder.current;
|
||||
if (!activeScreenRecorder || activeScreenRecorder.recorder.state !== "recording") return;
|
||||
if (
|
||||
activeScreenRecorder?.recorder.state === "recording" ||
|
||||
activeScreenRecorder?.recorder.state === "paused"
|
||||
) {
|
||||
const activeRecordingId = recordingId.current;
|
||||
discardRecordingId.current = activeRecordingId;
|
||||
allowAutoFinalize.current = false;
|
||||
|
||||
const activeRecordingId = recordingId.current;
|
||||
discardRecordingId.current = activeRecordingId;
|
||||
allowAutoFinalize.current = false;
|
||||
stopRecording.current();
|
||||
return;
|
||||
}
|
||||
|
||||
stopRecording.current();
|
||||
if (countdownActive) {
|
||||
cancelCountdown();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<I18nProvider>
|
||||
|
||||
Reference in New Issue
Block a user