Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/lib/exporter/modernVideoExporter.fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +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 () => {
vi.stubGlobal("navigator", {
platform: "Win32",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
});

const { ModernVideoExporter } = await import("./modernVideoExporter");
const staticLayoutResult = {
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 }>;
initializeEncoder: () => Promise<unknown>;
loadNativeStaticLayoutVideoInfo: () => Promise<unknown>;
tryExportNativeStaticLayout: () => Promise<unknown>;
tryStartNativeVideoExport: () => Promise<boolean>;
};

const initializeEncoder = vi.spyOn(exporter, "initializeEncoder").mockResolvedValue({
codec: "avc1.640034",
hardwareAcceleration: "prefer-hardware",
});
const loadNativeStaticLayoutVideoInfo = vi
.spyOn(exporter, "loadNativeStaticLayoutVideoInfo")
.mockResolvedValue(mocks.videoInfo);
const tryExportNativeStaticLayout = vi
.spyOn(exporter, "tryExportNativeStaticLayout")
.mockResolvedValue(staticLayoutResult);
const tryStartNativeVideoExport = vi
.spyOn(exporter, "tryStartNativeVideoExport")
.mockResolvedValue(true);

const result = await exporter.export();

expect(result).toBe(staticLayoutResult);
expect(loadNativeStaticLayoutVideoInfo).toHaveBeenCalledTimes(1);
expect(tryExportNativeStaticLayout).toHaveBeenCalledTimes(1);
expect(tryStartNativeVideoExport).not.toHaveBeenCalled();
expect(initializeEncoder).not.toHaveBeenCalled();
expect(mocks.streamingDecoderLoadMetadata).not.toHaveBeenCalled();
}, 15_000);

it("retries the main decode path once with a readable file-backed source", async () => {
const { ModernVideoExporter } = await import("./modernVideoExporter");
mocks.streamingDecoderGetEffectiveDuration.mockReturnValue(1);
Expand Down
19 changes: 12 additions & 7 deletions src/lib/exporter/modernVideoExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import type {
CursorStyle,
CursorTelemetryPoint,
Padding,
SpeedRegion,
SourceAudioTrackSettings,
SpeedRegion,
TrimRegion,
WebcamOverlaySettings,
ZoomMotionBlurTuning,
Expand Down Expand Up @@ -49,7 +49,11 @@ import {
isVideoWallpaperSource,
} from "@/lib/wallpapers";
import { AudioProcessor, isAacAudioEncodingSupported } from "./audioEncoder";
import { normalizeLightningRuntimePlatform, shouldPreferNativeAutoBackend } from "./backendPolicy";
import {
normalizeLightningRuntimePlatform,
shouldPreferNativeAutoBackend,
shouldPreferNativeStaticLayoutBeforeBreeze,
} from "./backendPolicy";
import { buildEditedTrackSourceSegments, classifyEditedTrackStrategy } from "./editedTrackStrategy";
import {
type ExportBackpressureProfile,
Expand Down Expand Up @@ -376,14 +380,15 @@ export class ModernVideoExporter {
const runtimePlatform = this.getRuntimePlatform();
let useNativeEncoder = false;
let triedNativeStaticLayoutWithProbe = false;
let shouldDeferNativeEncoderStart = backendPreference === "breeze";
let shouldDeferNativeEncoderStart =
backendPreference === "breeze" ||
shouldPreferNativeStaticLayoutBeforeBreeze(runtimePlatform, backendPreference);
this.lastNativeExportError = null;

let stageStartedAt = this.getNowMs();
if (backendPreference === "breeze") {
// Defer the streaming native encoder until after metadata is known.
// Static-layout exports can then use the faster Windows D3D compositor
// instead of unnecessarily rendering every frame through JS first.
if (shouldDeferNativeEncoderStart) {
// Defer the streaming native encoder until after metadata is known so
// static-layout exports can use the fastest compatible compositor first.
} else if (
backendPreference === "auto" &&
shouldPreferNativeAutoBackend(runtimePlatform)
Expand Down