mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
Merge remote-tracking branch 'origin/main' into refactor/editor-project-dirty-state-local
# Conflicts: # src/components/video-editor/VideoEditor.tsx
This commit is contained in:
@@ -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,
|
||||
@@ -89,10 +87,7 @@ import {
|
||||
getAspectRatioValue,
|
||||
} from "@/utils/aspectRatioUtils";
|
||||
import { ExtensionIcon } from "./ExtensionIcon";
|
||||
import {
|
||||
calculateMp4ExportDimensions,
|
||||
calculateMp4SourceDimensions,
|
||||
} from "./exportDimensions";
|
||||
import { calculateMp4ExportDimensions, calculateMp4SourceDimensions } from "./exportDimensions";
|
||||
|
||||
const PhCursorFill = (props: { className?: string; weight?: "fill" | "regular" }) => (
|
||||
<Cursor weight="fill" className={props.className} />
|
||||
@@ -113,11 +108,21 @@ const PhSettings = (props: { className?: string; weight?: "fill" | "regular" })
|
||||
<Gear weight={props.weight ?? "regular"} className={props.className} />
|
||||
);
|
||||
|
||||
import type { SourceAudioTrackSettings } from "@/components/video-editor/audio/audioTypes";
|
||||
import { extensionHost } from "@/lib/extensions";
|
||||
import { useVideoEditorAudio } from "./audio/useVideoEditorAudio";
|
||||
import { resolveAutoCaptionSourcePath } from "./autoCaptionSource";
|
||||
import { CropControl } from "./CropControl";
|
||||
import { ExportSettingsMenu } from "./ExportSettingsMenu";
|
||||
import ExtensionManager from "./ExtensionManager";
|
||||
import {
|
||||
createEditorHistoryStack,
|
||||
type EditorHistorySnapshot,
|
||||
recordEditorHistorySnapshot,
|
||||
redoEditorHistoryStack,
|
||||
resetEditorHistoryStack,
|
||||
undoEditorHistoryStack,
|
||||
} from "./editorHistory";
|
||||
import {
|
||||
type EditorPreset,
|
||||
type EditorPresetSnapshot,
|
||||
@@ -127,15 +132,8 @@ import {
|
||||
saveEditorPresets,
|
||||
serializeEditorPresetSnapshot,
|
||||
} from "./editorPreferences";
|
||||
import {
|
||||
createEditorHistoryStack,
|
||||
type EditorHistorySnapshot,
|
||||
recordEditorHistorySnapshot,
|
||||
redoEditorHistoryStack,
|
||||
resetEditorHistoryStack,
|
||||
undoEditorHistoryStack,
|
||||
} from "./editorHistory";
|
||||
import ProjectBrowserDialog, { type ProjectLibraryEntry } from "./ProjectBrowserDialog";
|
||||
import { hasUnsavedProjectChanges } from "./projectDirtyState";
|
||||
import {
|
||||
createProjectData,
|
||||
deriveNextId,
|
||||
@@ -147,9 +145,8 @@ import {
|
||||
toFileUrl,
|
||||
validateProjectData,
|
||||
} from "./projectPersistence";
|
||||
import { hasUnsavedProjectChanges } from "./projectDirtyState";
|
||||
import { SettingsPanel } from "./SettingsPanel";
|
||||
import { useVideoEditorAudio } from "./audio/useVideoEditorAudio";
|
||||
import { getDevOpenRecordingConfig, getSmokeExportConfig } from "./smokeExportConfig";
|
||||
import {
|
||||
APP_HEADER_ICON_BUTTON_CLASS,
|
||||
DiscordLinkButton,
|
||||
@@ -159,7 +156,6 @@ import {
|
||||
} from "./TutorialHelp";
|
||||
import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor";
|
||||
import { normalizeCursorTelemetry } from "./timeline/zoomSuggestionUtils";
|
||||
import type { SourceAudioTrackSettings } from "@/components/video-editor/audio/audioTypes";
|
||||
import {
|
||||
type AnnotationRegion,
|
||||
type AudioRegion,
|
||||
@@ -229,27 +225,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<string | null> {
|
||||
@@ -308,11 +283,6 @@ type SaveProjectOptions = {
|
||||
captureThumbnail?: boolean;
|
||||
};
|
||||
|
||||
type DevOpenRecordingConfig = {
|
||||
inputPath: string | null;
|
||||
webcamInputPath: string | null;
|
||||
};
|
||||
|
||||
async function writeSmokeExportReport(
|
||||
outputPath: string | null,
|
||||
report: Record<string, unknown>,
|
||||
@@ -362,108 +332,6 @@ function cloneStructured<T>(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 getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
@@ -623,9 +491,8 @@ export default function VideoEditor() {
|
||||
const [sourceAudioTrackSettingsByClip, setSourceAudioTrackSettingsByClip] = useState<
|
||||
Record<string, SourceAudioTrackSettings>
|
||||
>({});
|
||||
const [defaultSourceAudioTrackSettings, setDefaultSourceAudioTrackSettings] = useState<
|
||||
SourceAudioTrackSettings
|
||||
>({});
|
||||
const [defaultSourceAudioTrackSettings, setDefaultSourceAudioTrackSettings] =
|
||||
useState<SourceAudioTrackSettings>({});
|
||||
const [hasClipSourceAudio, setHasClipSourceAudio] = useState(false);
|
||||
const [autoCaptions, setAutoCaptions] = useState<CaptionCue[]>([]);
|
||||
const [autoCaptionSettings, setAutoCaptionSettings] = useState<AutoCaptionSettings>(
|
||||
@@ -1915,43 +1782,39 @@ export default function VideoEditor() {
|
||||
selectedAudioId,
|
||||
]);
|
||||
|
||||
const applyHistorySnapshot = useCallback(
|
||||
(snapshot: EditorHistorySnapshot) => {
|
||||
applyingHistoryRef.current = true;
|
||||
const cloned = cloneStructured(snapshot);
|
||||
setZoomRegions(cloned.zoomRegions);
|
||||
setClipRegions(cloned.clipRegions);
|
||||
setSpeedRegions(cloned.speedRegions);
|
||||
setAnnotationRegions(cloned.annotationRegions);
|
||||
setAudioRegions(cloned.audioRegions);
|
||||
setAutoCaptions(cloned.autoCaptions);
|
||||
setSelectedZoomId(cloned.selectedZoomId);
|
||||
setSelectedClipId(cloned.selectedClipId);
|
||||
setSelectedAnnotationId(cloned.selectedAnnotationId);
|
||||
setSelectedAudioId(cloned.selectedAudioId);
|
||||
const applyHistorySnapshot = useCallback((snapshot: EditorHistorySnapshot) => {
|
||||
applyingHistoryRef.current = true;
|
||||
const cloned = cloneStructured(snapshot);
|
||||
setZoomRegions(cloned.zoomRegions);
|
||||
setClipRegions(cloned.clipRegions);
|
||||
setSpeedRegions(cloned.speedRegions);
|
||||
setAnnotationRegions(cloned.annotationRegions);
|
||||
setAudioRegions(cloned.audioRegions);
|
||||
setAutoCaptions(cloned.autoCaptions);
|
||||
setSelectedZoomId(cloned.selectedZoomId);
|
||||
setSelectedClipId(cloned.selectedClipId);
|
||||
setSelectedAnnotationId(cloned.selectedAnnotationId);
|
||||
setSelectedAudioId(cloned.selectedAudioId);
|
||||
|
||||
nextZoomIdRef.current = deriveNextId(
|
||||
"zoom",
|
||||
cloned.zoomRegions.map((region) => region.id),
|
||||
);
|
||||
nextClipIdRef.current = deriveNextId(
|
||||
"clip",
|
||||
cloned.clipRegions.map((region) => region.id),
|
||||
);
|
||||
nextAnnotationIdRef.current = deriveNextId(
|
||||
"annotation",
|
||||
cloned.annotationRegions.map((region) => region.id),
|
||||
);
|
||||
nextAudioIdRef.current = deriveNextId(
|
||||
"audio",
|
||||
cloned.audioRegions.map((region) => region.id),
|
||||
);
|
||||
nextAnnotationZIndexRef.current =
|
||||
cloned.annotationRegions.reduce((max, region) => Math.max(max, region.zIndex), 0) +
|
||||
1;
|
||||
},
|
||||
[],
|
||||
);
|
||||
nextZoomIdRef.current = deriveNextId(
|
||||
"zoom",
|
||||
cloned.zoomRegions.map((region) => region.id),
|
||||
);
|
||||
nextClipIdRef.current = deriveNextId(
|
||||
"clip",
|
||||
cloned.clipRegions.map((region) => region.id),
|
||||
);
|
||||
nextAnnotationIdRef.current = deriveNextId(
|
||||
"annotation",
|
||||
cloned.annotationRegions.map((region) => region.id),
|
||||
);
|
||||
nextAudioIdRef.current = deriveNextId(
|
||||
"audio",
|
||||
cloned.audioRegions.map((region) => region.id),
|
||||
);
|
||||
nextAnnotationZIndexRef.current =
|
||||
cloned.annotationRegions.reduce((max, region) => Math.max(max, region.zIndex), 0) + 1;
|
||||
}, []);
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
const previous = undoEditorHistoryStack(editorHistoryRef.current, buildHistorySnapshot());
|
||||
@@ -2063,7 +1926,9 @@ export default function VideoEditor() {
|
||||
setSpeedRegions(normalizedEditor.speedRegions);
|
||||
setAnnotationRegions(normalizedEditor.annotationRegions);
|
||||
setAudioRegions(normalizedEditor.audioRegions);
|
||||
setSourceAudioTrackSettingsByClip(normalizedEditor.sourceAudioTrackSettingsByClip ?? {});
|
||||
setSourceAudioTrackSettingsByClip(
|
||||
normalizedEditor.sourceAudioTrackSettingsByClip ?? {},
|
||||
);
|
||||
setDefaultSourceAudioTrackSettings(
|
||||
normalizedEditor.defaultSourceAudioTrackSettings ?? {},
|
||||
);
|
||||
@@ -2429,7 +2294,7 @@ export default function VideoEditor() {
|
||||
sessionVideoPath: session?.videoPath,
|
||||
videoSourcePath: videoSourcePath,
|
||||
match: session?.videoPath === videoSourcePath,
|
||||
webcamPath: session?.webcamPath
|
||||
webcamPath: session?.webcamPath,
|
||||
});
|
||||
|
||||
if (!session || session.videoPath !== videoSourcePath) {
|
||||
@@ -3324,21 +3189,27 @@ export default function VideoEditor() {
|
||||
setAutoSuggestZoomsTrigger(0);
|
||||
}, []);
|
||||
|
||||
const handleSeek = useCallback((time: number, options: { pause?: boolean } = {}) => {
|
||||
const playback = videoPlaybackRef.current;
|
||||
const video = playback?.video;
|
||||
if (!video) return;
|
||||
const handleSeek = useCallback(
|
||||
(time: number, options: { pause?: boolean } = {}) => {
|
||||
const playback = videoPlaybackRef.current;
|
||||
const video = playback?.video;
|
||||
if (!video) return;
|
||||
|
||||
if (options.pause && !video.paused) {
|
||||
playback?.pause();
|
||||
}
|
||||
if (options.pause && !video.paused) {
|
||||
playback?.pause();
|
||||
}
|
||||
|
||||
video.currentTime = mapTimelineTimeToSourceTime(time * 1000) / 1000;
|
||||
}, [mapTimelineTimeToSourceTime]);
|
||||
video.currentTime = mapTimelineTimeToSourceTime(time * 1000) / 1000;
|
||||
},
|
||||
[mapTimelineTimeToSourceTime],
|
||||
);
|
||||
|
||||
const handleTimelineSeek = useCallback((time: number) => {
|
||||
handleSeek(time, { pause: true });
|
||||
}, [handleSeek]);
|
||||
const handleTimelineSeek = useCallback(
|
||||
(time: number) => {
|
||||
handleSeek(time, { pause: true });
|
||||
},
|
||||
[handleSeek],
|
||||
);
|
||||
|
||||
const handleSelectZoom = useCallback((id: string | null) => {
|
||||
setSelectedZoomId(id);
|
||||
@@ -3735,17 +3606,17 @@ export default function VideoEditor() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAudioAdded = useCallback((span: Span, audioPath: string, trackIndex?: number) => {
|
||||
const id = `audio-${nextAudioIdRef.current++}`;
|
||||
const newRegion: AudioRegion = {
|
||||
id,
|
||||
startMs: Math.round(span.start),
|
||||
endMs: Math.round(span.end),
|
||||
audioPath,
|
||||
volume: 1,
|
||||
normalize: false,
|
||||
trackIndex,
|
||||
};
|
||||
const handleAudioAdded = useCallback((span: Span, audioPath: string, trackIndex?: number) => {
|
||||
const id = `audio-${nextAudioIdRef.current++}`;
|
||||
const newRegion: AudioRegion = {
|
||||
id,
|
||||
startMs: Math.round(span.start),
|
||||
endMs: Math.round(span.end),
|
||||
audioPath,
|
||||
volume: 1,
|
||||
normalize: false,
|
||||
trackIndex,
|
||||
};
|
||||
setAudioRegions((prev) => [...prev, newRegion]);
|
||||
setSelectedAudioId(id);
|
||||
setSelectedZoomId(null);
|
||||
@@ -3795,29 +3666,29 @@ export default function VideoEditor() {
|
||||
[selectedAudioId],
|
||||
);
|
||||
|
||||
const handleAudioDelete = useCallback(
|
||||
(id: string) => {
|
||||
const handleAudioDelete = useCallback(
|
||||
(id: string) => {
|
||||
setAudioRegions((prev) => prev.filter((region) => region.id !== id));
|
||||
if (selectedAudioId === id) {
|
||||
setSelectedAudioId(null);
|
||||
}
|
||||
},
|
||||
[selectedAudioId],
|
||||
);
|
||||
[selectedAudioId],
|
||||
);
|
||||
|
||||
const handleAudioNormalizeChange = useCallback(
|
||||
(normalize: boolean) => {
|
||||
if (!selectedAudioId) {
|
||||
return;
|
||||
}
|
||||
setAudioRegions((prev) =>
|
||||
prev.map((region) =>
|
||||
region.id === selectedAudioId ? { ...region, normalize } : region,
|
||||
),
|
||||
);
|
||||
},
|
||||
[selectedAudioId],
|
||||
);
|
||||
const handleAudioNormalizeChange = useCallback(
|
||||
(normalize: boolean) => {
|
||||
if (!selectedAudioId) {
|
||||
return;
|
||||
}
|
||||
setAudioRegions((prev) =>
|
||||
prev.map((region) =>
|
||||
region.id === selectedAudioId ? { ...region, normalize } : region,
|
||||
),
|
||||
);
|
||||
},
|
||||
[selectedAudioId],
|
||||
);
|
||||
|
||||
const handleAnnotationAdded = useCallback((span: Span, trackIndex = 0) => {
|
||||
const id = `annotation-${nextAnnotationIdRef.current++}`;
|
||||
@@ -5715,19 +5586,20 @@ export default function VideoEditor() {
|
||||
selectedClipId={selectedClipId}
|
||||
selectedClipSpeed={
|
||||
selectedClipId
|
||||
? clipRegions.find((c) => c.id === selectedClipId)?.speed ?? 1
|
||||
? (clipRegions.find((c) => c.id === selectedClipId)
|
||||
?.speed ?? 1)
|
||||
: null
|
||||
}
|
||||
selectedClipMuted={
|
||||
selectedClipId
|
||||
? clipRegions.find((c) => c.id === selectedClipId)?.muted ??
|
||||
false
|
||||
? (clipRegions.find((c) => c.id === selectedClipId)
|
||||
?.muted ?? false)
|
||||
: null
|
||||
}
|
||||
selectedClipShowSourceAudio={
|
||||
selectedClipId
|
||||
? clipRegions.find((c) => c.id === selectedClipId)
|
||||
?.showSourceAudio ?? false
|
||||
? (clipRegions.find((c) => c.id === selectedClipId)
|
||||
?.showSourceAudio ?? false)
|
||||
: null
|
||||
}
|
||||
onClipSpeedChange={handleClipSpeedChange}
|
||||
@@ -5736,7 +5608,9 @@ export default function VideoEditor() {
|
||||
onClipDelete={handleClipDelete}
|
||||
hasClipSourceAudio={hasClipSourceAudio}
|
||||
sourceAudioTrackMeta={audio.sourceAudioTrackMeta}
|
||||
sourceAudioTrackSettings={audio.selectedClipSourceAudioTrackSettings}
|
||||
sourceAudioTrackSettings={
|
||||
audio.selectedClipSourceAudioTrackSettings
|
||||
}
|
||||
onSourceAudioTrackVolumeChange={
|
||||
audio.onSelectedClipSourceAudioTrackVolumeChange
|
||||
}
|
||||
@@ -5744,21 +5618,21 @@ export default function VideoEditor() {
|
||||
audio.onSelectedClipSourceAudioTrackNormalizeChange
|
||||
}
|
||||
selectedAudioId={selectedAudioId}
|
||||
selectedAudioVolume={
|
||||
selectedAudioId
|
||||
? (audioRegions.find((r) => r.id === selectedAudioId)
|
||||
?.volume ?? null)
|
||||
: null
|
||||
}
|
||||
selectedAudioNormalize={
|
||||
selectedAudioId
|
||||
? (audioRegions.find((r) => r.id === selectedAudioId)
|
||||
?.normalize ?? false)
|
||||
: null
|
||||
}
|
||||
onAudioVolumeChange={handleAudioVolumeChange}
|
||||
onAudioNormalizeChange={handleAudioNormalizeChange}
|
||||
onAudioDelete={handleAudioDelete}
|
||||
selectedAudioVolume={
|
||||
selectedAudioId
|
||||
? (audioRegions.find((r) => r.id === selectedAudioId)
|
||||
?.volume ?? null)
|
||||
: null
|
||||
}
|
||||
selectedAudioNormalize={
|
||||
selectedAudioId
|
||||
? (audioRegions.find((r) => r.id === selectedAudioId)
|
||||
?.normalize ?? false)
|
||||
: null
|
||||
}
|
||||
onAudioVolumeChange={handleAudioVolumeChange}
|
||||
onAudioNormalizeChange={handleAudioNormalizeChange}
|
||||
onAudioDelete={handleAudioDelete}
|
||||
shadowIntensity={shadowIntensity}
|
||||
onShadowChange={setShadowIntensity}
|
||||
backgroundBlur={backgroundBlur}
|
||||
@@ -6055,13 +5929,15 @@ export default function VideoEditor() {
|
||||
}
|
||||
cursorSway={cursorSway}
|
||||
volume={
|
||||
audio.shouldMutePreviewVideo || audio.isCurrentClipMuted
|
||||
audio.shouldMutePreviewVideo ||
|
||||
audio.isCurrentClipMuted
|
||||
? 0
|
||||
: Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
previewVolume * audio.embeddedSourcePreviewGain,
|
||||
previewVolume *
|
||||
audio.embeddedSourcePreviewGain,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user