diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 57b83ba5..d7117fdd 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -157,6 +157,7 @@ let currentProjectPath: string | null = null let nativeScreenRecordingActive = false let currentVideoPath: string | null = null let currentRecordingSession: RecordingSessionData | null = null +const approvedLocalReadPaths = new Set() let nativeCaptureProcess: ChildProcessWithoutNullStreams | null = null let nativeCaptureOutputBuffer = '' let nativeCaptureTargetPath: string | null = null @@ -623,6 +624,44 @@ function normalizeVideoSourcePath(videoPath?: string | null): string | null { return trimmed } +function isPathInsideDirectory(candidatePath: string, directoryPath: string) { + const normalizedDirectoryPath = normalizePath(directoryPath) + return candidatePath === normalizedDirectoryPath || candidatePath.startsWith(`${normalizedDirectoryPath}${path.sep}`) +} + +function isAllowedLocalReadPath(candidatePath: string) { + const allowedPrefixes = [ + RECORDINGS_DIR, + USER_DATA_PATH, + getAssetRootPath(), + app.getPath('temp'), + ] + + return allowedPrefixes.some((prefix) => isPathInsideDirectory(candidatePath, prefix)) + || approvedLocalReadPaths.has(candidatePath) +} + +async function rememberApprovedLocalReadPath(filePath?: string | null) { + const normalizedPath = normalizeVideoSourcePath(filePath) + if (!normalizedPath) { + return + } + + const resolvedPath = normalizePath(normalizedPath) + approvedLocalReadPaths.add(resolvedPath) + + try { + approvedLocalReadPaths.add(await fs.realpath(resolvedPath)) + } catch { + // Ignore missing files; the eventual read will surface the real error. + } +} + +async function replaceApprovedSessionLocalReadPaths(filePaths: Array) { + approvedLocalReadPaths.clear() + await Promise.all(filePaths.map((filePath) => rememberApprovedLocalReadPath(filePath))) +} + async function resolveProjectMediaSources(project: unknown): Promise< | { success: true @@ -4887,7 +4926,12 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} } try { - return { success: true, paths: await getCompanionAudioFallbackPaths(videoPath) } + const paths = await getCompanionAudioFallbackPaths(videoPath) + await Promise.all([ + rememberApprovedLocalReadPath(videoPath), + ...paths.map((fallbackPath) => rememberApprovedLocalReadPath(fallbackPath)), + ]) + return { success: true, paths } } catch (error) { console.error('Failed to resolve companion audio fallback paths:', error) return { success: false, paths: [], error: String(error) } @@ -5349,14 +5393,9 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} try { // Security: only allow reads from known safe directories to prevent // malicious code from reading arbitrary files (SSH keys, credentials, etc.) - const resolved = path.resolve(filePath) - const allowedPrefixes = [ - RECORDINGS_DIR, - USER_DATA_PATH, - getAssetRootPath(), - app.getPath('temp'), - ] - if (!allowedPrefixes.some((prefix) => resolved.startsWith(path.resolve(prefix)))) { + const resolved = normalizePath(filePath) + const realResolved = await fs.realpath(resolved).catch(() => resolved) + if (!isAllowedLocalReadPath(resolved) && !isAllowedLocalReadPath(realResolved)) { console.warn(`[read-local-file] Blocked read outside allowed directories: ${resolved}`) return { success: false, error: 'Access denied: path outside allowed directories' } } @@ -6151,6 +6190,10 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} } currentRecordingSession = resolvedSession + await replaceApprovedSessionLocalReadPaths([ + resolvedSession.videoPath, + resolvedSession.webcamPath, + ]) if (resolvedSession.webcamPath) { await persistRecordingSessionManifest(resolvedSession) @@ -6168,6 +6211,10 @@ body{background:transparent;overflow:hidden;width:100vw;height:100vh} webcamPath: normalizeVideoSourcePath(session.webcamPath ?? null), timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), } + await replaceApprovedSessionLocalReadPaths([ + currentRecordingSession.videoPath, + currentRecordingSession.webcamPath, + ]) currentProjectPath = null await persistRecordingSessionManifest(currentRecordingSession) return { success: true } diff --git a/package-lock.json b/package-lock.json index be372cdc..46d2bfe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "recordly", - "version": "1.1.22", + "version": "1.1.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "recordly", - "version": "1.1.22", + "version": "1.1.23", "hasInstallScript": true, "dependencies": { "capturekit": "^1.0.13", diff --git a/package.json b/package.json index 0d37a181..68af12b7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/webadderall/Recordly/issues" }, "private": true, - "version": "1.1.22", + "version": "1.1.23", "type": "module", "scripts": { "dev": "vite", diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index c1c1d366..3a36d1d9 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -12,7 +12,7 @@ import type { ZoomRegion, ZoomTransitionEasing, } from "@/components/video-editor/types"; -import { AudioProcessor } from "./audioEncoder"; +import { AudioProcessor, isAacAudioEncodingSupported } from "./audioEncoder"; import { normalizeLightningRuntimePlatform, shouldPreferNativeAutoBackend, @@ -293,7 +293,9 @@ export class ModernVideoExporter { this.metadataLoadTimeMs = this.getNowMs() - stageStartedAt; const nativeAudioPlan = this.buildNativeAudioPlan(videoInfo); const shouldUseFfmpegAudioFallback = - !useNativeEncoder && nativeAudioPlan.audioMode !== "none"; + !useNativeEncoder + && nativeAudioPlan.audioMode !== "none" + && !(await isAacAudioEncodingSupported()); const effectiveDuration = this.streamingDecoder.getEffectiveDuration( this.config.trimRegions, this.config.speedRegions, diff --git a/src/lib/exporter/videoExporter.ts b/src/lib/exporter/videoExporter.ts index 4c249df5..a346b7fa 100644 --- a/src/lib/exporter/videoExporter.ts +++ b/src/lib/exporter/videoExporter.ts @@ -12,7 +12,7 @@ import type { ZoomTransitionEasing, ZoomRegion, } from "@/components/video-editor/types"; -import { AudioProcessor } from "./audioEncoder"; +import { AudioProcessor, isAacAudioEncodingSupported } from "./audioEncoder"; import { FrameRenderer } from "./frameRenderer"; import type { SupportedMp4EncoderPath } from "./mp4Support"; import { captureCanvasFrameForNativeExport } from "./nativeFrameCapture"; @@ -129,7 +129,10 @@ export class VideoExporter { let useNativeEncoder = shouldUseExperimentalNativeExport ? await this.tryStartNativeVideoExport() : false; - const shouldUseFfmpegAudioFallback = !useNativeEncoder && audioPlan.audioMode !== "none"; + const shouldUseFfmpegAudioFallback = + !useNativeEncoder + && audioPlan.audioMode !== "none" + && !(await isAacAudioEncodingSupported()); if (!useNativeEncoder) { await this.initializeEncoder();