+
+
-
- {selectedZoomId && (
-
-
-
{tSettings("zoom.level")}
-
- {selectedZoomDepth && (
-
- {ZOOM_DEPTH_OPTIONS.find((o) => o.depth === selectedZoomDepth)?.label}
-
- )}
-
-
-
-
-
-
-
-
- {selectedZoomMode === 'manual'
- ? "Set a fixed focus point for this zoom"
- : "Camera follows cursor automatically"}
-
-
-
- {ZOOM_DEPTH_OPTIONS.map((option) => {
- const isActive = selectedZoomDepth === option.depth;
- return (
-
- );
- })}
-
-
-
+
+ {selectedTrimId && (
@@ -2594,7 +2907,9 @@ export function SettingsPanel({
: "border-white/5 bg-white/5 text-slate-400 hover:bg-white/10 hover:border-white/10 hover:text-slate-200",
)}
>
-
{option.label}
+
+ {option.label}
+
);
})}
@@ -2610,59 +2925,7 @@ export function SettingsPanel({
)}
-
- {selectedClipId && (
-
-
- Clip Speed
- {selectedClipSpeed != null && selectedClipSpeed !== 1 && (
-
- {selectedClipSpeed}×
-
- )}
-
-
- {[
- { speed: 0.25, label: "0.25×" },
- { speed: 0.5, label: "0.5×" },
- { speed: 0.75, label: "0.75×" },
- { speed: 1, label: "1×" },
- { speed: 1.25, label: "1.25×" },
- { speed: 1.5, label: "1.5×" },
- { speed: 1.75, label: "1.75×" },
- { speed: 2, label: "2×" },
- ].map((option) => {
- const isActive = selectedClipSpeed === option.speed;
- return (
-
- );
- })}
-
-
-
- )}
-
);
}
diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx
index 18d36f98..d9759756 100644
--- a/src/components/video-editor/VideoEditor.tsx
+++ b/src/components/video-editor/VideoEditor.tsx
@@ -1,32 +1,47 @@
-import type { Span } from "dnd-timeline";
import {
- Camera,
- Captions,
- Download,
+ ArrowClockwise as Redo2,
+ ArrowCounterClockwise as Undo2,
+ CaretDown as ChevronDown,
+ CaretUp as ChevronUp,
+ Check,
+ ClosedCaptioning,
+ Crop,
+ Cursor,
+ DownloadSimple as Download,
+ FloppyDisk as Save,
FolderOpen,
- MousePointer2,
- Puzzle,
- Redo2,
- Save,
- Sparkles,
- Undo2,
+ Gear,
+ MagicWand as WandSparkles,
+ MagnifyingGlassPlus as ZoomIn,
+ Pause,
+ Play,
+ Plus,
+ Camera as PhCameraRegular,
+ PuzzlePiece,
+ Scissors,
+ SkipBack,
+ SkipForward,
+ SpeakerHigh as Volume2,
+ SpeakerLow as Volume1,
+ SpeakerX as VolumeX,
+ Sparkle,
+ UserCircle as User,
X,
-} from "lucide-react";
-import { AnimatePresence, LayoutGroup, motion } from "motion/react";
+} from "@phosphor-icons/react";
+import type { Span } from "dnd-timeline";
+import { motion } from "motion/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
-import { ExtensionIcon } from "./ExtensionIcon";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
+ DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Toaster } from "@/components/ui/sonner";
import { useI18n } from "@/contexts/I18nContext";
import { useShortcuts } from "@/contexts/ShortcutsContext";
-import type { AppLocale } from "@/i18n/config";
-import { SUPPORTED_LOCALES } from "@/i18n/config";
import {
calculateOutputDimensions,
DEFAULT_MP4_CODEC,
@@ -55,12 +70,39 @@ import {
getMediaSyncPlaybackRate,
} from "@/lib/mediaTiming";
import { matchesShortcut } from "@/lib/shortcuts";
-import { type AspectRatio, getAspectRatioValue } from "@/utils/aspectRatioUtils";
+import {
+ ASPECT_RATIOS,
+ type AspectRatio,
+ getAspectRatioLabel,
+ getAspectRatioValue,
+} from "@/utils/aspectRatioUtils";
+import { ExtensionIcon } from "./ExtensionIcon";
+
+const PhCursorFill = (props: { className?: string; weight?: "fill" | "regular" }) => (
+
+);
+const PhCamera = (props: { className?: string; weight?: "fill" | "regular" }) => (
+
+);
+const PhCaptions = (props: { className?: string; weight?: "fill" | "regular" }) => (
+
+);
+const PhPuzzle = (props: { className?: string; weight?: "fill" | "regular" }) => (
+
+);
+const PhSparkle = (props: { className?: string; weight?: "fill" | "regular" }) => (
+
+);
+const PhSettings = (props: { className?: string; weight?: "fill" | "regular" }) => (
+
+);
+
+import { extensionHost } from "@/lib/extensions";
import { resolveAutoCaptionSourcePath } from "./autoCaptionSource";
import { CropControl } from "./CropControl";
import { ExportSettingsMenu } from "./ExportSettingsMenu";
+import ExtensionManager from "./ExtensionManager";
import { loadEditorPreferences, saveEditorPreferences } from "./editorPreferences";
-import PlaybackControls from "./PlaybackControls";
import ProjectBrowserDialog, { type ProjectLibraryEntry } from "./ProjectBrowserDialog";
import {
createProjectData,
@@ -72,31 +114,31 @@ import {
validateProjectData,
} from "./projectPersistence";
import { type EditorEffectSection, SettingsPanel } from "./SettingsPanel";
-import ExtensionManager from "./ExtensionManager";
-import { extensionHost } from "@/lib/extensions";
import {
APP_HEADER_ICON_BUTTON_CLASS,
DiscordLinkButton,
FeedbackDialog,
- KeyboardShortcutsDialog,
openExternalLink,
RECORDLY_ISSUES_URL,
} from "./TutorialHelp";
-import TimelineEditor from "./timeline/TimelineEditor";
+import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor";
import { normalizeCursorTelemetry } from "./timeline/zoomSuggestionUtils";
import {
type AnnotationRegion,
type AudioRegion,
type AutoCaptionSettings,
type CaptionCue,
+ type ClipRegion,
type CropRegion,
type CursorStyle,
type CursorTelemetryPoint,
clampFocusToDepth,
+ clipsToTrims,
DEFAULT_ANNOTATION_POSITION,
DEFAULT_ANNOTATION_SIZE,
DEFAULT_ANNOTATION_STYLE,
DEFAULT_AUTO_CAPTION_SETTINGS,
+ DEFAULT_AUTO_ZOOM_DEPTH,
DEFAULT_CONNECTED_ZOOM_DURATION_MS,
DEFAULT_CONNECTED_ZOOM_EASING,
DEFAULT_CONNECTED_ZOOM_GAP_MS,
@@ -106,22 +148,20 @@ import {
DEFAULT_PLAYBACK_SPEED,
DEFAULT_WEBCAM_OVERLAY,
DEFAULT_ZOOM_DEPTH,
- DEFAULT_AUTO_ZOOM_DEPTH,
- type ZoomMode,
DEFAULT_ZOOM_IN_DURATION_MS,
DEFAULT_ZOOM_IN_EASING,
DEFAULT_ZOOM_IN_OVERLAP_MS,
DEFAULT_ZOOM_OUT_DURATION_MS,
DEFAULT_ZOOM_OUT_EASING,
type FigureData,
+ getClipSourceEndMs,
type PlaybackSpeed,
type SpeedRegion,
type TrimRegion,
- type ClipRegion,
- clipsToTrims,
type WebcamOverlaySettings,
type ZoomDepth,
type ZoomFocus,
+ type ZoomMode,
type ZoomRegion,
type ZoomTransitionEasing,
} from "./types";
@@ -186,7 +226,10 @@ async function writeSmokeExportReport(
reportBytes.byteOffset,
reportBytes.byteOffset + reportBytes.byteLength,
) as ArrayBuffer;
- await window.electronAPI.writeExportedVideoToPath(reportBuffer, `${outputPath}.report.json`);
+ await window.electronAPI.writeExportedVideoToPath(
+ reportBuffer,
+ `${outputPath}.report.json`,
+ );
} catch (error) {
console.error("[smoke-export] Failed to write report", error);
}
@@ -254,16 +297,16 @@ function getSmokeExportConfig(search: string): SmokeExportConfig {
: 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,
+ 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"
@@ -278,9 +321,15 @@ function getSmokeExportConfig(search: string): SmokeExportConfig {
: enabled && params.get("smokeBackendPreference") === "breeze"
? "breeze"
: undefined,
- maxEncodeQueue: enabled ? parseSmokeExportNumber(params.get("smokeMaxEncodeQueue")) : undefined,
- maxDecodeQueue: enabled ? parseSmokeExportNumber(params.get("smokeMaxDecodeQueue")) : undefined,
- maxPendingFrames: enabled ? parseSmokeExportNumber(params.get("smokeMaxPendingFrames")) : undefined,
+ maxEncodeQueue: enabled
+ ? parseSmokeExportNumber(params.get("smokeMaxEncodeQueue"))
+ : undefined,
+ maxDecodeQueue: enabled
+ ? parseSmokeExportNumber(params.get("smokeMaxDecodeQueue"))
+ : undefined,
+ maxPendingFrames: enabled
+ ? parseSmokeExportNumber(params.get("smokeMaxPendingFrames"))
+ : undefined,
};
}
@@ -419,31 +468,6 @@ function getErrorMessage(error: unknown): string {
return "Something went wrong";
}
-function LanguageSwitcher() {
- const { locale, setLocale, t } = useI18n();
- const idx = SUPPORTED_LOCALES.indexOf(locale as (typeof SUPPORTED_LOCALES)[number]);
- const next = SUPPORTED_LOCALES[(idx + 1) % SUPPORTED_LOCALES.length] as AppLocale;
- const labels: Record
= {
- en: "EN",
- es: "ES",
- "zh-CN": "中文",
- ko: "한국어",
- };
- return (
-
- );
-}
-
export default function VideoEditor() {
const { t } = useI18n();
const smokeExportConfig = useMemo(
@@ -465,9 +489,14 @@ export default function VideoEditor() {
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [wallpaper, setWallpaper] = useState(initialEditorPreferences.wallpaper);
- const [shadowIntensity, setShadowIntensity] = useState(initialEditorPreferences.shadowIntensity);
+ const [shadowIntensity, setShadowIntensity] = useState(
+ initialEditorPreferences.shadowIntensity,
+ );
const [backgroundBlur, setBackgroundBlur] = useState(initialEditorPreferences.backgroundBlur);
const [zoomMotionBlur, setZoomMotionBlur] = useState(initialEditorPreferences.zoomMotionBlur);
+ const [autoApplyFreshRecordingAutoZooms, setAutoApplyFreshRecordingAutoZooms] = useState(
+ initialEditorPreferences.autoApplyFreshRecordingAutoZooms,
+ );
const [connectZooms, setConnectZooms] = useState(initialEditorPreferences.connectZooms);
const [zoomInDurationMs, setZoomInDurationMs] = useState(
initialEditorPreferences.zoomInDurationMs ?? DEFAULT_ZOOM_IN_DURATION_MS,
@@ -499,7 +528,9 @@ export default function VideoEditor() {
initialEditorPreferences.cursorStyle ?? DEFAULT_CURSOR_STYLE,
);
const [cursorSize, setCursorSize] = useState(initialEditorPreferences.cursorSize);
- const [cursorSmoothing, setCursorSmoothing] = useState(initialEditorPreferences.cursorSmoothing);
+ const [cursorSmoothing, setCursorSmoothing] = useState(
+ initialEditorPreferences.cursorSmoothing,
+ );
const [zoomSmoothness, setZoomSmoothness] = useState(0.5);
const [zoomClassicMode, setZoomClassicMode] = useState(false);
const [cursorMotionBlur, setCursorMotionBlur] = useState(
@@ -542,7 +573,9 @@ export default function VideoEditor() {
const [whisperModelPath, setWhisperModelPath] = useState(
initialEditorPreferences.whisperModelPath,
);
- const [downloadedWhisperModelPath, setDownloadedWhisperModelPath] = useState(null);
+ const [downloadedWhisperModelPath, setDownloadedWhisperModelPath] = useState(
+ null,
+ );
const [whisperModelDownloadStatus, setWhisperModelDownloadStatus] = useState<
"idle" | "downloading" | "downloaded" | "error"
>(initialEditorPreferences.whisperModelPath ? "downloaded" : "idle");
@@ -554,7 +587,9 @@ export default function VideoEditor() {
const [showExportDropdown, setShowExportDropdown] = useState(false);
const [previewVolume, setPreviewVolume] = useState(1);
const [sourceAudioFallbackPaths, setSourceAudioFallbackPaths] = useState([]);
- const [aspectRatio, setAspectRatio] = useState(initialEditorPreferences.aspectRatio);
+ const [aspectRatio, setAspectRatio] = useState(
+ initialEditorPreferences.aspectRatio,
+ );
const [activeEffectSection, setActiveEffectSection] = useState("scene");
const [exportQuality, setExportQuality] = useState(
initialEditorPreferences.exportQuality,
@@ -562,8 +597,9 @@ export default function VideoEditor() {
const [exportEncodingMode, setExportEncodingMode] = useState(
initialEditorPreferences.exportEncodingMode,
);
- const [exportBackendPreference, setExportBackendPreference] =
- useState(initialEditorPreferences.exportBackendPreference);
+ const [exportBackendPreference, setExportBackendPreference] = useState(
+ initialEditorPreferences.exportBackendPreference,
+ );
const [exportPipelineModel, setExportPipelineModel] = useState(
initialEditorPreferences.exportPipelineModel,
);
@@ -616,9 +652,19 @@ export default function VideoEditor() {
const mp4SupportRequestRef = useRef(0);
const smokeExportStartedRef = useRef(false);
const [historyVersion, setHistoryVersion] = useState(0);
+ const timelineRef = useRef(null);
+
+ function formatTime(seconds: number) {
+ if (!isFinite(seconds) || isNaN(seconds) || seconds < 0) return "0:00";
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
+ }
+
+ const [timelineCollapsed, setTimelineCollapsed] = useState(false);
useEffect(() => {
- void window.electronAPI.getPlatform().then((platform) => {
+ void window.electronAPI?.getPlatform?.()?.then((platform) => {
setAppPlatform(platform);
});
}, []);
@@ -756,7 +802,10 @@ export default function VideoEditor() {
await frameRenderer.renderFrame(videoFrame, frameTimestampUs);
return frameRenderer.getCanvas().toDataURL("image/png");
} catch (thumbnailRenderError) {
- console.warn("Unable to render thumbnail from composed frame:", thumbnailRenderError);
+ console.warn(
+ "Unable to render thumbnail from composed frame:",
+ thumbnailRenderError,
+ );
} finally {
videoFrame?.close();
frameRenderer?.destroy();
@@ -775,7 +824,9 @@ export default function VideoEditor() {
}
const sourceWidth =
- drawableSource instanceof HTMLVideoElement ? drawableSource.videoWidth : drawableSource.width;
+ drawableSource instanceof HTMLVideoElement
+ ? drawableSource.videoWidth
+ : drawableSource.width;
const sourceHeight =
drawableSource instanceof HTMLVideoElement
? drawableSource.videoHeight
@@ -926,37 +977,41 @@ export default function VideoEditor() {
supportedMp4SourceDimensions.width,
]);
- const ensureSupportedMp4SourceDimensions = useCallback(async (frameRate: ExportMp4FrameRate) => {
- const result = await probeSupportedMp4Dimensions({
- width: desiredMp4SourceDimensions.width,
- height: desiredMp4SourceDimensions.height,
- frameRate,
- codec: DEFAULT_MP4_CODEC,
- getBitrate: getSourceQualityBitrate,
- });
+ const ensureSupportedMp4SourceDimensions = useCallback(
+ async (frameRate: ExportMp4FrameRate) => {
+ const result = await probeSupportedMp4Dimensions({
+ width: desiredMp4SourceDimensions.width,
+ height: desiredMp4SourceDimensions.height,
+ frameRate,
+ codec: DEFAULT_MP4_CODEC,
+ getBitrate: getSourceQualityBitrate,
+ });
- if (!result.encoderPath) {
- throw new Error(
- `Video encoding not supported on this system. Tried codec ${DEFAULT_MP4_CODEC} at ${frameRate} FPS up to ${desiredMp4SourceDimensions.width}x${desiredMp4SourceDimensions.height}.`,
- );
- }
-
- setSupportedMp4SourceDimensions((current) => {
- if (
- current.width === result.width &&
- current.height === result.height &&
- current.capped === result.capped &&
- current.encoderPath?.codec === result.encoderPath?.codec &&
- current.encoderPath?.hardwareAcceleration === result.encoderPath?.hardwareAcceleration
- ) {
- return current;
+ if (!result.encoderPath) {
+ throw new Error(
+ `Video encoding not supported on this system. Tried codec ${DEFAULT_MP4_CODEC} at ${frameRate} FPS up to ${desiredMp4SourceDimensions.width}x${desiredMp4SourceDimensions.height}.`,
+ );
}
- return result;
- });
+ setSupportedMp4SourceDimensions((current) => {
+ if (
+ current.width === result.width &&
+ current.height === result.height &&
+ current.capped === result.capped &&
+ current.encoderPath?.codec === result.encoderPath?.codec &&
+ current.encoderPath?.hardwareAcceleration ===
+ result.encoderPath?.hardwareAcceleration
+ ) {
+ return current;
+ }
- return result;
- }, [desiredMp4SourceDimensions.height, desiredMp4SourceDimensions.width]);
+ return result;
+ });
+
+ return result;
+ },
+ [desiredMp4SourceDimensions.height, desiredMp4SourceDimensions.width],
+ );
useEffect(() => {
let cancelled = false;
@@ -1000,17 +1055,17 @@ export default function VideoEditor() {
// Extension-contributed standalone section pages (no parentSection)
const [extensionSectionButtons, setExtensionSectionButtons] = useState<
- { id: EditorEffectSection; label: string; icon: typeof Puzzle | string }[]
+ { id: EditorEffectSection; label: string; icon: typeof PhPuzzle | string }[]
>([]);
useEffect(() => {
const update = () => {
const panels = extensionHost.getSettingsPanels();
const standalone = panels
- .filter(p => !p.panel.parentSection)
- .map(p => ({
+ .filter((p) => !p.panel.parentSection)
+ .map((p) => ({
id: `ext:${p.extensionId}/${p.panel.id}` as EditorEffectSection,
label: p.panel.label,
- icon: p.panel.icon || (Puzzle as typeof Puzzle | string),
+ icon: p.panel.icon || (PhPuzzle as typeof PhPuzzle | string),
}));
setExtensionSectionButtons(standalone);
};
@@ -1020,34 +1075,39 @@ export default function VideoEditor() {
const editorSectionButtons = useMemo(
() => [
- { id: "scene" as const, label: t("settings.sections.scene", "Scene"), icon: Sparkles },
+ { id: "scene" as const, label: t("settings.sections.scene", "Scene"), icon: PhSparkle },
{
id: "cursor" as const,
label: t("settings.sections.cursor", "Cursor"),
- icon: MousePointer2,
+ icon: PhCursorFill,
+ },
+ {
+ id: "webcam" as const,
+ label: t("settings.sections.webcam", "Webcam"),
+ icon: PhCamera,
},
- { id: "webcam" as const, label: t("settings.sections.webcam", "Webcam"), icon: Camera },
{
id: "captions" as const,
label: t("settings.sections.captions", "Captions"),
- icon: Captions,
+ icon: PhCaptions,
+ },
+ {
+ id: "settings" as const,
+ label: t("settings.sections.settings", "Settings"),
+ icon: PhSettings,
},
...extensionSectionButtons,
{
id: "extensions" as const,
label: t("settings.sections.extensions", "Extensions"),
- icon: Puzzle,
+ icon: PhPuzzle,
},
],
[t, extensionSectionButtons],
);
useEffect(() => {
- if (
- activeEffectSection === "zoom" ||
- activeEffectSection === "frame" ||
- activeEffectSection === "crop"
- ) {
+ if (activeEffectSection === "frame" || activeEffectSection === "crop") {
setActiveEffectSection("scene");
}
}, [activeEffectSection]);
@@ -1128,7 +1188,8 @@ export default function VideoEditor() {
void (async () => {
try {
- const result = await window.electronAPI.getVideoAudioFallbackPaths(currentSourcePath);
+ const result =
+ await window.electronAPI.getVideoAudioFallbackPaths(currentSourcePath);
if (cancelled) {
return;
}
@@ -1147,7 +1208,9 @@ export default function VideoEditor() {
const projectDisplayName = useMemo(() => {
const fileName =
- currentProjectPath?.split(/[\\/]/).pop() ?? currentSourcePath?.split(/[\\/]/).pop() ?? "";
+ currentProjectPath?.split(/[\\/]/).pop() ??
+ currentSourcePath?.split(/[\\/]/).pop() ??
+ "";
const withoutExtension = fileName.replace(/\.recordly$/i, "").replace(/\.[^.]+$/, "");
return withoutExtension || t("editor.project.untitled", "Untitled");
}, [currentProjectPath, currentSourcePath, t]);
@@ -1208,6 +1271,7 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
+ autoApplyFreshRecordingAutoZooms,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -1320,7 +1384,8 @@ export default function VideoEditor() {
cloned.audioRegions.map((region) => region.id),
);
nextAnnotationZIndexRef.current =
- cloned.annotationRegions.reduce((max, region) => Math.max(max, region.zIndex), 0) + 1;
+ cloned.annotationRegions.reduce((max, region) => Math.max(max, region.zIndex), 0) +
+ 1;
},
[cloneSnapshot],
);
@@ -1415,8 +1480,8 @@ export default function VideoEditor() {
setWebcam(normalizedEditor.webcam);
setZoomRegions(normalizedEditor.zoomRegions);
setTrimRegions(normalizedEditor.trimRegions);
- setClipRegions((normalizedEditor as any).clipRegions ?? []);
- clipInitializedRef.current = ((normalizedEditor as any).clipRegions ?? []).length > 0;
+ setClipRegions(normalizedEditor.clipRegions);
+ clipInitializedRef.current = normalizedEditor.clipRegions.length > 0;
setSpeedRegions(normalizedEditor.speedRegions);
setAnnotationRegions(normalizedEditor.annotationRegions);
setAudioRegions(normalizedEditor.audioRegions);
@@ -1450,7 +1515,7 @@ export default function VideoEditor() {
);
nextClipIdRef.current = deriveNextId(
"clip",
- ((normalizedEditor as any).clipRegions ?? []).map((region: ClipRegion) => region.id),
+ normalizedEditor.clipRegions.map((region: ClipRegion) => region.id),
);
nextSpeedIdRef.current = deriveNextId(
"speed",
@@ -1477,7 +1542,9 @@ export default function VideoEditor() {
syncHistoryButtons();
setLastSavedSnapshot(
- cloneStructured(createProjectData(sourcePath, buildPersistedEditorState(normalizedEditor))),
+ cloneStructured(
+ createProjectData(sourcePath, buildPersistedEditorState(normalizedEditor)),
+ ),
);
await refreshProjectLibrary();
return true;
@@ -1638,10 +1705,14 @@ export default function VideoEditor() {
setBorderRadius(initialEditorPreferences.borderRadius);
setAspectRatio(initialEditorPreferences.aspectRatio);
setExportFormat(initialEditorPreferences.exportFormat);
- setMp4FrameRate(initialEditorPreferences.mp4FrameRate ?? DEFAULT_MP4_EXPORT_FRAME_RATE);
+ setMp4FrameRate(
+ initialEditorPreferences.mp4FrameRate ?? DEFAULT_MP4_EXPORT_FRAME_RATE,
+ );
setExportQuality(initialEditorPreferences.exportQuality);
setExportEncodingMode(initialEditorPreferences.exportEncodingMode);
- setExportBackendPreference(initialEditorPreferences.exportBackendPreference);
+ setExportBackendPreference(
+ initialEditorPreferences.exportBackendPreference,
+ );
setExportPipelineModel(initialEditorPreferences.exportPipelineModel);
setGifFrameRate(initialEditorPreferences.gifFrameRate);
setGifLoop(initialEditorPreferences.gifLoop);
@@ -1658,7 +1729,8 @@ export default function VideoEditor() {
setVideoPath(sourceVideoUrl);
setCurrentProjectPath(null);
setLastSavedSnapshot(null);
- pendingFreshRecordingAutoZoomPathRef.current = sourceVideoUrl;
+ pendingFreshRecordingAutoZoomPathRef.current =
+ autoApplyFreshRecordingAutoZooms ? sourceVideoUrl : null;
setWebcam((prev) => ({
...prev,
enabled: Boolean(sessionResult.session?.webcamPath),
@@ -1675,7 +1747,8 @@ export default function VideoEditor() {
setVideoPath(sourceVideoUrl);
setCurrentProjectPath(null);
setLastSavedSnapshot(null);
- pendingFreshRecordingAutoZoomPathRef.current = sourceVideoUrl;
+ pendingFreshRecordingAutoZoomPathRef.current =
+ autoApplyFreshRecordingAutoZooms ? sourceVideoUrl : null;
setWebcam((prev) => ({
...prev,
enabled: false,
@@ -1692,8 +1765,12 @@ export default function VideoEditor() {
}
loadInitialData();
-
- }, [applyLoadedProject, smokeExportConfig.enabled, smokeExportConfig.inputPath]);
+ }, [
+ applyLoadedProject,
+ autoApplyFreshRecordingAutoZooms,
+ smokeExportConfig.enabled,
+ smokeExportConfig.inputPath,
+ ]);
useEffect(() => {
saveEditorPreferences({
@@ -1741,6 +1818,7 @@ export default function VideoEditor() {
shadowIntensity,
backgroundBlur,
zoomMotionBlur,
+ autoApplyFreshRecordingAutoZooms,
connectZooms,
zoomInDurationMs,
zoomInOverlapMs,
@@ -1894,7 +1972,9 @@ export default function VideoEditor() {
sessionResult?.success && sessionResult.session?.videoPath
? sessionResult.session.videoPath
: null,
- currentVideoPath: currentVideoResult.success ? (currentVideoResult.path ?? null) : null,
+ currentVideoPath: currentVideoResult.success
+ ? (currentVideoResult.path ?? null)
+ : null,
});
}
@@ -1926,7 +2006,9 @@ export default function VideoEditor() {
if (!result.success || !result.cues) {
toast.error(
- result.message || getErrorMessage(result.error) || "Failed to generate captions",
+ result.message ||
+ getErrorMessage(result.error) ||
+ "Failed to generate captions",
);
return;
}
@@ -2223,7 +2305,45 @@ export default function VideoEditor() {
setTrimRegions(clipsToTrims(clipRegions, totalMs));
}, [clipRegions, duration]);
- const effectiveZoomRegions = zoomRegions;
+ const mapTimelineTimeToSourceTime = useCallback(
+ (timeMs: number) => {
+ for (const clip of clipRegions) {
+ if (timeMs < clip.startMs || timeMs > clip.endMs) continue;
+ const speed = Number.isFinite(clip.speed) && clip.speed > 0 ? clip.speed : 1;
+ return Math.round(clip.startMs + (timeMs - clip.startMs) * speed);
+ }
+ return Math.round(timeMs);
+ },
+ [clipRegions],
+ );
+
+ const mapSourceTimeToTimelineTime = useCallback(
+ (timeMs: number) => {
+ for (const clip of clipRegions) {
+ const sourceEndMs = getClipSourceEndMs(clip);
+ if (timeMs < clip.startMs || timeMs > sourceEndMs) continue;
+ const speed = Number.isFinite(clip.speed) && clip.speed > 0 ? clip.speed : 1;
+ return Math.round(clip.startMs + (timeMs - clip.startMs) / speed);
+ }
+ return Math.round(timeMs);
+ },
+ [clipRegions],
+ );
+
+ const effectiveZoomRegions = useMemo(
+ () =>
+ zoomRegions.map((region) => ({
+ ...region,
+ startMs: mapTimelineTimeToSourceTime(region.startMs),
+ endMs: mapTimelineTimeToSourceTime(region.endMs),
+ })),
+ [zoomRegions, mapTimelineTimeToSourceTime],
+ );
+
+ const timelinePlayheadTime = useMemo(
+ () => mapSourceTimeToTimelineTime(currentTime * 1000) / 1000,
+ [currentTime, mapSourceTimeToTimelineTime],
+ );
// Merge clip speeds into speed regions so playback + export respect per-clip speed
const effectiveSpeedRegions = useMemo(() => {
@@ -2232,7 +2352,7 @@ export default function VideoEditor() {
.map((clip) => ({
id: `clip-speed-${clip.id}`,
startMs: clip.startMs,
- endMs: clip.endMs,
+ endMs: getClipSourceEndMs(clip),
speed: clip.speed as SpeedRegion["speed"],
}));
if (clipDerived.length === 0) return speedRegions;
@@ -2268,14 +2388,17 @@ export default function VideoEditor() {
function handleSeek(time: number) {
const video = videoPlaybackRef.current?.video;
if (!video) return;
- video.currentTime = time;
+ video.currentTime = mapTimelineTimeToSourceTime(time * 1000) / 1000;
}
const handleSelectZoom = useCallback((id: string | null) => {
setSelectedZoomId(id);
if (id) {
+ setActiveEffectSection("zoom");
setSelectedTrimId(null);
setSelectedAudioId(null);
+ } else {
+ setActiveEffectSection((s) => (s === "zoom" ? "scene" : s));
}
}, []);
@@ -2297,47 +2420,57 @@ export default function VideoEditor() {
}
}, []);
- const handleZoomAdded = useCallback((span: Span) => {
- const id = `zoom-${nextZoomIdRef.current++}`;
- const newRegion: ZoomRegion = {
- id,
- startMs: Math.round(span.start),
- endMs: Math.round(span.end),
- depth: DEFAULT_ZOOM_DEPTH,
- focus: { cx: 0.5, cy: 0.5 },
- mode: "manual",
- };
- if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
- autoSuggestedVideoPathRef.current = videoPath;
- pendingFreshRecordingAutoZoomPathRef.current = null;
- }
- setZoomRegions((prev) => [...prev, newRegion]);
- setSelectedZoomId(id);
- setSelectedTrimId(null);
- setSelectedAnnotationId(null);
- extensionHost.emitEvent({ type: 'timeline:region-added', data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs } });
- }, [videoPath]);
+ const handleZoomAdded = useCallback(
+ (span: Span) => {
+ const id = `zoom-${nextZoomIdRef.current++}`;
+ const newRegion: ZoomRegion = {
+ id,
+ startMs: Math.round(span.start),
+ endMs: Math.round(span.end),
+ depth: DEFAULT_ZOOM_DEPTH,
+ focus: { cx: 0.5, cy: 0.5 },
+ mode: "manual",
+ };
+ if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
+ autoSuggestedVideoPathRef.current = videoPath;
+ pendingFreshRecordingAutoZoomPathRef.current = null;
+ }
+ setZoomRegions((prev) => [...prev, newRegion]);
+ setSelectedZoomId(id);
+ setSelectedTrimId(null);
+ setSelectedAnnotationId(null);
+ extensionHost.emitEvent({
+ type: "timeline:region-added",
+ data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs },
+ });
+ },
+ [videoPath],
+ );
- const handleZoomSuggested = useCallback((span: Span, focus: ZoomFocus) => {
- const id = `zoom-${nextZoomIdRef.current++}`;
- const newRegion: ZoomRegion = {
- id,
- startMs: Math.round(span.start),
- endMs: Math.round(span.end),
- depth: DEFAULT_AUTO_ZOOM_DEPTH,
- focus: clampFocusToDepth(focus, DEFAULT_AUTO_ZOOM_DEPTH),
- mode: "auto",
- };
- if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
- autoSuggestedVideoPathRef.current = videoPath;
- pendingFreshRecordingAutoZoomPathRef.current = null;
- }
- setZoomRegions((prev) => [...prev, newRegion]);
- setSelectedZoomId(id);
- setSelectedTrimId(null);
- setSelectedAnnotationId(null);
- extensionHost.emitEvent({ type: 'timeline:region-added', data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs } });
- }, [videoPath]);
+ const handleZoomSuggested = useCallback(
+ (span: Span, focus: ZoomFocus) => {
+ const id = `zoom-${nextZoomIdRef.current++}`;
+ const newRegion: ZoomRegion = {
+ id,
+ startMs: Math.round(span.start),
+ endMs: Math.round(span.end),
+ depth: DEFAULT_AUTO_ZOOM_DEPTH,
+ focus: clampFocusToDepth(focus, DEFAULT_AUTO_ZOOM_DEPTH),
+ mode: "auto",
+ };
+ if (videoPath && pendingFreshRecordingAutoZoomPathRef.current === videoPath) {
+ autoSuggestedVideoPathRef.current = videoPath;
+ pendingFreshRecordingAutoZoomPathRef.current = null;
+ }
+ setZoomRegions((prev) => [...prev, newRegion]);
+ // Don't auto-select suggested zooms — they follow cursor and don't need user interaction
+ extensionHost.emitEvent({
+ type: "timeline:region-added",
+ data: { id, startMs: newRegion.startMs, endMs: newRegion.endMs },
+ });
+ },
+ [videoPath],
+ );
useEffect(() => {
if (
@@ -2474,11 +2607,7 @@ export default function VideoEditor() {
(mode: ZoomMode) => {
if (!selectedZoomId) return;
setZoomRegions((prev) =>
- prev.map((region) =>
- region.id === selectedZoomId
- ? { ...region, mode }
- : region,
- ),
+ prev.map((region) => (region.id === selectedZoomId ? { ...region, mode } : region)),
);
},
[selectedZoomId],
@@ -2490,7 +2619,7 @@ export default function VideoEditor() {
if (selectedZoomId === id) {
setSelectedZoomId(null);
}
- extensionHost.emitEvent({ type: 'timeline:region-removed', data: { id } });
+ extensionHost.emitEvent({ type: "timeline:region-removed", data: { id } });
},
[selectedZoomId],
);
@@ -2508,73 +2637,111 @@ export default function VideoEditor() {
const handleSelectClip = useCallback((id: string | null) => {
setSelectedClipId(id);
if (id) {
+ setActiveEffectSection("clip");
setSelectedZoomId(null);
setSelectedAnnotationId(null);
setSelectedAudioId(null);
+ } else {
+ setActiveEffectSection((s) => (s === "clip" ? "scene" : s));
}
}, []);
- const handleClipSplit = useCallback(
- (splitMs: number) => {
- setClipRegions((prev) => {
- const target = prev.find((c) => splitMs > c.startMs && splitMs < c.endMs);
- if (!target) return prev;
- const leftId = `clip-${nextClipIdRef.current++}`;
- const rightId = `clip-${nextClipIdRef.current++}`;
- const left: ClipRegion = { id: leftId, startMs: target.startMs, endMs: Math.round(splitMs), speed: target.speed };
- const right: ClipRegion = { id: rightId, startMs: Math.round(splitMs), endMs: target.endMs, speed: target.speed };
- return prev.flatMap((c) => (c.id === target.id ? [left, right] : [c]));
- });
- },
- [],
- );
+ const handleClipSplit = useCallback((splitMs: number) => {
+ setClipRegions((prev) => {
+ const target = prev.find((c) => splitMs > c.startMs && splitMs < c.endMs);
+ if (!target) return prev;
+ const leftId = `clip-${nextClipIdRef.current++}`;
+ const rightId = `clip-${nextClipIdRef.current++}`;
+ const left: ClipRegion = {
+ id: leftId,
+ startMs: target.startMs,
+ endMs: Math.round(splitMs),
+ speed: target.speed,
+ };
+ const right: ClipRegion = {
+ id: rightId,
+ startMs: Math.round(splitMs),
+ endMs: target.endMs,
+ speed: target.speed,
+ };
+ return prev.flatMap((c) => (c.id === target.id ? [left, right] : [c]));
+ });
+ }, []);
- const handleClipSpanChange = useCallback((id: string, span: Span) => {
- const oldClip = clipRegions.find((c) => c.id === id);
- const newStart = Math.round(span.start);
- const newEnd = Math.round(span.end);
+ const handleClipSpanChange = useCallback(
+ (id: string, span: Span) => {
+ const oldClip = clipRegions.find((c) => c.id === id);
+ const newStart = Math.round(span.start);
+ const newEnd = Math.round(span.end);
- if (oldClip) {
- const startDelta = newStart - oldClip.startMs;
- const endDelta = newEnd - oldClip.endMs;
- const isMove = Math.abs(startDelta - endDelta) < 1 && Math.abs(startDelta) > 0;
+ if (oldClip) {
+ const startDelta = newStart - oldClip.startMs;
+ const endDelta = newEnd - oldClip.endMs;
+ const isMove = Math.abs(startDelta - endDelta) < 1 && Math.abs(startDelta) > 0;
- if (isMove) {
- const delta = startDelta;
- setZoomRegions((prev) =>
- prev.map((zoom) => {
- const overlaps = zoom.startMs < oldClip.endMs && zoom.endMs > oldClip.startMs;
- if (overlaps) {
- return {
- ...zoom,
- startMs: zoom.startMs + delta,
- endMs: zoom.endMs + delta,
- };
- }
- return zoom;
- }),
- );
+ if (isMove) {
+ const delta = startDelta;
+ setZoomRegions((prev) =>
+ prev.map((zoom) => {
+ const overlaps =
+ zoom.startMs < oldClip.endMs && zoom.endMs > oldClip.startMs;
+ if (overlaps) {
+ return {
+ ...zoom,
+ startMs: zoom.startMs + delta,
+ endMs: zoom.endMs + delta,
+ };
+ }
+ return zoom;
+ }),
+ );
+ }
}
- }
- setClipRegions((prev) =>
- prev.map((clip) =>
- clip.id === id
- ? { ...clip, startMs: newStart, endMs: newEnd }
- : clip,
- ),
- );
- }, [clipRegions]);
+ setClipRegions((prev) =>
+ prev.map((clip) =>
+ clip.id === id ? { ...clip, startMs: newStart, endMs: newEnd } : clip,
+ ),
+ );
+ },
+ [clipRegions],
+ );
const handleClipSpeedChange = useCallback(
(speed: number) => {
if (!selectedClipId) return;
+ const clip = clipRegions.find((c) => c.id === selectedClipId);
+ if (!clip) return;
+ const oldSpeed = clip.speed ?? 1;
+ const sourceDurationMs = (clip.endMs - clip.startMs) * oldSpeed;
+ const newEndMs = Math.round(clip.startMs + sourceDurationMs / speed);
+ const scaleFactor = oldSpeed / speed;
+
setClipRegions((prev) =>
- prev.map((clip) =>
- clip.id === selectedClipId
- ? { ...clip, speed }
- : clip,
- ),
+ prev.map((c) => (c.id === selectedClipId ? { ...c, speed, endMs: newEndMs } : c)),
+ );
+ // Scale zoom regions that lie within this clip proportionally
+ setZoomRegions((prev) =>
+ prev.map((zoom) => {
+ if (zoom.startMs < clip.startMs || zoom.startMs >= clip.endMs) return zoom;
+ return {
+ ...zoom,
+ startMs: Math.round(
+ clip.startMs + (zoom.startMs - clip.startMs) * scaleFactor,
+ ),
+ endMs: Math.round(clip.startMs + (zoom.endMs - clip.startMs) * scaleFactor),
+ };
+ }),
+ );
+ },
+ [selectedClipId, clipRegions],
+ );
+
+ const handleClipMutedChange = useCallback(
+ (muted: boolean) => {
+ if (!selectedClipId) return;
+ setClipRegions((prev) =>
+ prev.map((clip) => (clip.id === selectedClipId ? { ...clip, muted } : clip)),
);
},
[selectedClipId],
@@ -2649,7 +2816,7 @@ export default function VideoEditor() {
}
}, []);
- const handleAudioAdded = useCallback((span: Span, audioPath: string) => {
+ const handleAudioAdded = useCallback((span: Span, audioPath: string, trackIndex?: number) => {
const id = `audio-${nextAudioIdRef.current++}`;
const newRegion: AudioRegion = {
id,
@@ -2657,6 +2824,7 @@ export default function VideoEditor() {
endMs: Math.round(span.end),
audioPath,
volume: 1,
+ trackIndex,
};
setAudioRegions((prev) => [...prev, newRegion]);
setSelectedAudioId(id);
@@ -2694,13 +2862,15 @@ export default function VideoEditor() {
(speed: PlaybackSpeed) => {
if (!selectedSpeedId) return;
setSpeedRegions((prev) =>
- prev.map((region) => (region.id === selectedSpeedId ? { ...region, speed } : region)),
+ prev.map((region) =>
+ region.id === selectedSpeedId ? { ...region, speed } : region,
+ ),
);
},
[selectedSpeedId],
);
- const handleAnnotationAdded = useCallback((span: Span) => {
+ const handleAnnotationAdded = useCallback((span: Span, trackIndex = 0) => {
const id = `annotation-${nextAnnotationIdRef.current++}`;
const zIndex = nextAnnotationZIndexRef.current++; // Assign z-index based on creation order
const newRegion: AnnotationRegion = {
@@ -2713,6 +2883,7 @@ export default function VideoEditor() {
size: { ...DEFAULT_ANNOTATION_SIZE },
style: { ...DEFAULT_ANNOTATION_STYLE },
zIndex,
+ trackIndex,
};
setAnnotationRegions((prev) => [...prev, newRegion]);
setSelectedAnnotationId(id);
@@ -3192,7 +3363,9 @@ export default function VideoEditor() {
const result = await window.electronAPI.revealInFolder(filePath);
if (!result.success) {
const errorMessage =
- result.error || result.message || "Failed to reveal item in folder.";
+ result.error ||
+ result.message ||
+ "Failed to reveal item in folder.";
toast.error(errorMessage);
}
} catch (err) {
@@ -3220,7 +3393,7 @@ export default function VideoEditor() {
setExportProgress(null);
setExportError(null);
clearPendingExportSave();
- extensionHost.emitEvent({ type: 'export:start' });
+ extensionHost.emitEvent({ type: "export:start" });
const smokeExportStartedAt = smokeExportConfig.enabled ? performance.now() : null;
let keepExportDialogOpen = false;
@@ -3348,7 +3521,7 @@ export default function VideoEditor() {
? await window.electronAPI.writeExportedVideoToPath(
arrayBuffer,
smokeExportConfig.outputPath,
- )
+ )
: await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
if (saveResult.canceled) {
@@ -3391,28 +3564,30 @@ export default function VideoEditor() {
// MP4 Export
const quality = settings.quality ?? exportQuality;
const encodingMode = smokeExportConfig.enabled
- ? smokeExportConfig.encodingMode ?? settings.encodingMode ?? exportEncodingMode
- : settings.encodingMode ?? exportEncodingMode;
+ ? (smokeExportConfig.encodingMode ??
+ settings.encodingMode ??
+ exportEncodingMode)
+ : (settings.encodingMode ?? exportEncodingMode);
const selectedMp4FrameRate = settings.mp4FrameRate ?? mp4FrameRate;
const pipelineModel = smokeExportConfig.enabled
- ? smokeExportConfig.pipelineModel ??
- (smokeExportConfig.useNativeExport ? "modern" : "legacy")
- : settings.pipelineModel ?? exportPipelineModel;
+ ? (smokeExportConfig.pipelineModel ??
+ (smokeExportConfig.useNativeExport ? "modern" : "legacy"))
+ : (settings.pipelineModel ?? exportPipelineModel);
const backendPreference =
pipelineModel === "legacy"
? "webcodecs"
: smokeExportConfig.enabled
- ? smokeExportConfig.backendPreference ??
- (smokeExportConfig.useNativeExport ? "breeze" : "webcodecs")
+ ? (smokeExportConfig.backendPreference ??
+ (smokeExportConfig.useNativeExport ? "breeze" : "webcodecs"))
: "auto";
- const supportedSourceDimensions = await ensureSupportedMp4SourceDimensions(
- selectedMp4FrameRate,
- );
- const { width: exportWidth, height: exportHeight } = calculateMp4ExportDimensions(
- supportedSourceDimensions.width,
- supportedSourceDimensions.height,
- quality,
- );
+ const supportedSourceDimensions =
+ await ensureSupportedMp4SourceDimensions(selectedMp4FrameRate);
+ const { width: exportWidth, height: exportHeight } =
+ calculateMp4ExportDimensions(
+ supportedSourceDimensions.width,
+ supportedSourceDimensions.height,
+ quality,
+ );
let bitrate: number;
if (quality === "source") {
@@ -3504,9 +3679,9 @@ export default function VideoEditor() {
const exporter =
pipelineModel === "modern"
? new ModernVideoExporter({
- ...exporterConfig,
- backendPreference,
- })
+ ...exporterConfig,
+ backendPreference,
+ })
: new VideoExporter(exporterConfig);
exporterRef.current = exporter;
@@ -3527,7 +3702,7 @@ export default function VideoEditor() {
? await window.electronAPI.writeExportedVideoToPath(
arrayBuffer,
smokeExportConfig.outputPath,
- )
+ )
: await window.electronAPI.saveExportedVideo(arrayBuffer, fileName);
if (saveResult.canceled) {
@@ -3654,7 +3829,7 @@ export default function VideoEditor() {
window.close();
}
} finally {
- extensionHost.emitEvent({ type: 'export:complete' });
+ extensionHost.emitEvent({ type: "export:complete" });
setIsExporting(false);
exporterRef.current = null;
setShowExportDropdown(keepExportDialogOpen);
@@ -3932,19 +4107,24 @@ export default function VideoEditor() {
const isExportSaving = exportProgress?.phase === "saving";
const isExportFinalizing = exportProgress?.phase === "finalizing";
- const isRenderingAudio = isExportFinalizing && typeof exportProgress?.audioProgress === "number";
+ const isRenderingAudio =
+ isExportFinalizing && typeof exportProgress?.audioProgress === "number";
const exportFinalizingProgress = isExportFinalizing
? Math.min(
typeof exportProgress?.renderProgress === "number"
? exportProgress.renderProgress
: (exportProgress?.percentage ?? 99),
99,
- )
+ )
: null;
const isLightningExportInProgress =
- exportFormat === "mp4" && exportPipelineModel === "modern" && (isExporting || exportProgress !== null);
+ exportFormat === "mp4" &&
+ exportPipelineModel === "modern" &&
+ (isExporting || exportProgress !== null);
const isLegacyExportInProgress =
- exportFormat === "mp4" && exportPipelineModel === "legacy" && (isExporting || exportProgress !== null);
+ exportFormat === "mp4" &&
+ exportPipelineModel === "legacy" &&
+ (isExporting || exportProgress !== null);
const exportRenderSpeedLabel =
typeof exportProgress?.renderFps === "number" &&
Number.isFinite(exportProgress.renderFps) &&
@@ -3963,11 +4143,7 @@ export default function VideoEditor() {
}
const rendererLabel =
- renderBackend === "webgpu"
- ? "WebGPU"
- : renderBackend === "webgl"
- ? "WebGL"
- : null;
+ renderBackend === "webgpu" ? "WebGPU" : renderBackend === "webgl" ? "WebGL" : null;
const encoderLabel =
encodeBackend === "ffmpeg"
? "Breeze"
@@ -3977,7 +4153,7 @@ export default function VideoEditor() {
const pathLabel =
rendererLabel && encoderLabel
? `${rendererLabel} + ${encoderLabel}`
- : rendererLabel ?? encoderLabel;
+ : (rendererLabel ?? encoderLabel);
if (!pathLabel) {
return encoderName ?? null;
@@ -4052,7 +4228,6 @@ export default function VideoEditor() {
className={`flex items-center gap-1.5 justify-self-start ${headerLeftControlsPaddingClass}`}
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
-
-