diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 589b79bf..58fc0ef8 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -62,14 +62,12 @@ import { type ExportPipelineModel, type ExportProgress, type ExportQuality, - type ExportRenderBackend, type ExportSettings, FrameRenderer, GIF_SIZE_PRESETS, GifExporter, type GifFrameRate, type GifSizePreset, - isValidMp4FrameRate, ModernVideoExporter, probeSupportedMp4Dimensions, type SupportedMp4Dimensions, @@ -140,6 +138,7 @@ import { validateProjectData, } from "./projectPersistence"; import { SettingsPanel } from "./SettingsPanel"; +import { getDevOpenRecordingConfig, getSmokeExportConfig } from "./smokeExportConfig"; import { useVideoEditorAudio } from "./audio/useVideoEditorAudio"; import { APP_HEADER_ICON_BUTTON_CLASS, @@ -233,27 +232,6 @@ type CancelableExporter = { cancel(): void; }; -type SmokeExportConfig = { - enabled: boolean; - inputPath: string | null; - outputPath: string | null; - useNativeExport: boolean; - encodingMode?: ExportEncodingMode; - shadowIntensity?: number; - webcamInputPath?: string | null; - webcamShadow?: number; - webcamSize?: number; - pipelineModel?: ExportPipelineModel; - backendPreference?: ExportBackendPreference; - renderBackend?: ExportRenderBackend; - maxEncodeQueue?: number; - maxDecodeQueue?: number; - maxPendingFrames?: number; - projectPath?: string | null; - quality?: ExportQuality; - fps?: ExportMp4FrameRate; -}; - const EXPORT_BLOB_STREAM_CHUNK_BYTES = 16 * 1024 * 1024; async function streamExportBlobToTempFile(blob: Blob, extension: string): Promise { @@ -312,11 +290,6 @@ type SaveProjectOptions = { captureThumbnail?: boolean; }; -type DevOpenRecordingConfig = { - inputPath: string | null; - webcamInputPath: string | null; -}; - async function writeSmokeExportReport( outputPath: string | null, report: Record, @@ -366,108 +339,6 @@ function cloneStructured(value: T): T { return globalThis.structuredClone(value); } -function parseSmokeExportNumber(value: string | null): number | undefined { - if (value === null) { - return undefined; - } - - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; -} - -function parseSmokeExportNonNegativeNumber(value: string | null): number | undefined { - if (value === null) { - return undefined; - } - - const parsed = Number.parseFloat(value); - return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; -} - -function parseSmokeExportQuality(value: string | null): ExportQuality | undefined { - if (value === "medium" || value === "good" || value === "high" || value === "source") { - return value; - } - return undefined; -} - -function parseSmokeExportFps(value: string | null): ExportMp4FrameRate | undefined { - if (value === null) return undefined; - const parsed = Number.parseInt(value, 10); - return isValidMp4FrameRate(parsed) ? parsed : undefined; -} - -function parseSmokeRenderBackend(value: string | null): ExportRenderBackend | undefined { - return value === "webgl" || value === "webgpu" ? value : undefined; -} - -function getSmokeExportConfig(search: string): SmokeExportConfig { - const params = new URLSearchParams(search); - const enabled = params.get("smokeExport") === "1"; - - return { - enabled, - inputPath: enabled ? params.get("smokeInput") : null, - outputPath: enabled ? params.get("smokeOutput") : null, - useNativeExport: enabled ? params.get("smokeUseNativeExport") === "1" : false, - encodingMode: - enabled && params.get("smokeEncodingMode") === "fast" - ? "fast" - : enabled && params.get("smokeEncodingMode") === "balanced" - ? "balanced" - : enabled && params.get("smokeEncodingMode") === "quality" - ? "quality" - : undefined, - shadowIntensity: enabled - ? parseSmokeExportNonNegativeNumber(params.get("smokeShadowIntensity")) - : undefined, - webcamInputPath: enabled ? params.get("smokeWebcamInput") : null, - webcamShadow: enabled - ? parseSmokeExportNonNegativeNumber(params.get("smokeWebcamShadow")) - : undefined, - webcamSize: enabled - ? parseSmokeExportNonNegativeNumber(params.get("smokeWebcamSize")) - : undefined, - pipelineModel: - enabled && params.get("smokePipelineModel") === "modern" - ? "modern" - : enabled && params.get("smokePipelineModel") === "legacy" - ? "legacy" - : undefined, - backendPreference: - enabled && params.get("smokeBackendPreference") === "auto" - ? "auto" - : enabled && params.get("smokeBackendPreference") === "webcodecs" - ? "webcodecs" - : enabled && params.get("smokeBackendPreference") === "breeze" - ? "breeze" - : undefined, - renderBackend: enabled - ? parseSmokeRenderBackend(params.get("smokeRenderBackend")) - : undefined, - maxEncodeQueue: enabled - ? parseSmokeExportNumber(params.get("smokeMaxEncodeQueue")) - : undefined, - maxDecodeQueue: enabled - ? parseSmokeExportNumber(params.get("smokeMaxDecodeQueue")) - : undefined, - maxPendingFrames: enabled - ? parseSmokeExportNumber(params.get("smokeMaxPendingFrames")) - : undefined, - projectPath: enabled ? params.get("smokeProject") : null, - quality: enabled ? parseSmokeExportQuality(params.get("smokeQuality")) : undefined, - fps: enabled ? parseSmokeExportFps(params.get("smokeFps")) : undefined, - }; -} - -function getDevOpenRecordingConfig(search: string): DevOpenRecordingConfig { - const params = new URLSearchParams(search); - return { - inputPath: params.get("devOpenInput"), - webcamInputPath: params.get("devOpenWebcam"), - }; -} - function isComparableObject(value: unknown): value is Record { return typeof value === "object" && value !== null; } diff --git a/src/components/video-editor/smokeExportConfig.test.ts b/src/components/video-editor/smokeExportConfig.test.ts new file mode 100644 index 00000000..c221e1e4 --- /dev/null +++ b/src/components/video-editor/smokeExportConfig.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "vitest"; + +import { getDevOpenRecordingConfig, getSmokeExportConfig } from "./smokeExportConfig"; + +describe("getSmokeExportConfig", () => { + it("keeps smoke export disabled when the flag is absent", () => { + expect(getSmokeExportConfig("?smokeInput=/tmp/input.mp4")).toEqual({ + enabled: false, + inputPath: null, + outputPath: null, + useNativeExport: false, + encodingMode: undefined, + shadowIntensity: undefined, + webcamInputPath: null, + webcamShadow: undefined, + webcamSize: undefined, + pipelineModel: undefined, + backendPreference: undefined, + renderBackend: undefined, + maxEncodeQueue: undefined, + maxDecodeQueue: undefined, + maxPendingFrames: undefined, + projectPath: null, + quality: undefined, + fps: undefined, + }); + }); + + it("parses the enabled smoke export query contract", () => { + const config = getSmokeExportConfig( + "?smokeExport=1" + + "&smokeInput=/tmp/input.mp4" + + "&smokeOutput=/tmp/output.mp4" + + "&smokeUseNativeExport=1" + + "&smokeEncodingMode=quality" + + "&smokeShadowIntensity=0" + + "&smokeWebcamInput=/tmp/webcam.mp4" + + "&smokeWebcamShadow=1.5" + + "&smokeWebcamSize=0.75" + + "&smokePipelineModel=legacy" + + "&smokeBackendPreference=breeze" + + "&smokeRenderBackend=webgpu" + + "&smokeMaxEncodeQueue=8" + + "&smokeMaxDecodeQueue=9" + + "&smokeMaxPendingFrames=10" + + "&smokeProject=/tmp/project.recordly" + + "&smokeQuality=source" + + "&smokeFps=60", + ); + + expect(config).toEqual({ + enabled: true, + inputPath: "/tmp/input.mp4", + outputPath: "/tmp/output.mp4", + useNativeExport: true, + encodingMode: "quality", + shadowIntensity: 0, + webcamInputPath: "/tmp/webcam.mp4", + webcamShadow: 1.5, + webcamSize: 0.75, + pipelineModel: "legacy", + backendPreference: "breeze", + renderBackend: "webgpu", + maxEncodeQueue: 8, + maxDecodeQueue: 9, + maxPendingFrames: 10, + projectPath: "/tmp/project.recordly", + quality: "source", + fps: 60, + }); + }); + + it("drops invalid optional smoke export values", () => { + const config = getSmokeExportConfig( + "?smokeExport=1" + + "&smokeEncodingMode=slow" + + "&smokeShadowIntensity=-1" + + "&smokeWebcamShadow=nan" + + "&smokeWebcamSize=-0.1" + + "&smokePipelineModel=classic" + + "&smokeBackendPreference=native" + + "&smokeRenderBackend=canvas" + + "&smokeMaxEncodeQueue=0" + + "&smokeMaxDecodeQueue=-4" + + "&smokeMaxPendingFrames=abc" + + "&smokeQuality=ultra" + + "&smokeFps=25", + ); + + expect(config).toMatchObject({ + enabled: true, + useNativeExport: false, + encodingMode: undefined, + shadowIntensity: undefined, + webcamShadow: undefined, + webcamSize: undefined, + pipelineModel: undefined, + backendPreference: undefined, + renderBackend: undefined, + maxEncodeQueue: undefined, + maxDecodeQueue: undefined, + maxPendingFrames: undefined, + quality: undefined, + fps: undefined, + }); + }); +}); + +describe("getDevOpenRecordingConfig", () => { + it("reads dev-open paths independently from smoke export", () => { + expect( + getDevOpenRecordingConfig( + "?devOpenInput=/tmp/input.mp4&devOpenWebcam=/tmp/webcam.mp4", + ), + ).toEqual({ + inputPath: "/tmp/input.mp4", + webcamInputPath: "/tmp/webcam.mp4", + }); + }); +}); diff --git a/src/components/video-editor/smokeExportConfig.ts b/src/components/video-editor/smokeExportConfig.ts new file mode 100644 index 00000000..0d71779b --- /dev/null +++ b/src/components/video-editor/smokeExportConfig.ts @@ -0,0 +1,131 @@ +import { + isValidMp4FrameRate, + type ExportBackendPreference, + type ExportEncodingMode, + type ExportMp4FrameRate, + type ExportPipelineModel, + type ExportQuality, + type ExportRenderBackend, +} from "@/lib/exporter/types"; + +export type SmokeExportConfig = { + enabled: boolean; + inputPath: string | null; + outputPath: string | null; + useNativeExport: boolean; + encodingMode?: ExportEncodingMode; + shadowIntensity?: number; + webcamInputPath?: string | null; + webcamShadow?: number; + webcamSize?: number; + pipelineModel?: ExportPipelineModel; + backendPreference?: ExportBackendPreference; + renderBackend?: ExportRenderBackend; + maxEncodeQueue?: number; + maxDecodeQueue?: number; + maxPendingFrames?: number; + projectPath?: string | null; + quality?: ExportQuality; + fps?: ExportMp4FrameRate; +}; + +export type DevOpenRecordingConfig = { + inputPath: string | null; + webcamInputPath: string | null; +}; + +function parseSmokeExportNumber(value: string | null): number | undefined { + if (value === null) { + return undefined; + } + + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; +} + +function parseSmokeExportNonNegativeNumber(value: string | null): number | undefined { + if (value === null) { + return undefined; + } + + const parsed = Number.parseFloat(value); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; +} + +function parseSmokeExportQuality(value: string | null): ExportQuality | undefined { + if (value === "medium" || value === "good" || value === "high" || value === "source") { + return value; + } + return undefined; +} + +function parseSmokeExportFps(value: string | null): ExportMp4FrameRate | undefined { + if (value === null) return undefined; + const parsed = Number.parseInt(value, 10); + return isValidMp4FrameRate(parsed) ? parsed : undefined; +} + +function parseSmokeRenderBackend(value: string | null): ExportRenderBackend | undefined { + return value === "webgl" || value === "webgpu" ? value : undefined; +} + +export function getSmokeExportConfig(search: string): SmokeExportConfig { + const params = new URLSearchParams(search); + const enabled = params.get("smokeExport") === "1"; + + return { + enabled, + inputPath: enabled ? params.get("smokeInput") : null, + outputPath: enabled ? params.get("smokeOutput") : null, + useNativeExport: enabled ? params.get("smokeUseNativeExport") === "1" : false, + encodingMode: + enabled && params.get("smokeEncodingMode") === "fast" + ? "fast" + : enabled && params.get("smokeEncodingMode") === "balanced" + ? "balanced" + : enabled && params.get("smokeEncodingMode") === "quality" + ? "quality" + : undefined, + shadowIntensity: enabled + ? parseSmokeExportNonNegativeNumber(params.get("smokeShadowIntensity")) + : undefined, + webcamInputPath: enabled ? params.get("smokeWebcamInput") : null, + webcamShadow: enabled + ? parseSmokeExportNonNegativeNumber(params.get("smokeWebcamShadow")) + : undefined, + webcamSize: enabled + ? parseSmokeExportNonNegativeNumber(params.get("smokeWebcamSize")) + : undefined, + pipelineModel: + enabled && params.get("smokePipelineModel") === "modern" + ? "modern" + : enabled && params.get("smokePipelineModel") === "legacy" + ? "legacy" + : undefined, + backendPreference: + enabled && params.get("smokeBackendPreference") === "auto" + ? "auto" + : enabled && params.get("smokeBackendPreference") === "webcodecs" + ? "webcodecs" + : enabled && params.get("smokeBackendPreference") === "breeze" + ? "breeze" + : undefined, + renderBackend: enabled ? parseSmokeRenderBackend(params.get("smokeRenderBackend")) : undefined, + maxEncodeQueue: enabled ? parseSmokeExportNumber(params.get("smokeMaxEncodeQueue")) : undefined, + maxDecodeQueue: enabled ? parseSmokeExportNumber(params.get("smokeMaxDecodeQueue")) : undefined, + maxPendingFrames: enabled + ? parseSmokeExportNumber(params.get("smokeMaxPendingFrames")) + : undefined, + projectPath: enabled ? params.get("smokeProject") : null, + quality: enabled ? parseSmokeExportQuality(params.get("smokeQuality")) : undefined, + fps: enabled ? parseSmokeExportFps(params.get("smokeFps")) : undefined, + }; +} + +export function getDevOpenRecordingConfig(search: string): DevOpenRecordingConfig { + const params = new URLSearchParams(search); + return { + inputPath: params.get("devOpenInput"), + webcamInputPath: params.get("devOpenWebcam"), + }; +}