diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index b771e46..f66af16 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -51,6 +51,13 @@ interface Window { openVideoFilePicker: () => Promise<{ success: boolean; path?: string; canceled?: boolean }>; setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>; getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; + readBinaryFile: (filePath: string) => Promise<{ + success: boolean; + data?: ArrayBuffer; + path?: string; + message?: string; + error?: string; + }>; clearCurrentVideoPath: () => Promise<{ success: boolean }>; saveProjectFile: ( projectData: unknown, diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 70d3ae4..5665cd7 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -200,6 +200,29 @@ export function registerIpcHandlers( } }); + ipcMain.handle("read-binary-file", async (_, inputPath: string) => { + try { + const normalizedPath = normalizeVideoSourcePath(inputPath); + if (!normalizedPath) { + return { success: false, message: "Invalid file path" }; + } + + const data = await fs.readFile(normalizedPath); + return { + success: true, + data: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength), + path: normalizedPath, + }; + } catch (error) { + console.error("Failed to read binary file:", error); + return { + success: false, + message: "Failed to read binary file", + error: String(error), + }; + } + }); + ipcMain.handle("set-recording-state", (_, recording: boolean) => { if (recording) { stopCursorCapture(); diff --git a/electron/preload.ts b/electron/preload.ts index 9eeb5b1..acdec4f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -60,6 +60,9 @@ contextBridge.exposeInMainWorld("electronAPI", { getCurrentVideoPath: () => { return ipcRenderer.invoke("get-current-video-path"); }, + readBinaryFile: (filePath: string) => { + return ipcRenderer.invoke("read-binary-file", filePath); + }, clearCurrentVideoPath: () => { return ipcRenderer.invoke("clear-current-video-path"); }, diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index 2d0a5a3..7ebd78d 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -32,11 +32,37 @@ export class StreamingVideoDecoder { private cancelled = false; private metadata: DecodedVideoInfo | null = null; - async loadMetadata(videoUrl: string): Promise { + private async loadSourceFile(videoUrl: string): Promise<{ file: File; blob: Blob }> { + const isRemoteUrl = /^(https?:|blob:|data:)/i.test(videoUrl); + + if (!isRemoteUrl && window.electronAPI?.readBinaryFile) { + const result = await window.electronAPI.readBinaryFile(videoUrl); + if (!result.success || !result.data) { + throw new Error(result.message || result.error || "Failed to read source video"); + } + + const filename = (result.path || videoUrl).split(/[\\/]/).pop() || "video"; + const blob = new Blob([result.data]); + return { + blob, + file: new File([blob], filename, { type: blob.type || "application/octet-stream" }), + }; + } + const response = await fetch(videoUrl); + if (!response.ok) { + throw new Error(`Failed to fetch source video: ${response.status} ${response.statusText}`); + } const blob = await response.blob(); const filename = videoUrl.split("/").pop() || "video"; - const file = new File([blob], filename, { type: blob.type }); + return { + blob, + file: new File([blob], filename, { type: blob.type }), + }; + } + + async loadMetadata(videoUrl: string): Promise { + const { file } = await this.loadSourceFile(videoUrl); // Relative URL so it resolves correctly in both dev (http) and packaged (file://) builds const wasmUrl = new URL("./wasm/web-demuxer.wasm", window.location.href).href;