From 012bec21a7abfcf2d2c75b5d69870ccc16ee6ec4 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Mon, 6 Apr 2026 15:23:06 +1000
Subject: [PATCH] Prefer WebCodecs auto export and harden Pixi fallback
---
src/components/video-editor/VideoEditor.tsx | 45 +++++-
src/components/video-editor/VideoPlayback.tsx | 2 +
src/lib/exporter/backendPolicy.test.ts | 19 +++
src/lib/exporter/backendPolicy.ts | 27 ++++
src/lib/exporter/frameRenderer.ts | 1 +
src/lib/exporter/modernFrameRenderer.ts | 1 +
src/lib/exporter/modernVideoExporter.ts | 130 +++++++++++++-----
7 files changed, 188 insertions(+), 37 deletions(-)
create mode 100644 src/lib/exporter/backendPolicy.test.ts
create mode 100644 src/lib/exporter/backendPolicy.ts
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[] {