diff --git a/electron/ipc/captions/generate.ts b/electron/ipc/captions/generate.ts index f74abf15..33c34584 100644 --- a/electron/ipc/captions/generate.ts +++ b/electron/ipc/captions/generate.ts @@ -12,9 +12,9 @@ import { resolveRecordingSession } from "../project/session"; const execFileAsync = promisify(execFile); -export async function ensureReadableFile(filePath: string, description: string) { +export async function ensureReadableFile(filePath: string, options?: { executable?: boolean }) { await fs.access(filePath, fsConstants.R_OK); - if (description === "whisper executable") { + if (options?.executable) { try { await fs.access(filePath, fsConstants.X_OK); } catch { @@ -113,7 +113,7 @@ export async function extractCaptionAudioSource(options: { for (const candidate of candidates) { try { - await ensureReadableFile(candidate.path, "video file"); + await ensureReadableFile(candidate.path); await execFileAsync( options.ffmpegPath, [ @@ -169,8 +169,8 @@ export async function generateAutoCaptionsFromVideo(options: { const whisperExecutablePath = await resolveWhisperExecutablePath(options.whisperExecutablePath); const whisperModelPath = path.resolve(options.whisperModelPath); - await ensureReadableFile(whisperExecutablePath, "whisper executable"); - await ensureReadableFile(whisperModelPath, "whisper model"); + await ensureReadableFile(whisperExecutablePath, { executable: true }); + await ensureReadableFile(whisperModelPath); const tempBase = path.join( app.getPath("temp"), diff --git a/electron/ipc/captions/whisper.ts b/electron/ipc/captions/whisper.ts index 70a97ede..c8e774c6 100644 --- a/electron/ipc/captions/whisper.ts +++ b/electron/ipc/captions/whisper.ts @@ -41,7 +41,7 @@ export function downloadFileWithProgress( ): Promise { const request = (currentUrl: string, redirectCount = 0): Promise => { return new Promise((resolve, reject) => { - const req = httpsGet(currentUrl, (response) => { + const req = httpsGet(currentUrl, { timeout: 30_000 }, (response) => { const statusCode = response.statusCode ?? 0; const location = response.headers.location; @@ -97,6 +97,9 @@ export function downloadFileWithProgress( }); req.on("error", reject); + req.on("timeout", () => { + req.destroy(new Error("Whisper model download timed out.")); + }); }); }; diff --git a/electron/ipc/cursor/interaction.ts b/electron/ipc/cursor/interaction.ts index 81b8bbdb..f5ae15ca 100644 --- a/electron/ipc/cursor/interaction.ts +++ b/electron/ipc/cursor/interaction.ts @@ -183,7 +183,9 @@ export async function startInteractionCapture() { hook.on("mousedown", onMouseDown); hook.on("mouseup", onMouseUp); - hook.on("mousemove", onMouseMove); + if (process.platform === "linux") { + hook.on("mousemove", onMouseMove); + } hook.start(); @@ -192,11 +194,15 @@ export async function startInteractionCapture() { if (typeof hook.off === "function") { hook.off("mousedown", onMouseDown); hook.off("mouseup", onMouseUp); - hook.off("mousemove", onMouseMove); + if (process.platform === "linux") { + hook.off("mousemove", onMouseMove); + } } else if (typeof hook.removeListener === "function") { hook.removeListener("mousedown", onMouseDown); hook.removeListener("mouseup", onMouseUp); - hook.removeListener("mousemove", onMouseMove); + if (process.platform === "linux") { + hook.removeListener("mousemove", onMouseMove); + } } } catch { // ignore listener cleanup errors diff --git a/electron/ipc/cursor/monitor.ts b/electron/ipc/cursor/monitor.ts index 4943e523..0c39909a 100644 --- a/electron/ipc/cursor/monitor.ts +++ b/electron/ipc/cursor/monitor.ts @@ -1,6 +1,7 @@ import { spawn } from "node:child_process"; import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; +import { BrowserWindow } from "electron"; import type { CursorVisualType } from "../types"; import { currentCursorVisualType, @@ -13,7 +14,6 @@ import { import { getCursorMonitorExePath, ensureNativeCursorMonitorBinary } from "../paths/binaries"; export function emitCursorStateChanged(cursorType: CursorVisualType) { - const { BrowserWindow } = require("electron") as typeof import("electron"); BrowserWindow.getAllWindows().forEach((window) => { if (!window.isDestroyed()) { window.webContents.send("cursor-state-changed", { cursorType }); diff --git a/electron/ipc/ffmpeg/binary.ts b/electron/ipc/ffmpeg/binary.ts index 095db5a8..c76acd40 100644 --- a/electron/ipc/ffmpeg/binary.ts +++ b/electron/ipc/ffmpeg/binary.ts @@ -6,13 +6,17 @@ import { app } from "electron"; const nodeRequire = createRequire(import.meta.url); export function loadFfmpegStatic(): string | null { - const moduleExports = nodeRequire("ffmpeg-static"); - if (typeof moduleExports === "string") { - return moduleExports; - } + try { + const moduleExports = nodeRequire("ffmpeg-static"); + if (typeof moduleExports === "string") { + return moduleExports; + } - if (typeof moduleExports?.default === "string") { - return moduleExports.default as string; + if (typeof moduleExports?.default === "string") { + return moduleExports.default as string; + } + } catch { + // ffmpeg-static not available; fall through to system FFmpeg } return null; diff --git a/electron/ipc/paths/binaries.ts b/electron/ipc/paths/binaries.ts index c43cb97c..45ec80da 100644 --- a/electron/ipc/paths/binaries.ts +++ b/electron/ipc/paths/binaries.ts @@ -1,13 +1,16 @@ -import { spawnSync } from "node:child_process"; +import { execFile } from "node:child_process"; import { existsSync, constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; +import { promisify } from "node:util"; import { app } from "electron"; import { nativeHelperMigrationPromise, setNativeHelperMigrationPromise, } from "../state"; +const execFileAsync = promisify(execFile); + /** * Resolve a path within the app bundle, handling asar unpacking in production. * Files listed in asarUnpack are extracted to app.asar.unpacked/ and must be @@ -205,13 +208,14 @@ export async function ensureSwiftHelperBinary( return binaryPath; } - const result = spawnSync("swiftc", ["-O", sourcePath, "-o", binaryPath], { - encoding: "utf8", - timeout: 120000, - }); - - if (result.status !== 0) { - const details = [result.stderr, result.stdout].filter(Boolean).join("\n").trim(); + try { + await execFileAsync("swiftc", ["-O", sourcePath, "-o", binaryPath], { + encoding: "utf8", + timeout: 120000, + }); + } catch (error) { + const err = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string }; + const details = [err.stderr, err.stdout].filter(Boolean).join("\n").trim(); throw new Error(details || `Failed to compile ${label}`); } diff --git a/electron/ipc/project/manager.ts b/electron/ipc/project/manager.ts index b3d09607..2db9e774 100644 --- a/electron/ipc/project/manager.ts +++ b/electron/ipc/project/manager.ts @@ -41,10 +41,11 @@ export function getAssetRootPath() { } export function isPathInsideDirectory(candidatePath: string, directoryPath: string) { + const normalizedCandidatePath = normalizePath(candidatePath); const normalizedDirectoryPath = normalizePath(directoryPath); return ( - candidatePath === normalizedDirectoryPath || - candidatePath.startsWith(`${normalizedDirectoryPath}${path.sep}`) + normalizedCandidatePath === normalizedDirectoryPath || + normalizedCandidatePath.startsWith(`${normalizedDirectoryPath}${path.sep}`) ); } @@ -52,9 +53,9 @@ export function isAllowedLocalReadPath(candidatePath: string) { const allowedPrefixes = [RECORDINGS_DIR, USER_DATA_PATH, getAssetRootPath(), app.getPath("temp")]; return ( - existsSync(candidatePath) || - allowedPrefixes.some((prefix) => isPathInsideDirectory(candidatePath, prefix)) || - approvedLocalReadPaths.has(candidatePath) + existsSync(candidatePath) && + (allowedPrefixes.some((prefix) => isPathInsideDirectory(candidatePath, prefix)) || + approvedLocalReadPaths.has(candidatePath)) ); } @@ -238,7 +239,10 @@ export async function buildProjectLibraryEntry( return { path: normalizedPath, - name: path.basename(normalizedPath).replace(/\.(recordly|openscreen)$/i, ""), + name: path.basename(normalizedPath).replace( + new RegExp(`\\.(${[PROJECT_FILE_EXTENSION, ...LEGACY_PROJECT_FILE_EXTENSIONS].join("|")})$`, "i"), + "", + ), updatedAt: stats.mtimeMs, thumbnailPath: thumbnailExists ? thumbnailPath : null, isCurrent: Boolean( diff --git a/electron/ipc/project/session.ts b/electron/ipc/project/session.ts index 429e7607..995f7193 100644 --- a/electron/ipc/project/session.ts +++ b/electron/ipc/project/session.ts @@ -65,7 +65,7 @@ export async function resolveRecordingSessionManifest( return { videoPath: normalizedVideoPath, webcamPath: null, - timeOffsetMs: 0, + timeOffsetMs: normalizeRecordingTimeOffsetMs(parsed.timeOffsetMs), }; } diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts index 07bdfe13..9ce89a89 100644 --- a/electron/ipc/recording/diagnostics.ts +++ b/electron/ipc/recording/diagnostics.ts @@ -55,13 +55,9 @@ export async function probeMediaDurationSeconds(filePath: string): Promise Boolean(value)) @@ -74,14 +76,12 @@ export async function pruneAutoRecordings(exemptPaths: string[] = []) { await fs.rm(getTelemetryPathForVideo(entry.filePath), { force: true }); // Clean up companion audio files left from recording (macOS .m4a, Windows .wav) const base = entry.filePath.replace(/\.(mp4|mov|webm)$/i, ""); - for (const suffix of [ - ".system.m4a", - ".mic.m4a", - ".system.wav", - ".mic.wav", - ".mic.webm", - ".system.webm", - ]) { + const companionSuffixes = Array.from( + new Set( + COMPANION_AUDIO_LAYOUTS.flatMap((layout) => [layout.systemSuffix, layout.micSuffix]), + ), + ); + for (const suffix of companionSuffixes) { await fs.rm(base + suffix, { force: true }).catch(() => undefined); } } catch (error) { diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index 76355b66..32c39cb4 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -32,21 +32,18 @@ const execFileAsync = promisify(execFile); export async function isNativeWindowsCaptureAvailable(): Promise { if (process.platform !== "win32") return false; - const helperPath = getWindowsCaptureExePath(); const os = await import("node:os"); const [major, , build] = os.release().split(".").map(Number); const supported = major >= 10 && build >= 19041; - let helperExists = false; + if (!supported) return false; try { - await fs.access(helperPath, fsConstants.X_OK); - helperExists = true; + await fs.access(getWindowsCaptureExePath(), fsConstants.X_OK); } catch { return false; } - void helperExists; - return supported; + return true; } export function waitForWindowsCaptureStart(proc: ChildProcessWithoutNullStreams) { diff --git a/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor index fdd0bd08..e0e478f1 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor and b/electron/native/bin/darwin-arm64/recordly-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper index ff4b8aa5..91b54570 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper and b/electron/native/bin/darwin-arm64/recordly-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-arm64/recordly-system-cursors b/electron/native/bin/darwin-arm64/recordly-system-cursors index d6564426..f4b41ab6 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-system-cursors and b/electron/native/bin/darwin-arm64/recordly-system-cursors differ diff --git a/electron/native/bin/darwin-arm64/recordly-window-list b/electron/native/bin/darwin-arm64/recordly-window-list index aa268ffd..76a7dab4 100755 Binary files a/electron/native/bin/darwin-arm64/recordly-window-list and b/electron/native/bin/darwin-arm64/recordly-window-list differ diff --git a/electron/native/bin/darwin-x64/recordly-native-cursor-monitor b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor index 58bdf488..d577b1a6 100755 Binary files a/electron/native/bin/darwin-x64/recordly-native-cursor-monitor and b/electron/native/bin/darwin-x64/recordly-native-cursor-monitor differ diff --git a/electron/native/bin/darwin-x64/recordly-screencapturekit-helper b/electron/native/bin/darwin-x64/recordly-screencapturekit-helper index 831b8690..3696e45d 100755 Binary files a/electron/native/bin/darwin-x64/recordly-screencapturekit-helper and b/electron/native/bin/darwin-x64/recordly-screencapturekit-helper differ diff --git a/electron/native/bin/darwin-x64/recordly-system-cursors b/electron/native/bin/darwin-x64/recordly-system-cursors index 2c53fd68..54561362 100755 Binary files a/electron/native/bin/darwin-x64/recordly-system-cursors and b/electron/native/bin/darwin-x64/recordly-system-cursors differ diff --git a/electron/native/bin/darwin-x64/recordly-window-list b/electron/native/bin/darwin-x64/recordly-window-list index 949a66bd..e165257a 100755 Binary files a/electron/native/bin/darwin-x64/recordly-window-list and b/electron/native/bin/darwin-x64/recordly-window-list differ