From 31cb818dfd90b5db3e54650982a5aa60d8a64b11 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 8 May 2026 21:57:04 +0700 Subject: [PATCH] fix(export): harden background fallback cleanup --- src/components/video-editor/VideoEditor.tsx | 17 +++---- src/lib/exporter/frameRenderer.ts | 55 ++++++++++++++++----- src/lib/exporter/modernFrameRenderer.ts | 55 ++++++++++++++++----- 3 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index c0656d3f..0145f273 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -2140,13 +2140,14 @@ export default function VideoEditor() { preserveProjectPath: Boolean(path), }, ); + const sessionResult = await window.electronAPI.getCurrentRecordingSession?.(); + applySessionPresentation(sessionResult?.success ? sessionResult.session : null); } else { await window.electronAPI.setCurrentVideoPath(sourcePath, { preserveProjectPath: Boolean(path), }); + applySessionPresentation(null); } - const sessionResult = await window.electronAPI.getCurrentRecordingSession?.(); - applySessionPresentation(sessionResult?.success ? sessionResult.session : null); setWallpaper(normalizedEditor.wallpaper); setShadowIntensity(normalizedEditor.shadowIntensity); @@ -4629,13 +4630,11 @@ export default function VideoEditor() { const backendPreference = pipelineModel === "legacy" ? "webcodecs" - : useExperimentalNativeExport - ? "auto" - : smokeExportConfig.enabled - ? (smokeExportConfig.backendPreference ?? - (smokeExportConfig.useNativeExport - ? "breeze" - : "webcodecs")) + : smokeExportConfig.enabled + ? (smokeExportConfig.backendPreference ?? + (smokeExportConfig.useNativeExport ? "breeze" : "webcodecs")) + : useExperimentalNativeExport + ? "auto" : (settings.backendPreference ?? exportBackendPreference); const supportedSourceDimensions = await ensureSupportedMp4SourceDimensions(selectedMp4FrameRate); diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 026b11ef..3a01403e 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -153,6 +153,7 @@ type PixiRendererAttempt = { }; const PIXI_RENDERER_INIT_TIMEOUT_MS = 8_000; +const BACKGROUND_MEDIA_ELEMENT_READY_TIMEOUT_MS = 5_000; function isCanvasRenderer(renderer: Application): boolean { const rendererName = renderer?.renderer?.constructor?.name?.toLowerCase(); @@ -924,31 +925,61 @@ export class FrameRenderer { video.load(); const ready = await new Promise((resolve) => { - const onReady = () => { - if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) { - return; + let settled = false; + let timeoutId: ReturnType | null = null; + + function cleanup() { + if (timeoutId !== null) { + clearTimeout(timeoutId); } - cleanup(); - resolve(true); - }; - const onError = () => { - cleanup(); - resolve(false); - }; - const cleanup = () => { video.removeEventListener("loadeddata", onReady); video.removeEventListener("canplay", onReady); video.removeEventListener("error", onError); - }; + } + + function settle(value: boolean) { + if (settled) { + return; + } + settled = true; + cleanup(); + resolve(value); + } + function onReady() { + if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) { + return; + } + settle(true); + } + function onError() { + settle(false); + } video.addEventListener("loadeddata", onReady); video.addEventListener("canplay", onReady); video.addEventListener("error", onError); + timeoutId = setTimeout(() => { + console.warn( + `[FrameRenderer] Video wallpaper media element fallback did not become ready within ${BACKGROUND_MEDIA_ELEMENT_READY_TIMEOUT_MS}ms`, + ); + settle(false); + }, BACKGROUND_MEDIA_ELEMENT_READY_TIMEOUT_MS); onReady(); }); if (!ready) { console.warn(`[FrameRenderer] Failed to load video wallpaper: ${errorLabel}`); + try { + video.pause(); + video.src = ""; + video.load(); + } catch { + // Ignore media element teardown errors on failed fallback. + } + backgroundSource.revoke(); + if (this.cleanupBackgroundSource === backgroundSource.revoke) { + this.cleanupBackgroundSource = null; + } return false; } diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 372dd2a4..cf52faea 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -246,6 +246,7 @@ type PixiRendererAttempt = { const CANVAS_RENDERER_NOT_IMPLEMENTED_HINT = "CanvasRenderer is not yet implemented"; const NO_RENDERER_HINT = "no available renderer"; const PIXI_RENDERER_INIT_TIMEOUT_MS = 8_000; +const BACKGROUND_MEDIA_ELEMENT_READY_TIMEOUT_MS = 5_000; const WEBCAM_MEDIA_ELEMENT_READY_TIMEOUT_MS = 5_000; function isCanvasRenderer(application: Application): boolean { @@ -2125,31 +2126,61 @@ export class FrameRenderer { video.load(); const ready = await new Promise((resolve) => { - const onReady = () => { - if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) { - return; + let settled = false; + let timeoutId: ReturnType | null = null; + + function cleanup() { + if (timeoutId !== null) { + clearTimeout(timeoutId); } - cleanup(); - resolve(true); - }; - const onError = () => { - cleanup(); - resolve(false); - }; - const cleanup = () => { video.removeEventListener("loadeddata", onReady); video.removeEventListener("canplay", onReady); video.removeEventListener("error", onError); - }; + } + + function settle(value: boolean) { + if (settled) { + return; + } + settled = true; + cleanup(); + resolve(value); + } + function onReady() { + if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) { + return; + } + settle(true); + } + function onError() { + settle(false); + } video.addEventListener("loadeddata", onReady); video.addEventListener("canplay", onReady); video.addEventListener("error", onError); + timeoutId = setTimeout(() => { + console.warn( + `[FrameRenderer] Video wallpaper media element fallback did not become ready within ${BACKGROUND_MEDIA_ELEMENT_READY_TIMEOUT_MS}ms`, + ); + settle(false); + }, BACKGROUND_MEDIA_ELEMENT_READY_TIMEOUT_MS); onReady(); }); if (!ready) { console.warn(`[FrameRenderer] Failed to load video wallpaper: ${errorLabel}`); + try { + video.pause(); + video.src = ""; + video.load(); + } catch { + // Ignore media element teardown errors on failed fallback. + } + backgroundSource.revoke(); + if (this.cleanupBackgroundSource === backgroundSource.revoke) { + this.cleanupBackgroundSource = null; + } return false; }