update usescreenrecorder

This commit is contained in:
Siddharth
2025-12-01 22:16:38 -07:00
parent 977be1e3b1
commit 899e55d257
4 changed files with 107 additions and 36 deletions
+7 -7
View File
@@ -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");
+1
View File
@@ -104,6 +104,7 @@ export function registerIpcHandlers(
}
})
ipcMain.handle('open-external-url', async (_, url: string) => {
try {
await shell.openExternal(url)
+33 -6
View File
@@ -112,6 +112,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
const layoutVideoContentRef = useRef<(() => void) | null>(null);
const trimRegionsRef = useRef<TrimRegion[]>([]);
const motionBlurEnabledRef = useRef(motionBlurEnabled);
const videoReadyRafRef = useRef<number | null>(null);
const clampFocusToStage = useCallback((focus: ZoomFocus, depth: ZoomDepth) => {
return clampFocusToStageUtil(focus, depth, stageSizeRef.current);
@@ -492,6 +493,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
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<VideoPlaybackRef, VideoPlaybackProps>(({
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<string | null>(null);
@@ -756,6 +774,15 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
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 || ''})` }
+66 -23
View File
@@ -13,6 +13,38 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
const chunks = useRef<Blob[]>([]);
const startTime = useRef<number>(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);