From 514943c1429c93373804aba04c4762deb96348fc Mon Sep 17 00:00:00 2001 From: Davide Mendolia Date: Sat, 23 May 2026 17:26:54 +0200 Subject: [PATCH] refactor: extract loadFileAsArrayBuffer to eliminate duplicated file loading Add a single exported loadFileAsArrayBuffer in streamingDecoder.ts that owns the IPC-vs-fetch branching for local and remote video URLs. StreamingVideoDecoder.loadSourceFile and useAudioPeaks both delegate to it, removing the duplicated readBinaryFile/fetch logic. Co-Authored-By: Claude Sonnet 4.6 --- src/hooks/useAudioPeaks.ts | 5 +-- src/lib/exporter/streamingDecoder.ts | 60 +++++++++++++--------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/src/hooks/useAudioPeaks.ts b/src/hooks/useAudioPeaks.ts index 206ad1d1..00e60675 100644 --- a/src/hooks/useAudioPeaks.ts +++ b/src/hooks/useAudioPeaks.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { loadFileAsArrayBuffer } from "@/lib/exporter/streamingDecoder"; // Module-level cache keyed by URL — survives re-mounts within the same page session. const peaksCache = new Map(); @@ -67,9 +68,7 @@ export function useAudioPeaks(videoUrl?: string): Float32Array | null { (async () => { try { - const response = await fetch(videoUrl); - if (cancelled) return; - const arrayBuffer = await response.arrayBuffer(); + const arrayBuffer = await loadFileAsArrayBuffer(videoUrl); if (cancelled) return; const audioBuffer = await getAudioCtx().decodeAudioData(arrayBuffer); if (cancelled) return; diff --git a/src/lib/exporter/streamingDecoder.ts b/src/lib/exporter/streamingDecoder.ts index e8093df0..f7687233 100644 --- a/src/lib/exporter/streamingDecoder.ts +++ b/src/lib/exporter/streamingDecoder.ts @@ -137,6 +137,29 @@ export function shouldFailDecodeEndedEarly({ return true; } +/** + * Loads a video file as an ArrayBuffer, using the Electron IPC bridge for + * local paths and falling back to `fetch` for remote / blob / data URLs. + * This is the single canonical place for reading raw video bytes in the renderer. + */ +export async function loadFileAsArrayBuffer(videoUrl: string): Promise { + 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 video file"); + } + return result.data; + } + + const response = await fetch(videoUrl); + if (!response.ok) { + throw new Error(`Failed to fetch video file: ${response.status} ${response.statusText}`); + } + return response.arrayBuffer(); +} + /** Caller must close the VideoFrame after use. */ type OnFrameCallback = ( frame: VideoFrame, @@ -158,43 +181,16 @@ export class StreamingVideoDecoder { private metadata: DecodedVideoInfo | null = null; 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 this.withTimeout( - window.electronAPI.readBinaryFile(videoUrl), - SOURCE_LOAD_TIMEOUT_MS, - "Timed out while loading the source video.", - ); - 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 this.withTimeout( - fetch(videoUrl), + const buffer = await this.withTimeout( + loadFileAsArrayBuffer(videoUrl), SOURCE_LOAD_TIMEOUT_MS, "Timed out while loading the source video.", ); - if (!response.ok) { - throw new Error(`Failed to fetch source video: ${response.status} ${response.statusText}`); - } - const blob = await this.withTimeout( - response.blob(), - SOURCE_LOAD_TIMEOUT_MS, - "Timed out while reading the source video.", - ); - const filename = videoUrl.split("/").pop() || "video"; + const filename = videoUrl.split(/[\\/]/).pop() || "video"; + const blob = new Blob([buffer]); return { blob, - file: new File([blob], filename, { type: blob.type }), + file: new File([blob], filename, { type: blob.type || "application/octet-stream" }), }; }