From 55a373c7ef108aab53b5e06ed1f7d7dc4f29686f Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 22 Nov 2025 22:28:58 -0700 Subject: [PATCH 1/4] recording optimizations --- .github/workflows/build.yml | 43 --------------------- src/components/video-editor/VideoEditor.tsx | 11 +++++- src/hooks/useScreenRecorder.ts | 26 ++++++++++--- src/lib/exporter/videoExporter.ts | 18 ++++++++- 4 files changed, 48 insertions(+), 50 deletions(-) delete mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index a3b5cee..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Build Electron App - -on: - push: - branches: [ main, master ] - workflow_dispatch: - -jobs: - - - build-macos: - runs-on: macos-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '22' - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: npm ci - - - name: Install app dependencies - run: npx electron-builder install-app-deps - - - name: Build macOS app - run: npm run build:mac - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload macOS build - uses: actions/upload-artifact@v4 - with: - name: macos-installer - path: release/**/*.dmg - retention-days: 30 \ No newline at end of file diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 8b75b20..485fa6c 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -187,12 +187,21 @@ export default function VideoEditor() { const width = 1920; const height = 1080; + // Calculate visually lossless bitrate matching screen recording optimization + const totalPixels = width * height; + 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; + } + const exporter = new VideoExporter({ videoUrl: videoPath, width, height, frameRate: 60, - bitrate: 15_000_000, + bitrate, codec: 'avc1.640033', wallpaper, zoomRegions, diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 636f31d..23c141d 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -55,12 +55,18 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } await window.electronAPI.startMouseTracking(); + // Enable hardware acceleration and set optimal resolution/framerate constraints const mediaStream = await (navigator.mediaDevices as any).getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: selectedSource.id, + minWidth: 1920, + minHeight: 1080, + maxWidth: 3840, + maxHeight: 2160, + frameRate: { ideal: 60, max: 60 } }, }, }); @@ -71,14 +77,21 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const videoTrack = stream.current.getVideoTracks()[0]; const { width = 1920, height = 1080 } = videoTrack.getSettings(); const totalPixels = width * height; - let bitrate = 150_000_000; + // Use visually lossless bitrates optimized for quality and file size balance + let bitrate = 30_000_000; if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) { - bitrate = 250_000_000; + bitrate = 50_000_000; } else if (totalPixels > 2560 * 1440) { - bitrate = 400_000_000; + bitrate = 80_000_000; } chunks.current = []; - const mimeType = "video/webm;codecs=vp9"; + // 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 }); mediaRecorder.current = recorder; recorder.ondataavailable = e => { @@ -89,6 +102,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { if (chunks.current.length === 0) return; const duration = Date.now() - startTime.current; const buggyBlob = new Blob(chunks.current, { type: mimeType }); + // Clear chunks early to free memory immediately after blob creation + chunks.current = []; const timestamp = Date.now(); const videoFileName = `recording-${timestamp}.webm`; const trackingFileName = `recording-${timestamp}_tracking.json`; @@ -110,7 +125,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } }; recorder.onerror = () => setRecording(false); - recorder.start(1000); + // Use larger timeslice to reduce recording overhead and improve smoothness + recorder.start(5000); startTime.current = Date.now(); setRecording(true); window.electronAPI?.setRecordingState(true); diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index e299b69..c7f06ae 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -25,6 +25,7 @@ export class VideoExporter { private encodeQueue = 0; private readonly MAX_ENCODE_QUEUE = 60; private videoDescription: Uint8Array | undefined; + private videoColorSpace: VideoColorSpaceInit | undefined; constructor(config: VideoExporterConfig) { this.config = config; @@ -164,13 +165,22 @@ export class VideoExporter { const chunk = this.encodedChunks[i]; const meta: EncodedVideoChunkMetadata = {}; - // Add decoder config for the first chunk + // Add decoder config with colorSpace metadata for the first chunk if (i === 0 && this.videoDescription) { + // Use captured colorSpace from encoder or fallback to default sRGB colorspace + const colorSpace = this.videoColorSpace || { + primaries: 'bt709', + transfer: 'iec61966-2-1', + matrix: 'rgb', + fullRange: true, + }; + meta.decoderConfig = { codec: this.config.codec || 'avc1.640033', codedWidth: this.config.width, codedHeight: this.config.height, description: this.videoDescription, + colorSpace, }; } @@ -199,11 +209,16 @@ export class VideoExporter { this.encoder = new VideoEncoder({ output: (chunk, meta) => { + // Capture decoder config metadata from encoder output if (meta?.decoderConfig?.description && !videoDescription) { const desc = meta.decoderConfig.description; videoDescription = new Uint8Array(desc instanceof ArrayBuffer ? desc : (desc as any)); this.videoDescription = videoDescription; } + // Capture colorSpace from encoder metadata if provided + if (meta?.decoderConfig?.colorSpace && !this.videoColorSpace) { + this.videoColorSpace = meta.decoderConfig.colorSpace; + } this.encodedChunks.push(chunk); this.encodeQueue--; }, @@ -265,5 +280,6 @@ export class VideoExporter { this.encodedChunks = []; this.encodeQueue = 0; this.videoDescription = undefined; + this.videoColorSpace = undefined; } } From 0e0f5003ff52a7057c67c0535736b329018b72b6 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 22 Nov 2025 23:40:39 -0700 Subject: [PATCH 2/4] fix screen recording, optimize exporting pipeline --- src/components/video-editor/VideoEditor.tsx | 11 ++++- src/hooks/useScreenRecorder.ts | 15 ++++--- src/lib/exporter/frameRenderer.ts | 18 ++++---- src/lib/exporter/videoDecoder.ts | 29 ------------ src/lib/exporter/videoExporter.ts | 50 +++++++++------------ 5 files changed, 48 insertions(+), 75 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 485fa6c..25b7571 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -184,8 +184,15 @@ export default function VideoEditor() { videoPlaybackRef.current?.pause(); } - const width = 1920; - const height = 1080; + // Get actual video dimensions to match recording resolution + const video = videoPlaybackRef.current?.video; + if (!video) { + toast.error('Video not ready'); + return; + } + + const width = video.videoWidth || 1920; + const height = video.videoHeight || 1080; // Calculate visually lossless bitrate matching screen recording optimization const totalPixels = width * height; diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 23c141d..f38a804 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -55,17 +55,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } await window.electronAPI.startMouseTracking(); - // Enable hardware acceleration and set optimal resolution/framerate constraints + // Capture screen at source resolution without constraints const mediaStream = await (navigator.mediaDevices as any).getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: selectedSource.id, - minWidth: 1920, - minHeight: 1080, - maxWidth: 3840, - maxHeight: 2160, frameRate: { ideal: 60, max: 60 } }, }, @@ -75,7 +71,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn { throw new Error("Media stream is not available."); } const videoTrack = stream.current.getVideoTracks()[0]; - const { width = 1920, height = 1080 } = videoTrack.getSettings(); + let { width = 1920, height = 1080 } = 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 totalPixels = width * height; // Use visually lossless bitrates optimized for quality and file size balance let bitrate = 30_000_000; diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 08b5ea0..20035e4 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -69,15 +69,15 @@ export class FrameRenderer { console.warn('[FrameRenderer] colorSpace not supported on this platform:', error); } - // Initialize PixiJS app with transparent background (background rendered separately) + // Initialize PixiJS with optimized settings for export performance this.app = new PIXI.Application(); await this.app.init({ canvas, width: this.config.width, height: this.config.height, backgroundAlpha: 0, - antialias: true, - resolution: 2, + antialias: false, + resolution: 1, autoDensity: true, }); @@ -249,15 +249,17 @@ export class FrameRenderer { this.currentVideoTime = timestamp / 1000000; - // Create or update video sprite from VideoFrame + // Create or update video sprite from VideoFrame (optimized to reuse sprite) if (!this.videoSprite) { const texture = PIXI.Texture.from(videoFrame as any); this.videoSprite = new PIXI.Sprite(texture); this.videoContainer.addChild(this.videoSprite); } else { - // Update texture with new frame - const texture = PIXI.Texture.from(videoFrame as any); - this.videoSprite.texture = texture; + // Destroy old texture to avoid memory leaks, then create new one + const oldTexture = this.videoSprite.texture; + const newTexture = PIXI.Texture.from(videoFrame as any); + this.videoSprite.texture = newTexture; + oldTexture.destroy(true); } // Apply layout @@ -442,7 +444,7 @@ export class FrameRenderer { console.warn('[FrameRenderer] No background sprite found during compositing!'); } - // Step 2: Draw video layer with shadows on top of background + // Draw video layer with shadows on top of background (using CSS filter for accuracy) if (this.config.showShadow && this.shadowCanvas && this.shadowCtx) { const shadowCtx = this.shadowCtx; shadowCtx.clearRect(0, 0, w, h); diff --git a/src/lib/exporter/videoDecoder.ts b/src/lib/exporter/videoDecoder.ts index 4f3c590..34ece77 100644 --- a/src/lib/exporter/videoDecoder.ts +++ b/src/lib/exporter/videoDecoder.ts @@ -7,7 +7,6 @@ export interface DecodedVideoInfo { } export class VideoFileDecoder { - private decoder: VideoDecoder | null = null; private info: DecodedVideoInfo | null = null; private videoElement: HTMLVideoElement | null = null; @@ -44,27 +43,6 @@ export class VideoFileDecoder { return this.videoElement; } - /** - * Seek to a specific time and wait for the frame to be ready - */ - async seekToTime(timeInSeconds: number): Promise { - if (!this.videoElement) { - throw new Error('Video not loaded'); - } - - return new Promise((resolve) => { - const video = this.videoElement!; - - const onSeeked = () => { - video.removeEventListener('seeked', onSeeked); - resolve(); - }; - - video.addEventListener('seeked', onSeeked); - video.currentTime = timeInSeconds; - }); - } - getInfo(): DecodedVideoInfo | null { return this.info; } @@ -75,12 +53,5 @@ export class VideoFileDecoder { this.videoElement.src = ''; this.videoElement = null; } - - if (this.decoder) { - if (this.decoder.state !== 'closed') { - this.decoder.close(); - } - this.decoder = null; - } } } diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index c7f06ae..769e918 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -23,7 +23,8 @@ export class VideoExporter { private cancelled = false; private encodedChunks: EncodedVideoChunk[] = []; private encodeQueue = 0; - private readonly MAX_ENCODE_QUEUE = 60; + // Increased queue size for better throughput with hardware encoding + private readonly MAX_ENCODE_QUEUE = 120; private videoDescription: Uint8Array | undefined; private videoColorSpace: VideoColorSpaceInit | undefined; @@ -68,38 +69,25 @@ export class VideoExporter { throw new Error('Video element not available'); } - // Process frames with optimized seeking + // Process frames with optimized seeking (no unnecessary timeouts) const frameDuration = 1_000_000 / this.config.frameRate; // in microseconds let frameIndex = 0; const timeStep = 1 / this.config.frameRate; - // Pre-load first frame - videoElement.currentTime = 0; - await new Promise(resolve => { - const onSeeked = () => { - videoElement.removeEventListener('seeked', onSeeked); - resolve(null); - }; - videoElement.addEventListener('seeked', onSeeked); - }); - while (frameIndex < totalFrames && !this.cancelled) { const timestamp = frameIndex * frameDuration; const videoTime = frameIndex * timeStep; - // Seek to frame (only seek if not already there) - if (Math.abs(videoElement.currentTime - videoTime) > 0.001) { - videoElement.currentTime = videoTime; - await Promise.race([ - new Promise(resolve => { - const onSeeked = () => { - videoElement.removeEventListener('seeked', onSeeked); - // Wait for video to render the frame - videoElement.requestVideoFrameCallback(() => resolve(null)); - }; - videoElement.addEventListener('seeked', onSeeked, { once: true }); - }), - new Promise(resolve => setTimeout(resolve, 200)) // higher this number, slower the export, but better capture/ no frame drops - ]); + + // Seek if needed or wait for first frame to be ready + const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001; + if (needsSeek || frameIndex === 0) { + if (needsSeek) { + videoElement.currentTime = videoTime; + } + // Wait for video frame to be ready + await new Promise(resolve => { + videoElement.requestVideoFrameCallback(() => resolve()); + }); } // Create a VideoFrame from the video element (on GPU!) @@ -112,16 +100,17 @@ export class VideoExporter { videoFrame.close(); + // Wait for encoder queue to have space (yield immediately instead of 1ms timeout) while (this.encodeQueue >= this.MAX_ENCODE_QUEUE && !this.cancelled) { - await new Promise(resolve => setTimeout(resolve, 1)); + await new Promise(resolve => setTimeout(resolve, 0)); } if (this.cancelled) break; const canvas = this.renderer!.getCanvas(); - - // @ts-ignore - TypeScript definitions may not include all VideoFrameInit properties + // Create VideoFrame from canvas on GPU without reading pixels + // @ts-ignore - colorSpace not in TypeScript definitions but works at runtime const exportFrame = new VideoFrame(canvas, { timestamp, duration: frameDuration, @@ -141,7 +130,8 @@ export class VideoExporter { frameIndex++; - if (this.config.onProgress) { + // Batch progress updates to reduce callback overhead (every 5 frames) + if (frameIndex % 5 === 0 && this.config.onProgress) { this.config.onProgress({ currentFrame: frameIndex, totalFrames, From 9fedc5c16751ea9bc742f03f152aa1cfe16b8aa3 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sun, 23 Nov 2025 00:33:10 -0700 Subject: [PATCH 3/4] 3x faster exports --- src/lib/exporter/frameRenderer.ts | 6 +- src/lib/exporter/videoExporter.ts | 102 ++++++++++++++++-------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 20035e4..0f947b4 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -53,7 +53,7 @@ export class FrameRenderer { } async initialize(): Promise { - // Create offscreen canvas with sRGB color space for fidelity + // Create canvas for rendering const canvas = document.createElement('canvas'); canvas.width = this.config.width; canvas.height = this.config.height; @@ -249,7 +249,7 @@ export class FrameRenderer { this.currentVideoTime = timestamp / 1000000; - // Create or update video sprite from VideoFrame (optimized to reuse sprite) + // Create or update video sprite from VideoFrame if (!this.videoSprite) { const texture = PIXI.Texture.from(videoFrame as any); this.videoSprite = new PIXI.Sprite(texture); @@ -444,7 +444,7 @@ export class FrameRenderer { console.warn('[FrameRenderer] No background sprite found during compositing!'); } - // Draw video layer with shadows on top of background (using CSS filter for accuracy) + // Draw video layer with shadows on top of background if (this.config.showShadow && this.shadowCanvas && this.shadowCtx) { const shadowCtx = this.shadowCtx; shadowCtx.clearRect(0, 0, w, h); diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 769e918..b7c630a 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -69,69 +69,73 @@ export class VideoExporter { throw new Error('Video element not available'); } - // Process frames with optimized seeking (no unnecessary timeouts) + // Process frames with optimized seeking const frameDuration = 1_000_000 / this.config.frameRate; // in microseconds let frameIndex = 0; const timeStep = 1 / this.config.frameRate; + const BATCH_SIZE = 5; // Process frames in batches for better throughput while (frameIndex < totalFrames && !this.cancelled) { - const timestamp = frameIndex * frameDuration; - const videoTime = frameIndex * timeStep; + // Process a batch of frames + const batchEnd = Math.min(frameIndex + BATCH_SIZE, totalFrames); - // Seek if needed or wait for first frame to be ready - const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001; - if (needsSeek || frameIndex === 0) { - if (needsSeek) { - videoElement.currentTime = videoTime; + for (let i = frameIndex; i < batchEnd && !this.cancelled; i++) { + const timestamp = i * frameDuration; + const videoTime = i * timeStep; + + // Seek if needed or wait for first frame to be ready + const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001; + if (needsSeek || i === 0) { + if (needsSeek) { + videoElement.currentTime = videoTime; + } + // Wait for video frame to be ready + await new Promise(resolve => { + videoElement.requestVideoFrameCallback(() => resolve()); + }); } - // Wait for video frame to be ready - await new Promise(resolve => { - videoElement.requestVideoFrameCallback(() => resolve()); + + // Create a VideoFrame from the video element (on GPU!) + const videoFrame = new VideoFrame(videoElement, { + timestamp, }); + + // Render the frame with all effects + await this.renderer!.renderFrame(videoFrame, timestamp); + + videoFrame.close(); + + const canvas = this.renderer!.getCanvas(); + + // Create VideoFrame from canvas on GPU without reading pixels + // @ts-ignore - colorSpace not in TypeScript definitions but works at runtime + const exportFrame = new VideoFrame(canvas, { + timestamp, + duration: frameDuration, + colorSpace: { + primaries: 'bt709', + transfer: 'iec61966-2-1', + matrix: 'rgb', + fullRange: true, + }, + }); + + if (this.encoder && this.encoder.state === 'configured') { + this.encodeQueue++; + this.encoder.encode(exportFrame, { keyFrame: i % 150 === 0 }); + } + exportFrame.close(); } - - // Create a VideoFrame from the video element (on GPU!) - const videoFrame = new VideoFrame(videoElement, { - timestamp, - }); - - // Render the frame with all effects - await this.renderer!.renderFrame(videoFrame, timestamp); - videoFrame.close(); - - // Wait for encoder queue to have space (yield immediately instead of 1ms timeout) + // Wait for encoder queue once per batch while (this.encodeQueue >= this.MAX_ENCODE_QUEUE && !this.cancelled) { await new Promise(resolve => setTimeout(resolve, 0)); } - if (this.cancelled) break; + frameIndex = batchEnd; - const canvas = this.renderer!.getCanvas(); - - // Create VideoFrame from canvas on GPU without reading pixels - // @ts-ignore - colorSpace not in TypeScript definitions but works at runtime - const exportFrame = new VideoFrame(canvas, { - timestamp, - duration: frameDuration, - colorSpace: { - primaries: 'bt709', - transfer: 'iec61966-2-1', - matrix: 'rgb', - fullRange: true, - }, - }); - - if (this.encoder && this.encoder.state === 'configured') { - this.encodeQueue++; - this.encoder.encode(exportFrame, { keyFrame: frameIndex % 150 === 0 }); - } - exportFrame.close(); - - frameIndex++; - - // Batch progress updates to reduce callback overhead (every 5 frames) - if (frameIndex % 5 === 0 && this.config.onProgress) { + // Batch progress updates to reduce callback overhead + if (this.config.onProgress) { this.config.onProgress({ currentFrame: frameIndex, totalFrames, @@ -155,7 +159,7 @@ export class VideoExporter { const chunk = this.encodedChunks[i]; const meta: EncodedVideoChunkMetadata = {}; - // Add decoder config with colorSpace metadata for the first chunk + // Add decoder config for the first chunk if (i === 0 && this.videoDescription) { // Use captured colorSpace from encoder or fallback to default sRGB colorspace const colorSpace = this.videoColorSpace || { From 325c239a3a405cf870e40e4fb7223b0a914931e6 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sun, 23 Nov 2025 00:48:16 -0700 Subject: [PATCH 4/4] ... --- dist-electron/main.js | 586 +++++++++++++++----------------------- dist-electron/preload.mjs | 52 +--- 2 files changed, 224 insertions(+), 414 deletions(-) diff --git a/dist-electron/main.js b/dist-electron/main.js index 22982c9..c9c27ca 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,483 +1,343 @@ -import { BrowserWindow, screen, ipcMain, desktopCapturer, shell, app, nativeImage, Tray, Menu } from "electron"; -import { fileURLToPath } from "node:url"; -import path from "node:path"; -import fs from "node:fs/promises"; -import { uIOhook } from "uiohook-napi"; -const __dirname$1 = path.dirname(fileURLToPath(import.meta.url)); -const APP_ROOT = path.join(__dirname$1, ".."); -const VITE_DEV_SERVER_URL$1 = process.env["VITE_DEV_SERVER_URL"]; -const RENDERER_DIST$1 = path.join(APP_ROOT, "dist"); -function createHudOverlayWindow() { - const win = new BrowserWindow({ +import { BrowserWindow as P, screen as O, ipcMain as c, desktopCapturer as M, shell as W, app as p, nativeImage as L, Tray as V, Menu as U } from "electron"; +import { fileURLToPath as E } from "node:url"; +import t from "node:path"; +import m from "node:fs/promises"; +import { uIOhook as w } from "uiohook-napi"; +const S = t.dirname(E(import.meta.url)), A = t.join(S, ".."), y = process.env.VITE_DEV_SERVER_URL, x = t.join(A, "dist"); +function C() { + const e = new P({ width: 250, height: 80, minWidth: 250, maxWidth: 250, minHeight: 80, maxHeight: 80, - frame: false, - transparent: true, - resizable: false, - alwaysOnTop: true, - skipTaskbar: true, - hasShadow: false, + frame: !1, + transparent: !0, + resizable: !1, + alwaysOnTop: !0, + skipTaskbar: !0, + hasShadow: !1, webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true, - backgroundThrottling: false + preload: t.join(S, "preload.mjs"), + nodeIntegration: !1, + contextIsolation: !0, + backgroundThrottling: !1 } }); - win.webContents.on("did-finish-load", () => { - win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }); - if (VITE_DEV_SERVER_URL$1) { - win.loadURL(VITE_DEV_SERVER_URL$1 + "?windowType=hud-overlay"); - } else { - win.loadFile(path.join(RENDERER_DIST$1, "index.html"), { - query: { windowType: "hud-overlay" } - }); - } - return win; + return e.webContents.on("did-finish-load", () => { + e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); + }), y ? e.loadURL(y + "?windowType=hud-overlay") : e.loadFile(t.join(x, "index.html"), { + query: { windowType: "hud-overlay" } + }), e; } -function createEditorWindow() { - const win = new BrowserWindow({ +function N() { + const e = new P({ width: 1200, height: 800, minWidth: 800, minHeight: 600, titleBarStyle: "hiddenInset", trafficLightPosition: { x: 12, y: 12 }, - transparent: false, - resizable: true, - alwaysOnTop: false, - skipTaskbar: false, + transparent: !1, + resizable: !0, + alwaysOnTop: !1, + skipTaskbar: !1, title: "OpenScreen", backgroundColor: "#000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true, - webSecurity: false + preload: t.join(S, "preload.mjs"), + nodeIntegration: !1, + contextIsolation: !0, + webSecurity: !1 } }); - win.maximize(); - win.webContents.on("did-finish-load", () => { - win == null ? void 0 : win.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }); - if (VITE_DEV_SERVER_URL$1) { - win.loadURL(VITE_DEV_SERVER_URL$1 + "?windowType=editor"); - } else { - win.loadFile(path.join(RENDERER_DIST$1, "index.html"), { - query: { windowType: "editor" } - }); - } - return win; + return e.maximize(), e.webContents.on("did-finish-load", () => { + e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); + }), y ? e.loadURL(y + "?windowType=editor") : e.loadFile(t.join(x, "index.html"), { + query: { windowType: "editor" } + }), e; } -function createSourceSelectorWindow() { - const { width, height } = screen.getPrimaryDisplay().workAreaSize; - const win = new BrowserWindow({ +function H() { + const { width: e, height: n } = O.getPrimaryDisplay().workAreaSize, i = new P({ width: 620, height: 420, minHeight: 350, maxHeight: 500, - x: Math.round((width - 620) / 2), - y: Math.round((height - 420) / 2), - frame: false, - resizable: false, - alwaysOnTop: true, - transparent: true, + x: Math.round((e - 620) / 2), + y: Math.round((n - 420) / 2), + frame: !1, + resizable: !1, + alwaysOnTop: !0, + transparent: !0, backgroundColor: "#00000000", webPreferences: { - preload: path.join(__dirname$1, "preload.mjs"), - nodeIntegration: false, - contextIsolation: true + preload: t.join(S, "preload.mjs"), + nodeIntegration: !1, + contextIsolation: !0 } }); - if (VITE_DEV_SERVER_URL$1) { - win.loadURL(VITE_DEV_SERVER_URL$1 + "?windowType=source-selector"); - } else { - win.loadFile(path.join(RENDERER_DIST$1, "index.html"), { - query: { windowType: "source-selector" } - }); - } - return win; + return y ? i.loadURL(y + "?windowType=source-selector") : i.loadFile(t.join(x, "index.html"), { + query: { windowType: "source-selector" } + }), i; } -let isMouseTrackingActive = false; -let isHookStarted = false; -let recordingStartTime = 0; -let mouseEventData = []; -function startMouseTracking() { - if (isMouseTrackingActive) { - return { success: false, message: "Already tracking" }; - } - isMouseTrackingActive = true; - recordingStartTime = performance.now(); - mouseEventData = []; - if (!isHookStarted) { - setupMouseEventListeners(); - try { - uIOhook.start(); - isHookStarted = true; - return { success: true, message: "Mouse tracking started", startTime: recordingStartTime }; - } catch (error) { - console.error("Failed to start mouse tracking:", error); - isMouseTrackingActive = false; - return { success: false, message: "Failed to start hook", error }; - } - } else { - return { success: true, message: "Mouse tracking resumed", startTime: recordingStartTime }; +let u = !1, _ = !1, d = 0, f = []; +function z() { + if (u) + return { success: !1, message: "Already tracking" }; + if (u = !0, d = performance.now(), f = [], _) + return { success: !0, message: "Mouse tracking resumed", startTime: d }; + q(); + try { + return w.start(), _ = !0, { success: !0, message: "Mouse tracking started", startTime: d }; + } catch (e) { + return console.error("Failed to start mouse tracking:", e), u = !1, { success: !1, message: "Failed to start hook", error: e }; } } -function stopMouseTracking() { - if (!isMouseTrackingActive) { - return { success: false, message: "Not currently tracking" }; - } - isMouseTrackingActive = false; - const duration = performance.now() - recordingStartTime; - const session = { - startTime: recordingStartTime, - events: mouseEventData, - duration - }; +function B() { + if (!u) + return { success: !1, message: "Not currently tracking" }; + u = !1; + const e = performance.now() - d; return { - success: true, + success: !0, message: "Mouse tracking stopped", - data: session + data: { + startTime: d, + events: f, + duration: e + } }; } -function setupMouseEventListeners() { - uIOhook.on("mousemove", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { +function q() { + w.on("mousemove", (e) => { + if (u) { + const i = { type: "move", - timestamp, + timestamp: performance.now() - d, x: e.x, y: e.y }; - mouseEventData.push(event); + f.push(i); } - }); - uIOhook.on("mousedown", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { + }), w.on("mousedown", (e) => { + if (u) { + const i = { type: "down", - timestamp, + timestamp: performance.now() - d, x: e.x, y: e.y, button: e.button, clicks: e.clicks }; - mouseEventData.push(event); + f.push(i); } - }); - uIOhook.on("mouseup", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { + }), w.on("mouseup", (e) => { + if (u) { + const i = { type: "up", - timestamp, + timestamp: performance.now() - d, x: e.x, y: e.y, button: e.button }; - mouseEventData.push(event); + f.push(i); } - }); - uIOhook.on("click", (e) => { - if (isMouseTrackingActive) { - const timestamp = performance.now() - recordingStartTime; - const event = { + }), w.on("click", (e) => { + if (u) { + const i = { type: "click", - timestamp, + timestamp: performance.now() - d, x: e.x, y: e.y, button: e.button, clicks: e.clicks }; - mouseEventData.push(event); + f.push(i); } }); } -function getTrackingData() { - return [...mouseEventData]; +function $() { + return [...f]; } -function cleanupMouseTracking() { - if (isHookStarted) { +function G() { + if (_) try { - uIOhook.stop(); - isHookStarted = false; - isMouseTrackingActive = false; - mouseEventData = []; - } catch (error) { - console.error("Error cleaning up mouse tracking:", error); + w.stop(), _ = !1, u = !1, f = []; + } catch (e) { + console.error("Error cleaning up mouse tracking:", e); } - } } -let selectedSource = null; -function registerIpcHandlers(createEditorWindow2, createSourceSelectorWindow2, getMainWindow, getSourceSelectorWindow, onRecordingStateChange) { - ipcMain.handle("get-sources", async (_, opts) => { - const sources = await desktopCapturer.getSources(opts); - return sources.map((source) => ({ - id: source.id, - name: source.name, - display_id: source.display_id, - thumbnail: source.thumbnail ? source.thumbnail.toDataURL() : null, - appIcon: source.appIcon ? source.appIcon.toDataURL() : null - })); - }); - ipcMain.handle("select-source", (_, source) => { - selectedSource = source; - const sourceSelectorWin = getSourceSelectorWindow(); - if (sourceSelectorWin) { - sourceSelectorWin.close(); - } - return selectedSource; - }); - ipcMain.handle("get-selected-source", () => { - return selectedSource; - }); - ipcMain.handle("open-source-selector", () => { - const sourceSelectorWin = getSourceSelectorWindow(); - if (sourceSelectorWin) { - sourceSelectorWin.focus(); +let b = null; +function J(e, n, i, v, T) { + c.handle("get-sources", async (o, a) => (await M.getSources(a)).map((r) => ({ + id: r.id, + name: r.name, + display_id: r.display_id, + thumbnail: r.thumbnail ? r.thumbnail.toDataURL() : null, + appIcon: r.appIcon ? r.appIcon.toDataURL() : null + }))), c.handle("select-source", (o, a) => { + b = a; + const s = v(); + return s && s.close(), b; + }), c.handle("get-selected-source", () => b), c.handle("open-source-selector", () => { + const o = v(); + if (o) { + o.focus(); return; } - createSourceSelectorWindow2(); - }); - ipcMain.handle("switch-to-editor", () => { - const mainWin = getMainWindow(); - if (mainWin) { - mainWin.close(); - } - createEditorWindow2(); - }); - ipcMain.handle("start-mouse-tracking", () => { - return startMouseTracking(); - }); - ipcMain.handle("stop-mouse-tracking", () => { - return stopMouseTracking(); - }); - ipcMain.handle("store-recorded-video", async (_, videoData, fileName) => { + n(); + }), c.handle("switch-to-editor", () => { + const o = i(); + o && o.close(), e(); + }), c.handle("start-mouse-tracking", () => z()), c.handle("stop-mouse-tracking", () => B()), c.handle("store-recorded-video", async (o, a, s) => { try { - const videoPath = path.join(RECORDINGS_DIR, fileName); - await fs.writeFile(videoPath, Buffer.from(videoData)); - return { - success: true, - path: videoPath, + const r = t.join(h, s); + return await m.writeFile(r, Buffer.from(a)), { + success: !0, + path: r, message: "Video stored successfully" }; - } catch (error) { - console.error("Failed to store video:", error); - return { - success: false, + } catch (r) { + return console.error("Failed to store video:", r), { + success: !1, message: "Failed to store video", - error: String(error) + error: String(r) }; } - }); - ipcMain.handle("store-mouse-tracking-data", async (_, fileName) => { + }), c.handle("store-mouse-tracking-data", async (o, a) => { try { - const data = getTrackingData(); - if (data.length === 0) { - return { success: false, message: "No tracking data to save" }; - } - const trackingPath = path.join(RECORDINGS_DIR, fileName); - await fs.writeFile(trackingPath, JSON.stringify(data, null, 2), "utf-8"); - return { - success: true, - path: trackingPath, - eventCount: data.length, + const s = $(); + if (s.length === 0) + return { success: !1, message: "No tracking data to save" }; + const r = t.join(h, a); + return await m.writeFile(r, JSON.stringify(s, null, 2), "utf-8"), { + success: !0, + path: r, + eventCount: s.length, message: "Mouse tracking data stored successfully" }; - } catch (error) { - console.error("Failed to store mouse tracking data:", error); - return { - success: false, + } catch (s) { + return console.error("Failed to store mouse tracking data:", s), { + success: !1, message: "Failed to store mouse tracking data", - error: String(error) + error: String(s) }; } - }); - ipcMain.handle("get-recorded-video-path", async () => { + }), c.handle("get-recorded-video-path", async () => { try { - const files = await fs.readdir(RECORDINGS_DIR); - const videoFiles = files.filter((file) => file.endsWith(".webm")); - if (videoFiles.length === 0) { - return { success: false, message: "No recorded video found" }; - } - const latestVideo = videoFiles.sort().reverse()[0]; - const videoPath = path.join(RECORDINGS_DIR, latestVideo); - return { success: true, path: videoPath }; - } catch (error) { - console.error("Failed to get video path:", error); - return { success: false, message: "Failed to get video path", error: String(error) }; + const a = (await m.readdir(h)).filter((R) => R.endsWith(".webm")); + if (a.length === 0) + return { success: !1, message: "No recorded video found" }; + const s = a.sort().reverse()[0]; + return { success: !0, path: t.join(h, s) }; + } catch (o) { + return console.error("Failed to get video path:", o), { success: !1, message: "Failed to get video path", error: String(o) }; } - }); - ipcMain.handle("set-recording-state", (_, recording) => { - const source = selectedSource || { name: "Screen" }; - if (onRecordingStateChange) { - onRecordingStateChange(recording, source.name); - } - }); - ipcMain.handle("open-external-url", async (_, url) => { + }), c.handle("set-recording-state", (o, a) => { + T && T(a, (b || { name: "Screen" }).name); + }), c.handle("open-external-url", async (o, a) => { try { - await shell.openExternal(url); - return { success: true }; - } catch (error) { - console.error("Failed to open URL:", error); - return { success: false, error: String(error) }; + return await W.openExternal(a), { success: !0 }; + } catch (s) { + return console.error("Failed to open URL:", s), { success: !1, error: String(s) }; } - }); - ipcMain.handle("get-asset-base-path", () => { + }), c.handle("get-asset-base-path", () => { try { - if (app.isPackaged) { - return path.join(process.resourcesPath, "assets"); - } - return path.join(app.getAppPath(), "public", "assets"); - } catch (err) { - console.error("Failed to resolve asset base path:", err); - return null; + return p.isPackaged ? t.join(process.resourcesPath, "assets") : t.join(p.getAppPath(), "public", "assets"); + } catch (o) { + return console.error("Failed to resolve asset base path:", o), null; } - }); - ipcMain.handle("save-exported-video", async (_, videoData, fileName) => { + }), c.handle("save-exported-video", async (o, a, s) => { try { - const downloadsPath = app.getPath("downloads"); - const videoPath = path.join(downloadsPath, fileName); - await fs.writeFile(videoPath, Buffer.from(videoData)); - return { - success: true, - path: videoPath, + const r = p.getPath("downloads"), R = t.join(r, s); + return await m.writeFile(R, Buffer.from(a)), { + success: !0, + path: R, message: "Video exported successfully" }; - } catch (error) { - console.error("Failed to save exported video:", error); - return { - success: false, + } catch (r) { + return console.error("Failed to save exported video:", r), { + success: !1, message: "Failed to save exported video", - error: String(error) + error: String(r) }; } }); } -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); -async function cleanupOldRecordings() { +const K = t.dirname(E(import.meta.url)), h = t.join(p.getPath("userData"), "recordings"); +async function Q() { try { - const files = await fs.readdir(RECORDINGS_DIR); - const now = Date.now(); - const maxAge = 1 * 24 * 60 * 60 * 1e3; - for (const file of files) { - const filePath = path.join(RECORDINGS_DIR, file); - const stats = await fs.stat(filePath); - if (now - stats.mtimeMs > maxAge) { - await fs.unlink(filePath); - console.log(`Deleted old recording: ${file}`); - } + const e = await m.readdir(h), n = Date.now(), i = 1 * 24 * 60 * 60 * 1e3; + for (const v of e) { + const T = t.join(h, v), o = await m.stat(T); + n - o.mtimeMs > i && (await m.unlink(T), console.log(`Deleted old recording: ${v}`)); } - } catch (error) { - console.error("Failed to cleanup old recordings:", error); + } catch (e) { + console.error("Failed to cleanup old recordings:", e); } } -async function ensureRecordingsDir() { +async function X() { try { - await fs.mkdir(RECORDINGS_DIR, { recursive: true }); - console.log("Recordings directory ready:", RECORDINGS_DIR); - } catch (error) { - console.error("Failed to create recordings directory:", error); + await m.mkdir(h, { recursive: !0 }), console.log("Recordings directory ready:", h); + } catch (e) { + console.error("Failed to create recordings directory:", e); } } -process.env.APP_ROOT = path.join(__dirname, ".."); -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"); -process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, "public") : RENDERER_DIST; -let mainWindow = null; -let sourceSelectorWindow = null; -let tray = null; -let selectedSourceName = ""; -function createWindow() { - mainWindow = createHudOverlayWindow(); +process.env.APP_ROOT = t.join(K, ".."); +const Y = process.env.VITE_DEV_SERVER_URL, ie = t.join(process.env.APP_ROOT, "dist-electron"), I = t.join(process.env.APP_ROOT, "dist"); +process.env.VITE_PUBLIC = Y ? t.join(process.env.APP_ROOT, "public") : I; +let l = null, k = null, g = null, j = ""; +function D() { + l = C(); } -function createTray() { - const iconPath = path.join(process.env.VITE_PUBLIC || RENDERER_DIST, "rec-button.png"); - let icon = nativeImage.createFromPath(iconPath); - icon = icon.resize({ width: 24, height: 24, quality: "best" }); - tray = new Tray(icon); - updateTrayMenu(); +function Z() { + const e = t.join(process.env.VITE_PUBLIC || I, "rec-button.png"); + let n = L.createFromPath(e); + n = n.resize({ width: 24, height: 24, quality: "best" }), g = new V(n), F(); } -function updateTrayMenu() { - if (!tray) return; - const menuTemplate = [ +function F() { + if (!g) return; + const e = [ { label: "Stop Recording", click: () => { - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send("stop-recording-from-tray"); - } + l && !l.isDestroyed() && l.webContents.send("stop-recording-from-tray"); } } - ]; - const contextMenu = Menu.buildFromTemplate(menuTemplate); - tray.setContextMenu(contextMenu); - tray.setToolTip(`Recording: ${selectedSourceName}`); + ], n = U.buildFromTemplate(e); + g.setContextMenu(n), g.setToolTip(`Recording: ${j}`); } -function createEditorWindowWrapper() { - if (mainWindow) { - mainWindow.close(); - mainWindow = null; - } - mainWindow = createEditorWindow(); +function ee() { + l && (l.close(), l = null), l = N(); } -function createSourceSelectorWindowWrapper() { - sourceSelectorWindow = createSourceSelectorWindow(); - sourceSelectorWindow.on("closed", () => { - sourceSelectorWindow = null; - }); - return sourceSelectorWindow; +function te() { + return k = H(), k.on("closed", () => { + k = null; + }), k; } -app.on("window-all-closed", () => { +p.on("window-all-closed", () => { }); -app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); - } +p.on("activate", () => { + P.getAllWindows().length === 0 && D(); }); -app.on("before-quit", async (event) => { - event.preventDefault(); - cleanupMouseTracking(); - await cleanupOldRecordings(); - app.exit(0); +p.on("before-quit", async (e) => { + e.preventDefault(), G(), await Q(), p.exit(0); }); -app.whenReady().then(async () => { - await ensureRecordingsDir(); - registerIpcHandlers( - createEditorWindowWrapper, - createSourceSelectorWindowWrapper, - () => mainWindow, - () => sourceSelectorWindow, - (recording, sourceName) => { - selectedSourceName = sourceName; - if (recording) { - if (!tray) createTray(); - updateTrayMenu(); - if (mainWindow) mainWindow.minimize(); - } else { - if (tray) { - tray.destroy(); - tray = null; - } - if (mainWindow) mainWindow.restore(); - } +p.whenReady().then(async () => { + await X(), J( + ee, + te, + () => l, + () => k, + (e, n) => { + j = n, e ? (g || Z(), F(), l && l.minimize()) : (g && (g.destroy(), g = null), l && l.restore()); } - ); - createWindow(); + ), D(); }); export { - MAIN_DIST, - RECORDINGS_DIR, - RENDERER_DIST, - VITE_DEV_SERVER_URL + ie as MAIN_DIST, + h as RECORDINGS_DIR, + I as RENDERER_DIST, + Y as VITE_DEV_SERVER_URL }; diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index 5c862ea..92d0779 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -1,51 +1 @@ -"use strict"; -const electron = require("electron"); -electron.contextBridge.exposeInMainWorld("electronAPI", { - getAssetBasePath: async () => { - return await electron.ipcRenderer.invoke("get-asset-base-path"); - }, - getSources: async (opts) => { - return await electron.ipcRenderer.invoke("get-sources", opts); - }, - switchToEditor: () => { - return electron.ipcRenderer.invoke("switch-to-editor"); - }, - openSourceSelector: () => { - return electron.ipcRenderer.invoke("open-source-selector"); - }, - selectSource: (source) => { - return electron.ipcRenderer.invoke("select-source", source); - }, - getSelectedSource: () => { - return electron.ipcRenderer.invoke("get-selected-source"); - }, - startMouseTracking: () => { - return electron.ipcRenderer.invoke("start-mouse-tracking"); - }, - stopMouseTracking: () => { - return electron.ipcRenderer.invoke("stop-mouse-tracking"); - }, - storeRecordedVideo: (videoData, fileName) => { - return electron.ipcRenderer.invoke("store-recorded-video", videoData, fileName); - }, - storeMouseTrackingData: (fileName) => { - return electron.ipcRenderer.invoke("store-mouse-tracking-data", fileName); - }, - getRecordedVideoPath: () => { - return electron.ipcRenderer.invoke("get-recorded-video-path"); - }, - setRecordingState: (recording) => { - return electron.ipcRenderer.invoke("set-recording-state", recording); - }, - onStopRecordingFromTray: (callback) => { - const listener = () => callback(); - electron.ipcRenderer.on("stop-recording-from-tray", listener); - return () => electron.ipcRenderer.removeListener("stop-recording-from-tray", listener); - }, - openExternalUrl: (url) => { - return electron.ipcRenderer.invoke("open-external-url", url); - }, - saveExportedVideo: (videoData, fileName) => { - return electron.ipcRenderer.invoke("save-exported-video", videoData, fileName); - } -}); +"use strict";const e=require("electron");e.contextBridge.exposeInMainWorld("electronAPI",{getAssetBasePath:async()=>await e.ipcRenderer.invoke("get-asset-base-path"),getSources:async r=>await e.ipcRenderer.invoke("get-sources",r),switchToEditor:()=>e.ipcRenderer.invoke("switch-to-editor"),openSourceSelector:()=>e.ipcRenderer.invoke("open-source-selector"),selectSource:r=>e.ipcRenderer.invoke("select-source",r),getSelectedSource:()=>e.ipcRenderer.invoke("get-selected-source"),startMouseTracking:()=>e.ipcRenderer.invoke("start-mouse-tracking"),stopMouseTracking:()=>e.ipcRenderer.invoke("stop-mouse-tracking"),storeRecordedVideo:(r,t)=>e.ipcRenderer.invoke("store-recorded-video",r,t),storeMouseTrackingData:r=>e.ipcRenderer.invoke("store-mouse-tracking-data",r),getRecordedVideoPath:()=>e.ipcRenderer.invoke("get-recorded-video-path"),setRecordingState:r=>e.ipcRenderer.invoke("set-recording-state",r),onStopRecordingFromTray:r=>{const t=()=>r();return e.ipcRenderer.on("stop-recording-from-tray",t),()=>e.ipcRenderer.removeListener("stop-recording-from-tray",t)},openExternalUrl:r=>e.ipcRenderer.invoke("open-external-url",r),saveExportedVideo:(r,t)=>e.ipcRenderer.invoke("save-exported-video",r,t)});