diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 4254b569..7dc6d812 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -226,57 +226,6 @@ type SmokeExportConfig = { fps?: ExportMp4FrameRate; }; -const EXPORT_BLOB_STREAM_CHUNK_BYTES = 16 * 1024 * 1024; - -async function streamExportBlobToTempFile(blob: Blob, extension: string): Promise { - if ( - typeof window === "undefined" || - !window.electronAPI?.openExportStream || - !window.electronAPI?.writeExportStreamChunk || - !window.electronAPI?.closeExportStream - ) { - return null; - } - - const openResult = await window.electronAPI.openExportStream({ extension }); - if (!openResult.success || !openResult.streamId || !openResult.tempPath) { - throw new Error(openResult.error || "Failed to open export stream"); - } - - const { streamId } = openResult; - let position = 0; - - try { - while (position < blob.size) { - const chunk = blob.slice(position, position + EXPORT_BLOB_STREAM_CHUNK_BYTES); - const chunkBuffer = await chunk.arrayBuffer(); - const writeResult = await window.electronAPI.writeExportStreamChunk( - streamId, - position, - new Uint8Array(chunkBuffer), - ); - if (!writeResult.success) { - throw new Error(writeResult.error || "Failed to write export stream chunk"); - } - position += chunkBuffer.byteLength; - } - - const closeResult = await window.electronAPI.closeExportStream(streamId); - if (!closeResult.success || !closeResult.tempPath) { - throw new Error(closeResult.error || "Failed to close export stream"); - } - - return closeResult.tempPath; - } catch (error) { - try { - await window.electronAPI.closeExportStream(streamId, { abort: true }); - } catch { - // Best-effort cleanup; preserve the original error below. - } - throw error; - } -} - type SaveProjectOptions = { silent?: boolean; remountPreviewAfterSave?: boolean; @@ -1056,43 +1005,6 @@ export default function VideoEditor() { return run; }, []); - const saveBlobExport = useCallback( - async (blob: Blob, fileName: string, outputPath: string | null = null) => { - const extension = fileName.split(".").pop()?.toLowerCase() || "bin"; - - try { - const tempFilePath = await streamExportBlobToTempFile(blob, extension); - if (tempFilePath) { - return { - saveResult: await window.electronAPI.finalizeExportedVideo({ - tempPath: tempFilePath, - fileName, - outputPath, - }), - pendingSave: { - fileName, - tempFilePath, - } satisfies PendingExportSave, - }; - } - } catch (error) { - console.warn("[export] Falling back to in-memory blob save", error); - } - - const arrayBuffer = await blob.arrayBuffer(); - return { - saveResult: outputPath - ? await window.electronAPI.writeExportedVideoToPath(arrayBuffer, outputPath) - : await window.electronAPI.saveExportedVideo(arrayBuffer, fileName), - pendingSave: { - fileName, - arrayBuffer, - } satisfies PendingExportSave, - }; - }, - [], - ); - useEffect(() => { return () => { exporterRef.current?.cancel(); @@ -1486,7 +1398,6 @@ export default function VideoEditor() { borderRadius, padding, frame, - cropRegion, webcam, zoomRegions, trimRegions, @@ -1535,7 +1446,6 @@ export default function VideoEditor() { cursorSway, borderRadius, padding, - cropRegion, webcam, zoomRegions, trimRegions, @@ -4149,18 +4059,21 @@ export default function VideoEditor() { const result = await gifExporter.export(); if (result.success && result.blob) { + const arrayBuffer = await result.blob.arrayBuffer(); const timestamp = Date.now(); const fileName = `export-${timestamp}.gif`; markExportAsSaving(); - const { saveResult, pendingSave } = await saveBlobExport( - result.blob, - fileName, - smokeExportConfig.enabled ? smokeExportConfig.outputPath : null, - ); + const saveResult = + smokeExportConfig.enabled && smokeExportConfig.outputPath + ? await window.electronAPI.writeExportedVideoToPath( + arrayBuffer, + smokeExportConfig.outputPath, + ) + : await window.electronAPI.saveExportedVideo(arrayBuffer, fileName); if (saveResult.canceled) { - pendingExportSaveRef.current = pendingSave; + pendingExportSaveRef.current = { arrayBuffer, fileName }; setHasPendingExportSave(true); setExportError( "Save dialog canceled. Click Save Again to save without re-rendering.", @@ -4360,16 +4273,20 @@ export default function VideoEditor() { }); pendingOnCancel = { fileName, tempFilePath: result.tempFilePath }; } else if (result.blob) { - // Legacy fallback: some export paths still surface a Blob, but in - // Electron we stream it into a temp file first so save/finalize - // never requires a giant renderer ArrayBuffer. - const blobSave = await saveBlobExport( - result.blob, - fileName, - smokeExportConfig.enabled ? smokeExportConfig.outputPath : null, - ); - saveResult = blobSave.saveResult; - pendingOnCancel = blobSave.pendingSave; + // Legacy fallback: small exports may still surface a Blob (GIF, + // smoke tests in non-Electron environments, etc.). + const arrayBuffer = await result.blob.arrayBuffer(); + saveResult = + smokeExportConfig.enabled && smokeExportConfig.outputPath + ? await window.electronAPI.writeExportedVideoToPath( + arrayBuffer, + smokeExportConfig.outputPath, + ) + : await window.electronAPI.saveExportedVideo( + arrayBuffer, + fileName, + ); + pendingOnCancel = { fileName, arrayBuffer }; } else { saveResult = { success: false, message: "Export produced no output" }; pendingOnCancel = { fileName };