configsupportcheck and throttling
This commit is contained in:
+265
-160
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -65,6 +65,7 @@ export function createEditorWindow(): BrowserWindow {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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<void>(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<void>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user