Merge pull request #461 from webadderallorg/fix/issue458-lightning-fallback-error

fix(export): fallback native Lightning errors
This commit is contained in:
Phạm Thị Minh Hồng
2026-05-09 08:14:33 +07:00
committed by GitHub
2 changed files with 136 additions and 3 deletions
@@ -0,0 +1,120 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => {
const videoInfo = {
width: 1920,
height: 1080,
duration: 1,
streamDuration: 1,
frameRate: 30,
codec: "h264",
hasAudio: false,
audioCodec: null,
audioSampleRate: null,
};
return {
videoInfo,
streamingDecoderDestroy: vi.fn(),
streamingDecoderCancel: vi.fn(),
streamingDecoderDecodeAll: vi.fn(async () => {}),
streamingDecoderGetDemuxer: vi.fn(() => null),
streamingDecoderGetEffectiveDuration: vi.fn(() => 0),
streamingDecoderLoadMetadata: vi.fn(async () => videoInfo),
frameRendererDestroy: vi.fn(),
frameRendererGetBackend: vi.fn(() => "webgl"),
frameRendererInitialize: vi.fn(async () => {}),
muxerDestroy: vi.fn(),
muxerFinalize: vi.fn(async () => ({
mode: "buffer" as const,
blob: new Blob([], { type: "video/mp4" }),
})),
muxerInitialize: vi.fn(async () => {}),
};
});
vi.mock("./streamingDecoder", () => ({
StreamingVideoDecoder: vi.fn().mockImplementation(function () {
return {
cancel: mocks.streamingDecoderCancel,
decodeAll: mocks.streamingDecoderDecodeAll,
destroy: mocks.streamingDecoderDestroy,
getDemuxer: mocks.streamingDecoderGetDemuxer,
getEffectiveDuration: mocks.streamingDecoderGetEffectiveDuration,
loadMetadata: mocks.streamingDecoderLoadMetadata,
};
}),
}));
vi.mock("./modernFrameRenderer", () => ({
FrameRenderer: vi.fn().mockImplementation(function () {
return {
destroy: mocks.frameRendererDestroy,
getRendererBackend: mocks.frameRendererGetBackend,
initialize: mocks.frameRendererInitialize,
};
}),
}));
vi.mock("./muxer", () => ({
VideoMuxer: vi.fn().mockImplementation(function () {
return {
destroy: mocks.muxerDestroy,
finalize: mocks.muxerFinalize,
initialize: mocks.muxerInitialize,
};
}),
}));
describe("ModernVideoExporter native fallback routing", () => {
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it("falls back to WebCodecs instead of surfacing a native error when Breeze is unavailable", async () => {
const { ModernVideoExporter } = await import("./modernVideoExporter");
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: "breeze",
} 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>;
lastNativeExportError: string | null;
};
vi.spyOn(exporter, "loadNativeStaticLayoutVideoInfo").mockResolvedValue(mocks.videoInfo);
vi.spyOn(exporter, "tryExportNativeStaticLayout").mockResolvedValue(null);
vi.spyOn(exporter, "tryStartNativeVideoExport").mockImplementation(async () => {
exporter.lastNativeExportError = "Breeze native encoder unavailable";
return false;
});
const initializeEncoder = vi.spyOn(exporter, "initializeEncoder").mockResolvedValue({
codec: "avc1.640034",
hardwareAcceleration: "prefer-hardware",
});
const result = await exporter.export();
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
expect(result.blob).toBeInstanceOf(Blob);
expect(initializeEncoder).toHaveBeenCalledTimes(1);
expect(mocks.muxerFinalize).toHaveBeenCalledTimes(1);
}, 15_000);
});
+16 -3
View File
@@ -346,7 +346,7 @@ export class ModernVideoExporter {
const runtimePlatform = this.getRuntimePlatform();
let useNativeEncoder = false;
let triedNativeStaticLayoutWithProbe = false;
const shouldDeferNativeEncoderStart = backendPreference === "breeze";
let shouldDeferNativeEncoderStart = backendPreference === "breeze";
this.lastNativeExportError = null;
let stageStartedAt = this.getNowMs();
@@ -512,10 +512,23 @@ export class ModernVideoExporter {
useNativeEncoder = await this.tryStartNativeVideoExport();
this.nativeSessionStartTimeMs = this.getNowMs() - stageStartedAt;
if (!useNativeEncoder) {
throw new Error(
const nativeFailure =
this.lastNativeExportError ??
`${NATIVE_EXPORT_ENGINE_NAME} export is unavailable for this output profile on this system.`,
`${NATIVE_EXPORT_ENGINE_NAME} export is unavailable for this output profile on this system.`;
console.warn(
`[VideoExporter] ${NATIVE_EXPORT_ENGINE_NAME} native export unavailable after static-layout fallback; falling back to WebCodecs.`,
nativeFailure,
);
shouldDeferNativeEncoderStart = false;
this.backpressureProfile = getExportBackpressureProfile({
encodeBackend: "webcodecs",
width: this.config.width,
height: this.config.height,
frameRate: this.config.frameRate,
encodingMode: this.config.encodingMode,
});
this.maxNativeWriteInFlight = 1;
await this.initializeEncoder();
}
}