mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
Prefer WebCodecs auto export and harden Pixi fallback
This commit is contained in:
@@ -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 ? (
|
||||
<p className="mt-1 text-[11px] text-slate-500">{exportRenderSpeedLabel}</p>
|
||||
) : null}
|
||||
{exportRuntimeLabel ? (
|
||||
<p className="mt-1 text-[11px] text-slate-500">Path: {exportRuntimeLabel}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : exportError ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-[#17171a] p-4 text-slate-200 shadow-2xl">
|
||||
<p className="text-sm font-semibold text-white">
|
||||
{t("editor.exportStatus.issue", "Export issue")}
|
||||
</p>
|
||||
{exportRuntimeLabel ? (
|
||||
<p className="mt-1 text-[11px] text-slate-500">Path: {exportRuntimeLabel}</p>
|
||||
) : null}
|
||||
<p className="mt-1 whitespace-pre-line text-xs leading-relaxed text-slate-400">{exportError}</p>
|
||||
<div className="mt-4 flex gap-2">
|
||||
{hasPendingExportSave ? (
|
||||
@@ -4009,6 +4049,9 @@ export default function VideoEditor() {
|
||||
"Your file was saved successfully.",
|
||||
)}
|
||||
</p>
|
||||
{exportRuntimeLabel ? (
|
||||
<p className="mt-1 text-[11px] text-slate-500">Path: {exportRuntimeLabel}</p>
|
||||
) : null}
|
||||
<p className="mt-3 truncate text-xs text-slate-500">
|
||||
{exportedFilePath.split("/").pop()}
|
||||
</p>
|
||||
|
||||
@@ -1044,8 +1044,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
height: container.clientHeight,
|
||||
backgroundAlpha: 0,
|
||||
antialias: true,
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
resolution: window.devicePixelRatio || 1,
|
||||
autoDensity: true,
|
||||
preference: "webgl",
|
||||
});
|
||||
|
||||
app.ticker.maxFPS = 60;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -210,6 +210,7 @@ export class FrameRenderer {
|
||||
height: this.config.height,
|
||||
backgroundAlpha: 0,
|
||||
antialias: true,
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
resolution: 1,
|
||||
autoDensity: true,
|
||||
autoStart: false,
|
||||
|
||||
@@ -470,6 +470,7 @@ export class FrameRenderer {
|
||||
height: this.config.height,
|
||||
backgroundAlpha: 0,
|
||||
antialias: true,
|
||||
failIfMajorPerformanceCaveat: false,
|
||||
resolution: 1,
|
||||
autoDensity: true,
|
||||
autoStart: false,
|
||||
|
||||
@@ -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<LightningRuntimePlatform> {
|
||||
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[] {
|
||||
|
||||
Reference in New Issue
Block a user