diff --git a/dist-electron/main.js b/dist-electron/main.js index c99de03..fce1288 100644 --- a/dist-electron/main.js +++ b/dist-electron/main.js @@ -1,236 +1,341 @@ -import { BrowserWindow as _, screen as D, ipcMain as c, desktopCapturer as j, shell as x, app as l, nativeImage as F, Tray as O, Menu as W } from "electron"; -import { fileURLToPath as P } from "node:url"; -import t from "node:path"; -import p from "node:fs/promises"; -const v = t.dirname(P(import.meta.url)), V = t.join(v, ".."), f = process.env.VITE_DEV_SERVER_URL, T = t.join(V, "dist"); -function L() { - const e = new _({ +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"; +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({ width: 250, height: 80, minWidth: 250, maxWidth: 250, minHeight: 80, maxHeight: 80, - frame: !1, - transparent: !0, - resizable: !1, - alwaysOnTop: !0, - skipTaskbar: !0, - hasShadow: !1, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, + hasShadow: false, webPreferences: { - preload: t.join(v, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - backgroundThrottling: !1 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + backgroundThrottling: false } }); - return e.webContents.on("did-finish-load", () => { - e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), f ? e.loadURL(f + "?windowType=hud-overlay") : e.loadFile(t.join(T, "index.html"), { - query: { windowType: "hud-overlay" } - }), e; + 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; } -function U() { - const e = new _({ +function createEditorWindow() { + const win = new BrowserWindow({ width: 1200, height: 800, minWidth: 800, minHeight: 600, titleBarStyle: "hiddenInset", trafficLightPosition: { x: 12, y: 12 }, - transparent: !1, - resizable: !0, - alwaysOnTop: !1, - skipTaskbar: !1, + transparent: false, + resizable: true, + alwaysOnTop: false, + skipTaskbar: false, title: "OpenScreen", backgroundColor: "#000000", webPreferences: { - preload: t.join(v, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0, - webSecurity: !1 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true, + webSecurity: false, + backgroundThrottling: false } }); - return e.maximize(), e.webContents.on("did-finish-load", () => { - e == null || e.webContents.send("main-process-message", (/* @__PURE__ */ new Date()).toLocaleString()); - }), f ? e.loadURL(f + "?windowType=editor") : e.loadFile(t.join(T, "index.html"), { - query: { windowType: "editor" } - }), e; + 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; } -function k() { - const { width: e, height: s } = D.getPrimaryDisplay().workAreaSize, u = new _({ +function createSourceSelectorWindow() { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + const win = new BrowserWindow({ width: 620, height: 420, minHeight: 350, maxHeight: 500, - x: Math.round((e - 620) / 2), - y: Math.round((s - 420) / 2), - frame: !1, - resizable: !1, - alwaysOnTop: !0, - transparent: !0, + x: Math.round((width - 620) / 2), + y: Math.round((height - 420) / 2), + frame: false, + resizable: false, + alwaysOnTop: true, + transparent: true, backgroundColor: "#00000000", webPreferences: { - preload: t.join(v, "preload.mjs"), - nodeIntegration: !1, - contextIsolation: !0 + preload: path.join(__dirname$1, "preload.mjs"), + nodeIntegration: false, + contextIsolation: true } }); - return f ? u.loadURL(f + "?windowType=source-selector") : u.loadFile(t.join(T, "index.html"), { - query: { windowType: "source-selector" } - }), u; + 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; } -let R = null; -function C(e, s, u, m, w) { - c.handle("get-sources", async (o, n) => (await j.getSources(n)).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, n) => { - R = n; - const a = m(); - return a && a.close(), R; - }), c.handle("get-selected-source", () => R), c.handle("open-source-selector", () => { - const o = m(); - if (o) { - o.focus(); +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(); return; } - s(); - }), c.handle("switch-to-editor", () => { - const o = u(); - o && o.close(), e(); - }), c.handle("store-recorded-video", async (o, n, a) => { + createSourceSelectorWindow2(); + }); + ipcMain.handle("switch-to-editor", () => { + const mainWin = getMainWindow(); + if (mainWin) { + mainWin.close(); + } + createEditorWindow2(); + }); + ipcMain.handle("store-recorded-video", async (_, videoData, fileName) => { try { - const r = t.join(h, a); - return await p.writeFile(r, Buffer.from(n)), { - success: !0, - path: r, + const videoPath = path.join(RECORDINGS_DIR, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + return { + success: true, + path: videoPath, message: "Video stored successfully" }; - } catch (r) { - return console.error("Failed to store video:", r), { - success: !1, + } catch (error) { + console.error("Failed to store video:", error); + return { + success: false, message: "Failed to store video", - error: String(r) + error: String(error) }; } - }), c.handle("get-recorded-video-path", async () => { + }); + ipcMain.handle("get-recorded-video-path", async () => { try { - const n = (await p.readdir(h)).filter((y) => y.endsWith(".webm")); - if (n.length === 0) - return { success: !1, message: "No recorded video found" }; - const a = n.sort().reverse()[0]; - return { success: !0, path: t.join(h, a) }; - } catch (o) { - return console.error("Failed to get video path:", o), { success: !1, message: "Failed to get video path", error: String(o) }; + 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) }; } - }), c.handle("set-recording-state", (o, n) => { - w && w(n, (R || { name: "Screen" }).name); - }), c.handle("open-external-url", async (o, n) => { - try { - return await x.openExternal(n), { success: !0 }; - } catch (a) { - return console.error("Failed to open URL:", a), { success: !1, error: String(a) }; + }); + ipcMain.handle("set-recording-state", (_, recording) => { + const source = selectedSource || { name: "Screen" }; + if (onRecordingStateChange) { + onRecordingStateChange(recording, source.name); } - }), c.handle("get-asset-base-path", () => { + }); + ipcMain.handle("open-external-url", async (_, url) => { try { - return l.isPackaged ? t.join(process.resourcesPath, "assets") : t.join(l.getAppPath(), "public", "assets"); - } catch (o) { - return console.error("Failed to resolve asset base path:", o), null; + await shell.openExternal(url); + return { success: true }; + } catch (error) { + console.error("Failed to open URL:", error); + return { success: false, error: String(error) }; } - }), c.handle("save-exported-video", async (o, n, a) => { + }); + ipcMain.handle("get-asset-base-path", () => { try { - const r = l.getPath("downloads"), y = t.join(r, a); - return await p.writeFile(y, Buffer.from(n)), { - success: !0, - path: y, + 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; + } + }); + ipcMain.handle("save-exported-video", async (_, videoData, fileName) => { + try { + const downloadsPath = app.getPath("downloads"); + const videoPath = path.join(downloadsPath, fileName); + await fs.writeFile(videoPath, Buffer.from(videoData)); + return { + success: true, + path: videoPath, message: "Video exported successfully" }; - } catch (r) { - return console.error("Failed to save exported video:", r), { - success: !1, + } catch (error) { + console.error("Failed to save exported video:", error); + return { + success: false, message: "Failed to save exported video", - error: String(r) + error: String(error) }; } }); } -const A = t.dirname(P(import.meta.url)), h = t.join(l.getPath("userData"), "recordings"); -async function M() { +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const RECORDINGS_DIR = path.join(app.getPath("userData"), "recordings"); +async function cleanupOldRecordings() { try { - const e = await p.readdir(h), s = Date.now(), u = 1 * 24 * 60 * 60 * 1e3; - for (const m of e) { - const w = t.join(h, m), o = await p.stat(w); - s - o.mtimeMs > u && (await p.unlink(w), console.log(`Deleted old recording: ${m}`)); + 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}`); + } } - } catch (e) { - console.error("Failed to cleanup old recordings:", e); + } catch (error) { + console.error("Failed to cleanup old recordings:", error); } } -async function z() { +async function ensureRecordingsDir() { try { - await p.mkdir(h, { recursive: !0 }), console.log("RECORDINGS_DIR:", h), console.log("User Data Path:", l.getPath("userData")); - } catch (e) { - console.error("Failed to create recordings directory:", e); + await fs.mkdir(RECORDINGS_DIR, { recursive: true }); + console.log("RECORDINGS_DIR:", RECORDINGS_DIR); + console.log("User Data Path:", app.getPath("userData")); + } catch (error) { + console.error("Failed to create recordings directory:", error); } } -process.env.APP_ROOT = t.join(A, ".."); -const H = process.env.VITE_DEV_SERVER_URL, Q = t.join(process.env.APP_ROOT, "dist-electron"), b = t.join(process.env.APP_ROOT, "dist"); -process.env.VITE_PUBLIC = H ? t.join(process.env.APP_ROOT, "public") : b; -let i = null, g = null, d = null, E = ""; -function S() { - i = L(); +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(); } -function N() { - const e = t.join(process.env.VITE_PUBLIC || b, "rec-button.png"); - let s = F.createFromPath(e); - s = s.resize({ width: 24, height: 24, quality: "best" }), d = new O(s), I(); +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 I() { - if (!d) return; - const e = [ +function updateTrayMenu() { + if (!tray) return; + const menuTemplate = [ { label: "Stop Recording", click: () => { - i && !i.isDestroyed() && i.webContents.send("stop-recording-from-tray"); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("stop-recording-from-tray"); + } } } - ], s = W.buildFromTemplate(e); - d.setContextMenu(s), d.setToolTip(`Recording: ${E}`); + ]; + const contextMenu = Menu.buildFromTemplate(menuTemplate); + tray.setContextMenu(contextMenu); + tray.setToolTip(`Recording: ${selectedSourceName}`); } -function B() { - i && (i.close(), i = null), i = U(); +function createEditorWindowWrapper() { + if (mainWindow) { + mainWindow.close(); + mainWindow = null; + } + mainWindow = createEditorWindow(); } -function q() { - return g = k(), g.on("closed", () => { - g = null; - }), g; +function createSourceSelectorWindowWrapper() { + sourceSelectorWindow = createSourceSelectorWindow(); + sourceSelectorWindow.on("closed", () => { + sourceSelectorWindow = null; + }); + return sourceSelectorWindow; } -l.on("window-all-closed", () => { +app.on("window-all-closed", () => { }); -l.on("activate", () => { - _.getAllWindows().length === 0 && S(); +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } }); -l.on("before-quit", async (e) => { - e.preventDefault(), await M(), l.exit(0); +app.on("before-quit", async (event) => { + event.preventDefault(); + await cleanupOldRecordings(); + app.exit(0); }); -l.whenReady().then(async () => { - await z(), C( - B, - q, - () => i, - () => g, - (e, s) => { - E = s, e ? (d || N(), I(), i && i.minimize()) : (d && (d.destroy(), d = null), i && i.restore()); +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(); + } } - ), S(); + ); + createWindow(); }); export { - Q as MAIN_DIST, - h as RECORDINGS_DIR, - b as RENDERER_DIST, - H as VITE_DEV_SERVER_URL + MAIN_DIST, + RECORDINGS_DIR, + RENDERER_DIST, + VITE_DEV_SERVER_URL }; diff --git a/dist-electron/preload.mjs b/dist-electron/preload.mjs index a08f981..7f7797b 100644 --- a/dist-electron/preload.mjs +++ b/dist-electron/preload.mjs @@ -1 +1,42 @@ -"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"),storeRecordedVideo:(r,t)=>e.ipcRenderer.invoke("store-recorded-video",r,t),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)}); +"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"); + }, + storeRecordedVideo: (videoData, fileName) => { + return electron.ipcRenderer.invoke("store-recorded-video", videoData, 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); + } +}); diff --git a/electron/windows.ts b/electron/windows.ts index 42875ee..754cdb2 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -65,6 +65,7 @@ export function createEditorWindow(): BrowserWindow { nodeIntegration: false, contextIsolation: true, webSecurity: false, + backgroundThrottling: false, }, }) diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 4bc6f94..fdcd35c 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -83,11 +83,17 @@ export class VideoExporter { // 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 + + if (needsSeek) { + // Attach listener BEFORE setting currentTime to avoid race condition + const seekedPromise = new Promise(resolve => { + videoElement.addEventListener('seeked', () => resolve(), { once: true }); + }); + + videoElement.currentTime = videoTime; + await seekedPromise; + } else if (i === 0) { + // Only for the very first frame, wait for it to be ready await new Promise(resolve => { videoElement.requestVideoFrameCallback(() => resolve()); }); @@ -126,11 +132,13 @@ export class VideoExporter { if (this.encoder && this.encoder.state === 'configured') { this.encodeQueue++; this.encoder.encode(exportFrame, { keyFrame: i % 150 === 0 }); + } else { + console.warn(`[Frame ${i}] Encoder not ready! State: ${this.encoder?.state}`); } exportFrame.close(); frameIndex++; - + // Update progress if (this.config.onProgress) { this.config.onProgress({ @@ -226,7 +234,9 @@ export class VideoExporter { this.encodeQueue--; }, error: (error) => { - console.error('VideoEncoder error:', error); + console.error('[VideoExporter] Encoder error:', error); + // Stop export encoding failed + this.cancelled = true; }, }); @@ -243,30 +253,24 @@ export class VideoExporter { hardwareAcceleration: 'prefer-hardware', }; - try { - console.log('[VideoExporter] Configuring encoder with hardware acceleration...', { - codec, - resolution: `${this.config.width}x${this.config.height}`, - bitrate: this.config.bitrate, - framerate: this.config.frameRate, - }); - - this.encoder.configure(encoderConfig as VideoEncoderConfig); - - console.log('[VideoExporter] Hardware encoder configured successfully'); - } catch (error) { - console.warn('[VideoExporter] Hardware encoding failed, falling back to software encoding...', error); - - // Fallback to software encoding if hardware fails + // Check hardware support first + const hardwareSupport = await VideoEncoder.isConfigSupported(encoderConfig); + + if (hardwareSupport.supported) { + // Use hardware encoding + console.log('[VideoExporter] Using hardware acceleration'); + this.encoder.configure(encoderConfig); + } else { + // Fall back to software encoding + console.log('[VideoExporter] Hardware not supported, using software encoding'); encoderConfig.hardwareAcceleration = 'prefer-software'; - try { - this.encoder.configure(encoderConfig as VideoEncoderConfig); - console.log('[VideoExporter] Software encoder configured successfully'); - } catch (softwareError) { - console.error('[VideoExporter] Software encoding also failed:', softwareError); - throw new Error(`Failed to initialize video encoder: ${softwareError instanceof Error ? softwareError.message : String(softwareError)}`); + const softwareSupport = await VideoEncoder.isConfigSupported(encoderConfig); + if (!softwareSupport.supported) { + throw new Error('Video encoding not supported on this system'); } + + this.encoder.configure(encoderConfig); } }