From 7ba7f70e26895fe3a2b78dad1873a6788d57cdc4 Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Sun, 24 May 2026 19:23:23 +0700 Subject: [PATCH] fix(export): keep Windows auto off static layout first --- src/lib/exporter/backendPolicy.test.ts | 16 ++--- src/lib/exporter/backendPolicy.ts | 12 +++- .../modernVideoExporter.fallback.test.ts | 60 ++++++++++++++++++- src/lib/exporter/modernVideoExporter.ts | 11 ++-- 4 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/lib/exporter/backendPolicy.test.ts b/src/lib/exporter/backendPolicy.test.ts index e2c3827f..6d375968 100644 --- a/src/lib/exporter/backendPolicy.test.ts +++ b/src/lib/exporter/backendPolicy.test.ts @@ -27,8 +27,8 @@ describe("backendPolicy", () => { expect(getDefaultLightningRenderBackend()).toBe("webgl"); }); - it("puts visually compatible Windows auto exports on native static layout before Breeze", () => { - expect(shouldPreferNativeStaticLayoutBeforeBreeze("win32", "auto")).toBe(true); + it("keeps Windows auto exports on the streaming route by default", () => { + expect(shouldPreferNativeStaticLayoutBeforeBreeze("win32", "auto")).toBe(false); expect(shouldPreferNativeStaticLayoutBeforeBreeze("darwin", "auto")).toBe(false); expect( @@ -38,16 +38,16 @@ describe("backendPolicy", () => { nativeStaticLayoutAvailable: true, }), ).toMatchObject({ - selectedRoute: "native-static-layout", + selectedRoute: "breeze-stream", decisions: [ - { route: "native-static-layout", status: "selected" }, - { route: "breeze-stream", status: "fallback" }, + { route: "native-static-layout", status: "rejected" }, + { route: "breeze-stream", status: "selected" }, { route: "webcodecs", status: "fallback" }, ], }); }); - it("documents the Breeze fallback when Windows static native is rejected", () => { + it("ignores native static skip reasons for Windows auto routing", () => { expect( planLightningExportRoutes({ backendPreference: "auto", @@ -61,12 +61,12 @@ describe("backendPolicy", () => { { route: "native-static-layout", status: "rejected", - reasons: ["unsupported-frame-overlay"], + reasons: ["platform-does-not-use-native-static-layout"], }, { route: "breeze-stream", status: "selected", - reasons: ["windows-native-static-fallback"], + reasons: ["platform-prefers-native-streaming"], }, { route: "webcodecs", diff --git a/src/lib/exporter/backendPolicy.ts b/src/lib/exporter/backendPolicy.ts index ecfaa11d..3c7f9847 100644 --- a/src/lib/exporter/backendPolicy.ts +++ b/src/lib/exporter/backendPolicy.ts @@ -41,11 +41,19 @@ export interface LightningExportRoutePlan { decisions: LightningExportRouteDecision[]; } +// Disabled while stabilizing v1.3.0: the Windows auto static-layout probe can +// leave Lightning at "Preparing export..." before the stable streaming fallback. +const WINDOWS_AUTO_STATIC_LAYOUT_FIRST_ENABLED = false; + export function shouldPreferNativeStaticLayoutBeforeBreeze( platform: LightningRuntimePlatform, backendPreference: ExportBackendPreference, ): boolean { - return backendPreference === "auto" && platform === "win32"; + return ( + WINDOWS_AUTO_STATIC_LAYOUT_FIRST_ENABLED && + backendPreference === "auto" && + platform === "win32" + ); } export function planLightningExportRoutes(options: { @@ -92,7 +100,7 @@ export function planLightningExportRoutes(options: { reasons: [ options.backendPreference === "breeze" ? "user-selected-breeze" - : "windows-native-static-fallback", + : "native-static-layout-not-auto-default", ], }); decisions.push({ diff --git a/src/lib/exporter/modernVideoExporter.fallback.test.ts b/src/lib/exporter/modernVideoExporter.fallback.test.ts index 7b491b00..ccc1d8db 100644 --- a/src/lib/exporter/modernVideoExporter.fallback.test.ts +++ b/src/lib/exporter/modernVideoExporter.fallback.test.ts @@ -118,7 +118,64 @@ describe("ModernVideoExporter native fallback routing", () => { expect(mocks.muxerFinalize).toHaveBeenCalledTimes(1); }, 15_000); - it("tries Windows auto static-layout before starting the streaming native encoder", async () => { + it("keeps Windows auto exports on the streaming native route before static layout", async () => { + vi.stubGlobal("navigator", { + platform: "Win32", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + }); + + const { ModernVideoExporter } = await import("./modernVideoExporter"); + const nativeResult = { + success: true, + blob: new Blob([], { type: "video/mp4" }), + }; + const exporter = new ModernVideoExporter({ + videoUrl: "file:///recording.mp4", + width: 1920, + height: 1080, + frameRate: 30, + bitrate: 8_000_000, + wallpaper: "#101010", + padding: 0, + borderRadius: 0, + backgroundBlur: 0, + shadowIntensity: 0, + showShadow: false, + cropRegion: { x: 0, y: 0, width: 1, height: 1 }, + experimentalNativeExport: true, + backendPreference: "auto", + } as never) as unknown as { + export: () => Promise<{ success: boolean; blob?: Blob; error?: string }>; + finishNativeVideoExport: () => Promise; + loadNativeStaticLayoutVideoInfo: () => Promise; + tryExportNativeStaticLayout: () => Promise; + tryStartNativeVideoExport: () => Promise; + }; + + const loadNativeStaticLayoutVideoInfo = vi.spyOn( + exporter, + "loadNativeStaticLayoutVideoInfo", + ); + const tryExportNativeStaticLayout = vi.spyOn(exporter, "tryExportNativeStaticLayout"); + const tryStartNativeVideoExport = vi + .spyOn(exporter, "tryStartNativeVideoExport") + .mockResolvedValue(true); + const finishNativeVideoExport = vi + .spyOn(exporter, "finishNativeVideoExport") + .mockResolvedValue(nativeResult); + + const result = await exporter.export(); + + expect(result.success).toBe(true); + expect(result.blob).toBe(nativeResult.blob); + expect(tryStartNativeVideoExport).toHaveBeenCalledTimes(1); + expect(loadNativeStaticLayoutVideoInfo).not.toHaveBeenCalled(); + expect(tryExportNativeStaticLayout).not.toHaveBeenCalled(); + expect(mocks.streamingDecoderLoadMetadata).toHaveBeenCalledTimes(1); + expect(finishNativeVideoExport).toHaveBeenCalledTimes(1); + }, 15_000); + + it("tries Windows auto static-layout first when NVIDIA CUDA is opted in", async () => { vi.stubGlobal("navigator", { platform: "Win32", userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", @@ -143,6 +200,7 @@ describe("ModernVideoExporter native fallback routing", () => { showShadow: false, cropRegion: { x: 0, y: 0, width: 1, height: 1 }, experimentalNativeExport: true, + experimentalNvidiaCudaExport: true, backendPreference: "auto", } as never) as unknown as { export: () => Promise<{ success: boolean; blob?: Blob; error?: string }>; diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index c1f01a54..1cb17bee 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -382,8 +382,12 @@ export class ModernVideoExporter { const runtimePlatform = this.getRuntimePlatform(); let useNativeEncoder = false; let triedNativeStaticLayoutWithProbe = false; + const shouldTryNativeStaticLayout = + backendPreference === "breeze" || + this.config.experimentalNvidiaCudaExport === true; let shouldDeferNativeEncoderStart = backendPreference === "breeze" || + this.config.experimentalNvidiaCudaExport === true || shouldPreferNativeStaticLayoutBeforeBreeze(runtimePlatform, backendPreference); this.lastNativeExportError = null; @@ -478,10 +482,7 @@ export class ModernVideoExporter { maxInFlightNativeWrites: this.maxNativeWriteInFlight, }); - if ( - (backendPreference === "auto" || backendPreference === "breeze") && - !useNativeEncoder - ) { + if (shouldTryNativeStaticLayout && !useNativeEncoder) { const nativeVideoInfo = await this.loadNativeStaticLayoutVideoInfo(); if (nativeVideoInfo) { triedNativeStaticLayoutWithProbe = true; @@ -530,7 +531,7 @@ export class ModernVideoExporter { const totalFrames = Math.ceil(effectiveDuration * this.config.frameRate); if ( - (backendPreference === "auto" || backendPreference === "breeze") && + shouldTryNativeStaticLayout && !useNativeEncoder && !triedNativeStaticLayoutWithProbe ) {