diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index d79d864d..9eadd382 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -815,6 +815,9 @@ export default function VideoEditor() { percentage: 100, estimatedTimeRemaining: 0, renderFps: previous?.renderFps, + renderBackend: previous?.renderBackend, + encodeBackend: previous?.encodeBackend, + encoderName: previous?.encoderName, phase: "saving", })); }, []); @@ -3463,7 +3466,6 @@ export default function VideoEditor() { setIsExporting(false); exporterRef.current = null; setShowExportDropdown(keepExportDialogOpen); - setExportProgress(null); remountPreview(); } }, @@ -3755,6 +3757,38 @@ export default function VideoEditor() { fps: exportProgress.renderFps.toFixed(1), }) : null; + const exportRuntimeLabel = useMemo(() => { + const renderBackend = exportProgress?.renderBackend; + const encodeBackend = exportProgress?.encodeBackend; + const encoderName = exportProgress?.encoderName; + + if (!renderBackend && !encodeBackend && !encoderName) { + return null; + } + + const rendererLabel = + renderBackend === "webgpu" + ? "WebGPU" + : renderBackend === "webgl" + ? "WebGL" + : null; + const encoderLabel = + encodeBackend === "ffmpeg" + ? "Breeze" + : encodeBackend === "webcodecs" + ? "WebCodecs" + : null; + const pathLabel = + rendererLabel && encoderLabel + ? `${rendererLabel} + ${encoderLabel}` + : rendererLabel ?? encoderLabel; + + if (!pathLabel) { + return encoderName ?? null; + } + + return encoderName ? `${pathLabel} (${encoderName})` : pathLabel; + }, [exportProgress]); const exportPercentLabel = exportProgress ? isExportSaving ? t("editor.exportStatus.saving", "Opening save dialog...") @@ -3971,12 +4005,18 @@ export default function VideoEditor() { {exportRenderSpeedLabel ? (

{exportRenderSpeedLabel}

) : null} + {exportRuntimeLabel ? ( +

Path: {exportRuntimeLabel}

+ ) : null} ) : exportError ? (

{t("editor.exportStatus.issue", "Export issue")}

+ {exportRuntimeLabel ? ( +

Path: {exportRuntimeLabel}

+ ) : null}

{exportError}

{hasPendingExportSave ? ( @@ -4009,6 +4049,9 @@ export default function VideoEditor() { "Your file was saved successfully.", )}

+ {exportRuntimeLabel ? ( +

Path: {exportRuntimeLabel}

+ ) : null}

{exportedFilePath.split("/").pop()}

diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 9c9691fa..e0a1d584 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1044,8 +1044,10 @@ const VideoPlayback = forwardRef( height: container.clientHeight, backgroundAlpha: 0, antialias: true, + failIfMajorPerformanceCaveat: false, resolution: window.devicePixelRatio || 1, autoDensity: true, + preference: "webgl", }); app.ticker.maxFPS = 60; diff --git a/src/lib/exporter/backendPolicy.test.ts b/src/lib/exporter/backendPolicy.test.ts new file mode 100644 index 00000000..84b69a4b --- /dev/null +++ b/src/lib/exporter/backendPolicy.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { normalizeLightningRuntimePlatform, shouldPreferNativeAutoBackend } from "./backendPolicy"; + +describe("backendPolicy", () => { + it("normalizes common platform hints", () => { + expect(normalizeLightningRuntimePlatform("Win32")).toBe("win32"); + expect(normalizeLightningRuntimePlatform("Linux x86_64")).toBe("linux"); + expect(normalizeLightningRuntimePlatform("MacIntel")).toBe("darwin"); + expect(normalizeLightningRuntimePlatform("unknown")).toBe("unknown"); + }); + + it("keeps auto backend WebCodecs-first on every platform", () => { + expect(shouldPreferNativeAutoBackend("win32")).toBe(false); + expect(shouldPreferNativeAutoBackend("linux")).toBe(false); + expect(shouldPreferNativeAutoBackend("darwin")).toBe(false); + expect(shouldPreferNativeAutoBackend("unknown")).toBe(false); + }); +}); diff --git a/src/lib/exporter/backendPolicy.ts b/src/lib/exporter/backendPolicy.ts new file mode 100644 index 00000000..dcc3c9c4 --- /dev/null +++ b/src/lib/exporter/backendPolicy.ts @@ -0,0 +1,27 @@ +export type LightningRuntimePlatform = "darwin" | "win32" | "linux" | "unknown"; + +export function normalizeLightningRuntimePlatform( + platformHint: string | null | undefined, +): LightningRuntimePlatform { + if (!platformHint) { + return "unknown"; + } + + if (/win/i.test(platformHint)) { + return "win32"; + } + + if (/linux/i.test(platformHint)) { + return "linux"; + } + + if (/mac|iphone|ipad|ipod/i.test(platformHint)) { + return "darwin"; + } + + return "unknown"; +} + +export function shouldPreferNativeAutoBackend(_platform: LightningRuntimePlatform): boolean { + return false; +} diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 1b2b0920..9263c150 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -210,6 +210,7 @@ export class FrameRenderer { height: this.config.height, backgroundAlpha: 0, antialias: true, + failIfMajorPerformanceCaveat: false, resolution: 1, autoDensity: true, autoStart: false, diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 52aaed7c..3ed26311 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -470,6 +470,7 @@ export class FrameRenderer { height: this.config.height, backgroundAlpha: 0, antialias: true, + failIfMajorPerformanceCaveat: false, resolution: 1, autoDensity: true, autoStart: false, diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 953c5eda..7d939944 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -13,6 +13,11 @@ import type { ZoomTransitionEasing, } from "@/components/video-editor/types"; import { AudioProcessor } from "./audioEncoder"; +import { + normalizeLightningRuntimePlatform, + shouldPreferNativeAutoBackend, + type LightningRuntimePlatform, +} from "./backendPolicy"; import { FrameRenderer as ModernFrameRenderer } from "./modernFrameRenderer"; import { getOrderedSupportedMp4EncoderCandidates, @@ -158,6 +163,10 @@ export class ModernVideoExporter { let stageStartedAt = this.getNowMs(); const backendPreference = this.config.backendPreference ?? "auto"; + const runtimePlatform = + backendPreference === "auto" ? await this.getRuntimePlatform() : "unknown"; + const preferNativeFirstInAuto = + backendPreference === "auto" && shouldPreferNativeAutoBackend(runtimePlatform); let useNativeEncoder = false; this.lastNativeExportError = null; @@ -172,39 +181,70 @@ export class ModernVideoExporter { ); } } else { - try { - const configuredWebCodecsPath = await this.initializeEncoder(); - if ( - backendPreference === "auto" && - configuredWebCodecsPath.hardwareAcceleration === "prefer-software" - ) { - console.warn( - "[VideoExporter] Auto backend resolved to a software WebCodecs encoder; trying Breeze native export instead.", - ); - stageStartedAt = this.getNowMs(); - useNativeEncoder = await this.tryStartNativeVideoExport(); - this.nativeSessionStartTimeMs = this.getNowMs() - stageStartedAt; - if (useNativeEncoder) { - this.disposeEncoder(); - } - } - } catch (error) { - const webCodecsError = error instanceof Error ? error : new Error(String(error)); - if (backendPreference === "webcodecs") { - throw webCodecsError; - } - console.warn( - `[VideoExporter] WebCodecs encoder unavailable, trying ${NATIVE_EXPORT_ENGINE_NAME} native export fallback`, - webCodecsError, - ); - this.disposeEncoder(); - + if (preferNativeFirstInAuto) { stageStartedAt = this.getNowMs(); useNativeEncoder = await this.tryStartNativeVideoExport(); this.nativeSessionStartTimeMs = this.getNowMs() - stageStartedAt; + if (useNativeEncoder) { + console.log( + `[VideoExporter] Auto backend preferred ${NATIVE_EXPORT_ENGINE_NAME} first on ${runtimePlatform}; skipping WebCodecs startup.`, + ); + } else { + console.log( + `[VideoExporter] Auto backend could not start ${NATIVE_EXPORT_ENGINE_NAME} on ${runtimePlatform}; falling back to WebCodecs.`, + ); + } + } - if (!useNativeEncoder) { - throw webCodecsError; + if (!useNativeEncoder) { + try { + const configuredWebCodecsPath = await this.initializeEncoder(); + if ( + !preferNativeFirstInAuto && + backendPreference === "auto" && + configuredWebCodecsPath.hardwareAcceleration === "prefer-software" + ) { + console.warn( + "[VideoExporter] Auto backend resolved to a software WebCodecs encoder; trying Breeze native export instead.", + ); + stageStartedAt = this.getNowMs(); + useNativeEncoder = await this.tryStartNativeVideoExport(); + this.nativeSessionStartTimeMs = this.getNowMs() - stageStartedAt; + if (useNativeEncoder) { + this.disposeEncoder(); + } + } else if ( + preferNativeFirstInAuto && + backendPreference === "auto" && + configuredWebCodecsPath.hardwareAcceleration === "prefer-software" + ) { + console.warn( + `[VideoExporter] Auto backend fell back to a software WebCodecs encoder after ${NATIVE_EXPORT_ENGINE_NAME} startup was unavailable on ${runtimePlatform}.`, + ); + } + } catch (error) { + const webCodecsError = error instanceof Error ? error : new Error(String(error)); + if (backendPreference === "webcodecs") { + throw webCodecsError; + } + + if (preferNativeFirstInAuto) { + throw webCodecsError; + } + + console.warn( + `[VideoExporter] WebCodecs encoder unavailable, trying ${NATIVE_EXPORT_ENGINE_NAME} native export fallback`, + webCodecsError, + ); + this.disposeEncoder(); + + stageStartedAt = this.getNowMs(); + useNativeEncoder = await this.tryStartNativeVideoExport(); + this.nativeSessionStartTimeMs = this.getNowMs() - stageStartedAt; + + if (!useNativeEncoder) { + throw webCodecsError; + } } } } @@ -453,23 +493,41 @@ export class ModernVideoExporter { } } + private async getRuntimePlatform(): Promise { + if (typeof window !== "undefined" && window.electronAPI?.getPlatform) { + try { + return normalizeLightningRuntimePlatform(await window.electronAPI.getPlatform()); + } catch (error) { + console.warn( + "[VideoExporter] Failed to read runtime platform from Electron API; falling back to navigator hints.", + error, + ); + } + } + + if (typeof navigator === "undefined") { + return "unknown"; + } + + return normalizeLightningRuntimePlatform(navigator.platform || navigator.userAgent || ""); + } + private getPlatformLabel(): string { if (typeof navigator === "undefined") { return "Unknown"; } const platformHint = navigator.platform || navigator.userAgent || ""; - if (/Win/i.test(platformHint)) { + switch (normalizeLightningRuntimePlatform(platformHint)) { + case "win32": return "Windows"; - } - if (/Linux/i.test(platformHint)) { + case "linux": return "Linux"; - } - if (/Mac|iPhone|iPad|iPod/i.test(platformHint)) { + case "darwin": return "macOS"; + default: + return platformHint || "Unknown"; } - - return platformHint || "Unknown"; } private getLightningErrorGuidance(message: string): string[] {