diff --git a/src/lib/exporter/modernVideoExporter.fallback.test.ts b/src/lib/exporter/modernVideoExporter.fallback.test.ts index 5431f7d7..a4a335d9 100644 --- a/src/lib/exporter/modernVideoExporter.fallback.test.ts +++ b/src/lib/exporter/modernVideoExporter.fallback.test.ts @@ -337,4 +337,83 @@ describe("ModernVideoExporter native fallback routing", () => { }), ); }); + + it("uses the stable Lightning render backend by default", async () => { + const { ModernVideoExporter } = await import("./modernVideoExporter"); + const { FrameRenderer } = await import("./modernFrameRenderer"); + mocks.streamingDecoderGetEffectiveDuration.mockReturnValue(1); + + 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 }, + backendPreference: "webcodecs", + } as never) as unknown as { + export: () => Promise<{ success: boolean; blob?: Blob; error?: string }>; + initializeEncoder: () => Promise; + }; + + vi.spyOn(exporter, "initializeEncoder").mockResolvedValue({ + codec: "avc1.640034", + hardwareAcceleration: "prefer-hardware", + }); + + const result = await exporter.export(); + + expect(result.success).toBe(true); + expect(FrameRenderer).toHaveBeenCalledWith( + expect.objectContaining({ + preferredRenderBackend: "webgl", + }), + ); + }); + + it("allows an explicit Lightning render backend override", async () => { + const { ModernVideoExporter } = await import("./modernVideoExporter"); + const { FrameRenderer } = await import("./modernFrameRenderer"); + mocks.streamingDecoderGetEffectiveDuration.mockReturnValue(1); + + 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 }, + backendPreference: "webcodecs", + preferredRenderBackend: "webgpu", + } as never) as unknown as { + export: () => Promise<{ success: boolean; blob?: Blob; error?: string }>; + initializeEncoder: () => Promise; + }; + + vi.spyOn(exporter, "initializeEncoder").mockResolvedValue({ + codec: "avc1.640034", + hardwareAcceleration: "prefer-hardware", + }); + + const result = await exporter.export(); + + expect(result.success).toBe(true); + expect(FrameRenderer).toHaveBeenCalledWith( + expect.objectContaining({ + preferredRenderBackend: "webgpu", + }), + ); + }); }); diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index 5b65882b..47bacf0f 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -51,6 +51,7 @@ import { } from "@/lib/wallpapers"; import { AudioProcessor, isAacAudioEncodingSupported } from "./audioEncoder"; import { + getDefaultLightningRenderBackend, normalizeLightningRuntimePlatform, shouldPreferNativeAutoBackend, shouldPreferNativeStaticLayoutBeforeBreeze, @@ -585,7 +586,8 @@ export class ModernVideoExporter { this.renderer = new ModernFrameRenderer({ width: this.config.width, height: this.config.height, - preferredRenderBackend: undefined, + preferredRenderBackend: + this.config.preferredRenderBackend ?? getDefaultLightningRenderBackend(), wallpaper: this.config.wallpaper, zoomRegions: this.config.zoomRegions, showShadow: this.config.showShadow,