From 899e55d257c5ff546042714dfa49c481b48d9593 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Mon, 1 Dec 2025 22:16:38 -0700 Subject: [PATCH] update usescreenrecorder --- dist-electron/main.js | 14 +-- electron/ipc/handlers.ts | 1 + src/components/video-editor/VideoPlayback.tsx | 39 ++++++-- src/hooks/useScreenRecorder.ts | 89 ++++++++++++++----- 4 files changed, 107 insertions(+), 36 deletions(-) diff --git a/dist-electron/main.js b/dist-electron/main.js index 84b48e9..4a24c2d 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -2,8 +2,8 @@ import { ipcMain, screen, BrowserWindow, desktopCapturer, shell, app, dialog, na import { fileURLToPath } from "node:url"; import path from "node:path"; import fs from "node:fs/promises"; -const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); -const APP_ROOT = path.join(__dirname$1, ".."); +const __dirname$2 = path.dirname(fileURLToPath(import.meta.url)); +const APP_ROOT = path.join(__dirname$2, ".."); const VITE_DEV_SERVER_URL$1 = process.env["VITE_DEV_SERVER_URL"]; const RENDERER_DIST$1 = path.join(APP_ROOT, "dist"); let hudOverlayWindow = null; @@ -35,7 +35,7 @@ function createHudOverlayWindow() { skipTaskbar: true, hasShadow: false, webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), + preload: path.join(__dirname$2, "preload.mjs"), nodeIntegration: false, contextIsolation: true, backgroundThrottling: false @@ -74,7 +74,7 @@ function createEditorWindow() { title: "OpenScreen", backgroundColor: "#000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), + preload: path.join(__dirname$2, "preload.mjs"), nodeIntegration: false, contextIsolation: true, webSecurity: false, @@ -109,7 +109,7 @@ function createSourceSelectorWindow() { transparent: true, backgroundColor: "#00000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), + preload: path.join(__dirname$2, "preload.mjs"), nodeIntegration: false, contextIsolation: true } @@ -293,7 +293,7 @@ function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, g return { success: true }; }); } -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); async function ensureRecordingsDir() { try { @@ -304,7 +304,7 @@ async function ensureRecordingsDir() { console.error("Failed to create recordings directory:", error); } } -process.env.APP_ROOT = path.join(__dirname, ".."); +process.env.APP_ROOT = path.join(__dirname$1, ".."); const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]; const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 0cf0b66..f83d3a8 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -104,6 +104,7 @@ export function registerIpcHandlers( } }) + ipcMain.handle('open-external-url', async (_, url: string) => { try { await shell.openExternal(url) diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 8978641..357adc9 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -112,6 +112,7 @@ const VideoPlayback = forwardRef(({ const layoutVideoContentRef = useRef<(() => void) | null>(null); const trimRegionsRef = useRef([]); const motionBlurEnabledRef = useRef(motionBlurEnabled); + const videoReadyRafRef = useRef(null); const clampFocusToStage = useCallback((focus: ZoomFocus, depth: ZoomDepth) => { return clampFocusToStageUtil(focus, depth, stageSizeRef.current); @@ -492,6 +493,12 @@ const VideoPlayback = forwardRef(({ video.pause(); video.currentTime = 0; allowPlaybackRef.current = false; + lockedVideoDimensionsRef.current = null; + setVideoReady(false); + if (videoReadyRafRef.current) { + cancelAnimationFrame(videoReadyRafRef.current); + videoReadyRafRef.current = null; + } }, [videoPath]); @@ -703,13 +710,24 @@ const VideoPlayback = forwardRef(({ video.pause(); allowPlaybackRef.current = false; currentTimeRef.current = 0; - - // hacky fix: To ensure video is fully ready for PixiJS - requestAnimationFrame(() => { - requestAnimationFrame(() => { + + if (videoReadyRafRef.current) { + cancelAnimationFrame(videoReadyRafRef.current); + videoReadyRafRef.current = null; + } + + const waitForRenderableFrame = () => { + const hasDimensions = video.videoWidth > 0 && video.videoHeight > 0; + const hasData = video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA; + if (hasDimensions && hasData) { + videoReadyRafRef.current = null; setVideoReady(true); - }); - }); + return; + } + videoReadyRafRef.current = requestAnimationFrame(waitForRenderableFrame); + }; + + videoReadyRafRef.current = requestAnimationFrame(waitForRenderableFrame); }; const [resolvedWallpaper, setResolvedWallpaper] = useState(null); @@ -756,6 +774,15 @@ const VideoPlayback = forwardRef(({ return () => { mounted = false } }, [wallpaper]) + useEffect(() => { + return () => { + if (videoReadyRafRef.current) { + cancelAnimationFrame(videoReadyRafRef.current); + videoReadyRafRef.current = null; + } + }; + }, []) + const isImageUrl = Boolean(resolvedWallpaper && (resolvedWallpaper.startsWith('file://') || resolvedWallpaper.startsWith('http') || resolvedWallpaper.startsWith('/') || resolvedWallpaper.startsWith('data:'))) const backgroundStyle = isImageUrl ? { backgroundImage: `url(${resolvedWallpaper || ''})` } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index f8477d8..cfb2183 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -13,6 +13,38 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const chunks = useRef([]); const startTime = useRef(0); + // Target visually lossless 4K @ 60fps; fall back gracefully when hardware cannot keep up + const TARGET_FRAME_RATE = 60; + const TARGET_WIDTH = 3840; + const TARGET_HEIGHT = 2160; + const FOUR_K_PIXELS = TARGET_WIDTH * TARGET_HEIGHT; + const selectMimeType = () => { + const preferred = [ + "video/webm;codecs=av1", + "video/webm;codecs=h264", + "video/webm;codecs=vp9", + "video/webm;codecs=vp8", + "video/webm" + ]; + + return preferred.find(type => MediaRecorder.isTypeSupported(type)) ?? "video/webm"; + }; + + const computeBitrate = (width: number, height: number) => { + const pixels = width * height; + const highFrameRateBoost = TARGET_FRAME_RATE >= 60 ? 1.7 : 1; + + if (pixels >= FOUR_K_PIXELS) { + return Math.round(45_000_000 * highFrameRateBoost); + } + + if (pixels >= 2560 * 1440) { + return Math.round(28_000_000 * highFrameRateBoost); + } + + return Math.round(18_000_000 * highFrameRateBoost); + }; + const stopRecording = useRef(() => { if (mediaRecorder.current?.state === "recording") { if (stream.current) { @@ -55,14 +87,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } - // Capture screen at source resolution without constraints const mediaStream = await (navigator.mediaDevices as any).getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: selectedSource.id, - frameRate: { ideal: 60, max: 60 } + maxWidth: TARGET_WIDTH, + maxHeight: TARGET_HEIGHT, + maxFrameRate: TARGET_FRAME_RATE, + minFrameRate: 30, }, }, }); @@ -71,31 +105,36 @@ export function useScreenRecorder(): UseScreenRecorderReturn { throw new Error("Media stream is not available."); } const videoTrack = stream.current.getVideoTracks()[0]; - let { width = 1920, height = 1080 } = videoTrack.getSettings(); + try { + await videoTrack.applyConstraints({ + frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, + width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH }, + height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT }, + }); + } catch (error) { + console.warn("Unable to lock 4K/60fps constraints, using best available track settings.", error); + } + + let { width = 1920, height = 1080, frameRate = TARGET_FRAME_RATE } = videoTrack.getSettings(); // Ensure dimensions are divisible by 2 for VP9/AV1 codec compatibility width = Math.floor(width / 2) * 2; height = Math.floor(height / 2) * 2; - console.log(`Recording at ${width}x${height}`); + const videoBitsPerSecond = computeBitrate(width, height); + const mimeType = selectMimeType(); + + console.log( + `Recording at ${width}x${height} @ ${frameRate ?? TARGET_FRAME_RATE}fps using ${mimeType} / ${Math.round( + videoBitsPerSecond / 1_000_000 + )} Mbps` + ); - const totalPixels = width * height; - // Use visually lossless bitrates optimized for quality and file size balance - let bitrate = 30_000_000; - if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) { - bitrate = 50_000_000; - } else if (totalPixels > 2560 * 1440) { - bitrate = 80_000_000; - } chunks.current = []; - // Prefer AV1 codec for better compression, fallback to VP9 then VP8 - const supportedCodecs = [ - 'video/webm;codecs=av1', - 'video/webm;codecs=vp9', - 'video/webm;codecs=vp8' - ]; - const mimeType = supportedCodecs.find(codec => MediaRecorder.isTypeSupported(codec)) || 'video/webm;codecs=vp9'; - const recorder = new MediaRecorder(stream.current, { mimeType, videoBitsPerSecond: bitrate }); + const recorder = new MediaRecorder(stream.current, { + mimeType, + videoBitsPerSecond, + }); mediaRecorder.current = recorder; recorder.ondataavailable = e => { if (e.data && e.data.size > 0) chunks.current.push(e.data); @@ -104,7 +143,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { stream.current = null; if (chunks.current.length === 0) return; const duration = Date.now() - startTime.current; - const buggyBlob = new Blob(chunks.current, { type: mimeType }); + const recordedChunks = chunks.current; + const buggyBlob = new Blob(recordedChunks, { type: mimeType }); // Clear chunks early to free memory immediately after blob creation chunks.current = []; const timestamp = Date.now(); @@ -119,14 +159,17 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } + if (videoResult.path) { + await window.electronAPI.setCurrentVideoPath(videoResult.path); + } + await window.electronAPI.switchToEditor(); } catch (error) { console.error('Error saving recording:', error); } }; recorder.onerror = () => setRecording(false); - // Use larger timeslice to reduce recording overhead and improve smoothness - recorder.start(5000); + recorder.start(1000); startTime.current = Date.now(); setRecording(true); window.electronAPI?.setRecordingState(true);