diff --git a/src/components/video-editor/EditorContent.tsx b/src/components/video-editor/EditorContent.tsx index ef3693b4..69d153ef 100644 --- a/src/components/video-editor/EditorContent.tsx +++ b/src/components/video-editor/EditorContent.tsx @@ -16,7 +16,7 @@ import { useI18n } from "@/contexts/I18nContext"; import { ASPECT_RATIOS, getAspectRatioLabel, getAspectRatioValue } from "@/utils/aspectRatioUtils"; import { CropControl } from "./CropControl"; import { EditorToolbar } from "./EditorToolbar"; -import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor"; +import type { TimelineEditorHandle } from "./timeline/TimelineEditor"; import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback"; import type { CursorTelemetryPoint } from "./types"; import type { useEditorPreferences } from "./hooks/useEditorPreferences"; @@ -41,8 +41,6 @@ interface EditorContentProps { isCropped: boolean; hasSourceAudioFallback: boolean; effectiveCursorTelemetry: CursorTelemetryPoint[]; - normalizedCursorTelemetry: CursorTelemetryPoint[]; - autoSuggestZoomsTrigger: number; videoPlaybackRef: React.RefObject; timelineRef: React.RefObject; setDuration: (v: number) => void; @@ -56,7 +54,6 @@ interface EditorContentProps { handleOpenCropEditor: () => void; handleCloseCropEditor: () => void; handleCancelCropEditor: () => void; - handleAutoSuggestZoomsConsumed: () => void; } export function EditorContent({ @@ -73,8 +70,6 @@ export function EditorContent({ isCropped, hasSourceAudioFallback, effectiveCursorTelemetry, - normalizedCursorTelemetry, - autoSuggestZoomsTrigger, videoPlaybackRef, timelineRef, setDuration, @@ -88,13 +83,11 @@ export function EditorContent({ handleOpenCropEditor, handleCloseCropEditor, handleCancelCropEditor, - handleAutoSuggestZoomsConsumed, }: EditorContentProps) { const { t } = useI18n(); return ( - <> -
+
{/* Preview */}
@@ -248,55 +241,6 @@ export function EditorContent({ togglePlayPause={togglePlayPause} handleSeek={handleSeek} /> -
- {/* Timeline */} -
- } - hideToolbar - videoDuration={duration} - currentTime={currentTime} - playheadTime={regions.timelinePlayheadTime} - onSeek={handleSeek} - videoPath={videoPath} - cursorTelemetry={normalizedCursorTelemetry} - autoSuggestZoomsTrigger={autoSuggestZoomsTrigger} - onAutoSuggestZoomsConsumed={handleAutoSuggestZoomsConsumed} - zoomRegions={regions.zoomRegions} - onZoomAdded={regions.handleZoomAdded} - onZoomSuggested={regions.handleZoomSuggested} - onZoomSpanChange={regions.handleZoomSpanChange} - onZoomDelete={regions.handleZoomDelete} - selectedZoomId={regions.selectedZoomId} - onSelectZoom={regions.handleSelectZoom} - trimRegions={regions.trimRegions} - clipRegions={regions.clipRegions} - onClipSplit={regions.handleClipSplit} - onClipSpanChange={regions.handleClipSpanChange} - onClipDelete={regions.handleClipDelete} - selectedClipId={regions.selectedClipId} - onSelectClip={regions.handleSelectClip} - audioRegions={regions.audioRegions} - onAudioAdded={regions.handleAudioAdded} - onAudioSpanChange={regions.handleAudioSpanChange} - onAudioDelete={regions.handleAudioDelete} - selectedAudioId={regions.selectedAudioId} - onSelectAudio={regions.handleSelectAudio} - annotationRegions={regions.annotationRegions} - onAnnotationAdded={regions.handleAnnotationAdded} - onAnnotationSpanChange={regions.handleAnnotationSpanChange} - onAnnotationDelete={regions.handleAnnotationDelete} - selectedAnnotationId={regions.selectedAnnotationId} - onSelectAnnotation={regions.handleSelectAnnotation} - aspectRatio={prefs.aspectRatio} - /> -
{/* Crop modal */} {showCropModal ? ( <> @@ -341,6 +285,6 @@ export function EditorContent({
) : null} - +
); } diff --git a/src/components/video-editor/EditorHeader.tsx b/src/components/video-editor/EditorHeader.tsx index 3b15e8ca..daf04886 100644 --- a/src/components/video-editor/EditorHeader.tsx +++ b/src/components/video-editor/EditorHeader.tsx @@ -2,10 +2,9 @@ import { DownloadSimple as Download, FolderOpen, ArrowClockwise as Redo2, - FloppyDisk as Save, ArrowCounterClockwise as Undo2, } from "@phosphor-icons/react"; -import { useMemo } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -41,7 +40,6 @@ interface EditorHeaderProps { headerLeftControlsPaddingClass: string; mp4OutputDimensions: Record; gifOutputDimensions: { width: number; height: number }; - openRecordingsFolder: () => Promise; revealExportedFile: () => Promise; projectBrowserTriggerRef: React.RefObject; } @@ -55,13 +53,65 @@ export function EditorHeader({ headerLeftControlsPaddingClass, mp4OutputDimensions, gifOutputDimensions, - openRecordingsFolder, revealExportedFile, projectBrowserTriggerRef, }: EditorHeaderProps) { const { t } = useI18n(); + const [isEditingProjectName, setIsEditingProjectName] = useState(false); + const [projectNameDraft, setProjectNameDraft] = useState(projectDisplayName); + const [isSavingProjectName, setIsSavingProjectName] = useState(false); + const projectNameInputRef = useRef(null); + + useEffect(() => { + if (!isEditingProjectName) { + setProjectNameDraft(projectDisplayName); + } + }, [isEditingProjectName, projectDisplayName]); + + useEffect(() => { + if (!isEditingProjectName) { + return; + } + + const frameId = window.requestAnimationFrame(() => { + projectNameInputRef.current?.focus(); + projectNameInputRef.current?.select(); + }); + + return () => { + window.cancelAnimationFrame(frameId); + }; + }, [isEditingProjectName]); + + const closeProjectNameEditor = useCallback(() => { + setProjectNameDraft(projectDisplayName); + setIsEditingProjectName(false); + }, [projectDisplayName]); + + const handleProjectNameSubmit = useCallback( + async (event?: React.FormEvent) => { + event?.preventDefault(); + const trimmedProjectName = projectNameDraft.trim(); + if (!trimmedProjectName) { + closeProjectNameEditor(); + return; + } + + setIsSavingProjectName(true); + const saved = await project.saveProjectWithName(trimmedProjectName); + setIsSavingProjectName(false); + + if (saved) { + setIsEditingProjectName(false); + return; + } + + projectNameInputRef.current?.focus(); + projectNameInputRef.current?.select(); + }, + [closeProjectNameEditor, project, projectNameDraft], + ); - // ── Export derived labels ───────────────────────────────────────── const isLightningExportInProgress = prefs.exportFormat === "mp4" && prefs.exportPipelineModel === "modern" && @@ -123,7 +173,7 @@ export function EditorHeader({ return (
@@ -168,48 +219,66 @@ export function EditorHeader({
- - {projectDisplayName} - - - .recordly - + {isEditingProjectName ? ( +
void handleProjectNameSubmit(event)} + className="flex max-w-[min(52vw,460px)] items-baseline gap-1 rounded-[7px] border border-foreground/10 bg-editor-panel/[0.88] px-2.5 py-1 shadow-[0_10px_28px_rgba(0,0,0,0.18)]" + > + {project.hasUnsavedChanges ? ( + + ) : null} + setProjectNameDraft(event.target.value)} + onBlur={() => { + if (!isSavingProjectName) { + closeProjectNameEditor(); + } + }} + onKeyDown={(event) => { + if (event.key === "Escape") { + event.preventDefault(); + closeProjectNameEditor(); + } + }} + disabled={isSavingProjectName} + className="min-w-[10ch] max-w-[min(40vw,360px)] bg-transparent text-sm font-semibold tracking-tight text-foreground/95 outline-none placeholder:text-muted-foreground/60 disabled:cursor-wait" + style={{ width: `${Math.max(projectNameDraft.length, 10)}ch` }} + aria-label={t("editor.project.renameInput", "Project name")} + /> + + .recordly + + + ) : ( + + )}
- - -
)}
-

- {exportPercentLabel} -

+

{exportPercentLabel}

{exp.isRenderingAudio ? (

Audio requires real-time playback for speed/overlay edits @@ -340,7 +412,10 @@ export function EditorHeader({ {t("editor.exportStatus.complete", "Export complete")}

- {t("editor.exportStatus.savedSuccessfully", "Your file was saved successfully.")} + {t( + "editor.exportStatus.savedSuccessfully", + "Your file was saved successfully.", + )}

{exportRuntimeLabel ? (

@@ -397,4 +472,4 @@ export function EditorHeader({

); -} +} \ No newline at end of file diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index a8d6be2e..2324b5f3 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -23,7 +23,7 @@ import ProjectBrowserDialog from "./ProjectBrowserDialog"; import { fromFileUrl } from "./projectPersistence"; import type { CropRegion } from "./types"; import { VideoPlaybackRef } from "./VideoPlayback"; -import type { TimelineEditorHandle } from "./timeline/TimelineEditor"; +import TimelineEditor, { type TimelineEditorHandle } from "./timeline/TimelineEditor"; import { getSmokeExportConfig } from "./videoEditorUtils"; export default function VideoEditor() { @@ -293,16 +293,6 @@ export default function VideoEditor() { }, [prefs.cropRegion]); // ── Misc handlers ──────────────────────────────────────────────── - const openRecordingsFolder = useCallback(async () => { - try { - const result = await window.electronAPI.openRecordingsFolder(); - if (!result.success) - toast.error(result.message || result.error || "Failed to open recordings folder."); - } catch (err) { - toast.error(`Failed to open recordings folder: ${String(err)}`); - } - }, []); - const revealExportedFile = useCallback(async () => { if (!exp.exportedFilePath) return; try { @@ -368,12 +358,11 @@ export default function VideoEditor() { headerLeftControlsPaddingClass={headerLeftControlsPaddingClass} mp4OutputDimensions={wiring.mp4OutputDimensions} gifOutputDimensions={wiring.gifOutputDimensions} - openRecordingsFolder={openRecordingsFolder} revealExportedFile={revealExportedFile} projectBrowserTriggerRef={projectBrowserTriggerRef} />
-
+
+
+
+
diff --git a/src/components/video-editor/hooks/useEditorProject.ts b/src/components/video-editor/hooks/useEditorProject.ts index 9eba6c5f..15f2063d 100644 --- a/src/components/video-editor/hooks/useEditorProject.ts +++ b/src/components/video-editor/hooks/useEditorProject.ts @@ -39,6 +39,14 @@ interface UseEditorProjectParams { clearHistory: () => void; } +type SaveProjectResult = { + success: boolean; + path?: string; + message?: string; + canceled?: boolean; + error?: string; +}; + export function useEditorProject({ getCurrentPersistedState, getCurrentSourcePath, @@ -71,18 +79,50 @@ export function useEditorProject({ return JSON.stringify(current) !== JSON.stringify(lastSavedSnapshot); }, [getCurrentPersistedState, getCurrentSourcePath, getCurrentProjectPath, lastSavedSnapshot]); - const saveProject = useCallback( - async (forceSaveAs: boolean) => { - const sourcePath = getCurrentSourcePath(); - if (!sourcePath) { - toast.error("No video loaded"); + const prepareProjectSave = useCallback(async () => { + const sourcePath = getCurrentSourcePath(); + if (!sourcePath) { + toast.error("No video loaded"); + return null; + } + + const projectData = createProjectData(sourcePath, getCurrentPersistedState()); + const fileNameBase = + sourcePath.split(/[\\/]/).pop()?.replace(/\.[^.]+$/, "") || + `project-${Date.now()}`; + const thumbnailDataUrl = await captureProjectThumbnail(); + + return { + projectData, + fileNameBase, + thumbnailDataUrl, + }; + }, [captureProjectThumbnail, getCurrentPersistedState, getCurrentSourcePath]); + + const completeProjectSave = useCallback( + async (result: SaveProjectResult, projectData: EditorProjectData) => { + if (result.canceled) { + toast.info("Project save canceled"); return false; } + if (!result.success) { + toast.error(result.message || "Failed to save project"); + return false; + } + if (result.path) setCurrentProjectPath(result.path); + setLastSavedSnapshot(globalThis.structuredClone(projectData)); + await refreshProjectLibrary(); + toast.success(result.path ? `Project saved to ${result.path}` : "Project saved"); + return true; + }, + [refreshProjectLibrary, setCurrentProjectPath], + ); + + const saveProject = useCallback( + async (forceSaveAs: boolean) => { + const preparedSave = await prepareProjectSave(); + if (!preparedSave) return false; try { - const projectData = createProjectData(sourcePath, getCurrentPersistedState()); - const fileNameBase = - sourcePath.split(/[\\/]/).pop()?.replace(/\.[^.]+$/, "") || - `project-${Date.now()}`; let targetProjectPath = forceSaveAs ? undefined : (getCurrentProjectPath() ?? undefined); if (!forceSaveAs && !targetProjectPath) { @@ -93,42 +133,53 @@ export function useEditorProject({ } } - const thumbnailDataUrl = await captureProjectThumbnail(); const result = await window.electronAPI.saveProjectFile( - projectData, - fileNameBase, + preparedSave.projectData, + preparedSave.fileNameBase, targetProjectPath, - thumbnailDataUrl, + preparedSave.thumbnailDataUrl, ); - if (result.canceled) { - toast.info("Project save canceled"); - return false; - } - if (!result.success) { - toast.error(result.message || "Failed to save project"); - return false; - } - if (result.path) setCurrentProjectPath(result.path); - setLastSavedSnapshot(globalThis.structuredClone(projectData)); - await refreshProjectLibrary(); - toast.success(`Project saved to ${result.path}`); - return true; + return await completeProjectSave(result, preparedSave.projectData); } finally { remountPreview(); } }, [ - captureProjectThumbnail, - getCurrentPersistedState, - getCurrentSourcePath, + completeProjectSave, getCurrentProjectPath, + prepareProjectSave, setCurrentProjectPath, - refreshProjectLibrary, remountPreview, ], ); + const saveProjectWithName = useCallback( + async (projectName: string) => { + const trimmedProjectName = projectName.trim(); + if (!trimmedProjectName) { + toast.error("Project name is required"); + return false; + } + + const preparedSave = await prepareProjectSave(); + if (!preparedSave) return false; + + try { + const result = await window.electronAPI.saveProjectFileNamed( + preparedSave.projectData, + trimmedProjectName, + preparedSave.thumbnailDataUrl, + ); + + return await completeProjectSave(result, preparedSave.projectData); + } finally { + remountPreview(); + } + }, + [completeProjectSave, prepareProjectSave, remountPreview], + ); + /** Load and apply a project from a raw (possibly unknown) candidate value. */ const applyLoadedProject = useCallback( async (candidate: unknown, path?: string | null) => { @@ -218,6 +269,7 @@ export function useEditorProject({ setLastSavedSnapshot, hasUnsavedChanges, saveProject, + saveProjectWithName, handleSaveProject, handleSaveProjectAs, applyLoadedProject, diff --git a/src/components/video-editor/hooks/useEditorRegions.ts b/src/components/video-editor/hooks/useEditorRegions.ts index 0a4ffda9..455a9d9b 100644 --- a/src/components/video-editor/hooks/useEditorRegions.ts +++ b/src/components/video-editor/hooks/useEditorRegions.ts @@ -309,7 +309,7 @@ export function useEditorRegions({ }) => { setZoomRegions(editor.zoomRegions); setClipRegions(editor.clipRegions); - clipInitializedRef.current = true; + clipInitializedRef.current = editor.clipRegions.length > 0; resetAnnotationAudioForProject(editor); setSelectedZoomId(null); setSelectedClipId(null); diff --git a/src/components/video-editor/hooks/useEditorWiring.ts b/src/components/video-editor/hooks/useEditorWiring.ts index 9b6e9fae..41a542ab 100644 --- a/src/components/video-editor/hooks/useEditorWiring.ts +++ b/src/components/video-editor/hooks/useEditorWiring.ts @@ -76,6 +76,7 @@ export function useEditorWiring({ (snapshot: EditorHistorySnapshot) => { regions.setZoomRegions(snapshot.zoomRegions); regions.setClipRegions(snapshot.clipRegions); + regions.clipInitializedRef.current = snapshot.clipRegions.length > 0; regions.setAnnotationRegions(snapshot.annotationRegions); regions.setAudioRegions(snapshot.audioRegions); captions.setAutoCaptions(snapshot.autoCaptions); diff --git a/src/components/video-editor/videoPlaybackComponent/index.tsx b/src/components/video-editor/videoPlaybackComponent/index.tsx index a38eb70b..6c6ad3fa 100644 --- a/src/components/video-editor/videoPlaybackComponent/index.tsx +++ b/src/components/video-editor/videoPlaybackComponent/index.tsx @@ -221,8 +221,7 @@ const VideoPlayback = forwardRef(function videoReady, onTimeUpdate, onPlayStateChange, - layoutVideoContent: layout.layoutVideoContent, - updateOverlayForRegion: () => layout.updateOverlayForRegion(null), + updateOverlayForRegion: layout.updateOverlayForRegion, }); usePlaybackTicker({ diff --git a/src/components/video-editor/videoPlaybackComponent/usePixiVideoScene.ts b/src/components/video-editor/videoPlaybackComponent/usePixiVideoScene.ts index ba6e9c4b..97fc73e2 100644 --- a/src/components/video-editor/videoPlaybackComponent/usePixiVideoScene.ts +++ b/src/components/video-editor/videoPlaybackComponent/usePixiVideoScene.ts @@ -10,8 +10,7 @@ interface UsePixiVideoSceneParams { videoReady: boolean; onTimeUpdate: (time: number) => void; onPlayStateChange: (playing: boolean) => void; - layoutVideoContent: () => void; - updateOverlayForRegion: () => void; + updateOverlayForRegion: (region: null) => void; } export function usePixiVideoScene({ @@ -20,7 +19,6 @@ export function usePixiVideoScene({ videoReady, onTimeUpdate, onPlayStateChange, - layoutVideoContent, updateOverlayForRegion, }: UsePixiVideoSceneParams) { useEffect(() => { @@ -64,7 +62,7 @@ export function usePixiVideoScene({ refs.blurFilterRef.current = blurFilter; refs.motionBlurFilterRef.current = motionBlurFilter; - layoutVideoContent(); + refs.layoutVideoContentRef.current?.(); video.pause(); const { handlePlay, handlePause, handleSeeked, handleSeeking } = createVideoEventHandlers({ @@ -118,7 +116,7 @@ export function usePixiVideoScene({ } videoTexture.destroy(false); refs.videoSpriteRef.current = null; - updateOverlayForRegion(); + updateOverlayForRegion(null); }; - }, [layoutVideoContent, onPlayStateChange, onTimeUpdate, pixiReady, refs, updateOverlayForRegion, videoReady]); + }, [onPlayStateChange, onTimeUpdate, pixiReady, refs, updateOverlayForRegion, videoReady]); } \ No newline at end of file diff --git a/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackRefs.ts b/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackRefs.ts index 4610b2a4..47230d34 100644 --- a/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackRefs.ts +++ b/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackRefs.ts @@ -83,79 +83,156 @@ export function useVideoPlaybackRefs({ "image" | "video" | "style" >("image"); - const refs: VideoPlaybackRuntimeRefs = { - videoRef: useRef(null), - containerRef: useRef(null), - appRef: useRef(null), - videoSpriteRef: useRef(null), - videoContainerRef: useRef(null), - cursorContainerRef: useRef(null), - cameraContainerRef: useRef(null), - timeUpdateAnimationRef: useRef(null), - overlayRef: useRef(null), - focusIndicatorRef: useRef(null), - webcamVideoRef: useRef(null), - webcamBubbleRef: useRef(null), - webcamBubbleInnerRef: useRef(null), - captionBoxRef: useRef(null), - currentTimeRef: useRef(0), - zoomRegionsRef: useRef(zoomRegions), - selectedZoomIdRef: useRef(selectedZoomId), - animationStateRef: useRef(createPlaybackAnimationState()), - blurFilterRef: useRef(null), - motionBlurFilterRef: useRef(null), - isDraggingFocusRef: useRef(false), - stageSizeRef: useRef({ width: 0, height: 0 }), - videoSizeRef: useRef({ width: 0, height: 0 }), - baseScaleRef: useRef(1), - baseOffsetRef: useRef({ x: 0, y: 0 }), - baseMaskRef: useRef({ x: 0, y: 0, width: 0, height: 0 }), - cropBoundsRef: useRef({ startX: 0, endX: 0, startY: 0, endY: 0 }), - maskGraphicsRef: useRef(null), - frameSpriteRef: useRef(null), - frameContainerRef: useRef(null), - frameIdRef: useRef(frame), - isPlayingRef: useRef(isPlaying), - isSeekingRef: useRef(false), - allowPlaybackRef: useRef(false), - lockedVideoDimensionsRef: useRef<{ width: number; height: number } | null>(null), - layoutVideoContentRef: useRef<(() => void) | null>(null), - trimRegionsRef: useRef(trimRegions), - speedRegionsRef: useRef(speedRegions), - lastWebcamSyncTimeRef: useRef(null), - bgVideoRef: useRef(null), - zoomMotionBlurRef: useRef(zoomMotionBlur), - connectZoomsRef: useRef(connectZooms), - zoomInDurationMsRef: useRef(zoomInDurationMs), - zoomInOverlapMsRef: useRef(zoomInOverlapMs), - zoomOutDurationMsRef: useRef(zoomOutDurationMs), - connectedZoomGapMsRef: useRef(connectedZoomGapMs), - connectedZoomDurationMsRef: useRef(connectedZoomDurationMs), - zoomInEasingRef: useRef(zoomInEasing), - zoomOutEasingRef: useRef(zoomOutEasing), - connectedZoomEasingRef: useRef(connectedZoomEasing), - videoReadyRafRef: useRef(null), - cursorOverlayRef: useRef(null), - cursorEffectsCanvasRef: useRef(null), - cursorTelemetryRef: useRef(cursorTelemetry), - showCursorRef: useRef(showCursor), - cursorSizeRef: useRef(cursorSize), - cursorStyleRef: useRef(cursorStyle), - cursorSmoothingRef: useRef(cursorSmoothing), - cursorMotionBlurRef: useRef(cursorMotionBlur), - cursorClickBounceRef: useRef(cursorClickBounce), - cursorClickBounceDurationRef: useRef(cursorClickBounceDuration), - cursorSwayRef: useRef(cursorSway), - lastEmittedClickTimeMsRef: useRef(-1), - springScaleRef: useRef(createSpringState(1)), - springXRef: useRef(createSpringState(0)), - springYRef: useRef(createSpringState(0)), - lastTickTimeRef: useRef(null), - zoomSmoothnessRef: useRef(zoomSmoothness), - zoomClassicModeRef: useRef(zoomClassicMode), - cursorFollowCameraRef: useRef(createCursorFollowCameraState()), - motionBlurStateRef: useRef(createMotionBlurState()), - }; + const videoRef = useRef(null); + const containerRef = useRef(null); + const appRef = useRef(null); + const videoSpriteRef = useRef(null); + const videoContainerRef = useRef(null); + const cursorContainerRef = useRef(null); + const cameraContainerRef = useRef(null); + const timeUpdateAnimationRef = useRef(null); + const overlayRef = useRef(null); + const focusIndicatorRef = useRef(null); + const webcamVideoRef = useRef(null); + const webcamBubbleRef = useRef(null); + const webcamBubbleInnerRef = useRef(null); + const captionBoxRef = useRef(null); + const currentTimeRef = useRef(0); + const zoomRegionsRef = useRef(zoomRegions); + const selectedZoomIdRef = useRef(selectedZoomId); + const animationStateRef = useRef(createPlaybackAnimationState()); + const blurFilterRef = useRef(null); + const motionBlurFilterRef = useRef(null); + const isDraggingFocusRef = useRef(false); + const stageSizeRef = useRef({ width: 0, height: 0 }); + const videoSizeRef = useRef({ width: 0, height: 0 }); + const baseScaleRef = useRef(1); + const baseOffsetRef = useRef({ x: 0, y: 0 }); + const baseMaskRef = useRef({ x: 0, y: 0, width: 0, height: 0 }); + const cropBoundsRef = useRef({ startX: 0, endX: 0, startY: 0, endY: 0 }); + const maskGraphicsRef = useRef(null); + const frameSpriteRef = useRef(null); + const frameContainerRef = useRef(null); + const frameIdRef = useRef(frame); + const isPlayingRef = useRef(isPlaying); + const isSeekingRef = useRef(false); + const allowPlaybackRef = useRef(false); + const lockedVideoDimensionsRef = useRef<{ width: number; height: number } | null>(null); + const layoutVideoContentRef = useRef<(() => void) | null>(null); + const trimRegionsRef = useRef(trimRegions); + const speedRegionsRef = useRef(speedRegions); + const lastWebcamSyncTimeRef = useRef(null); + const bgVideoRef = useRef(null); + const zoomMotionBlurRef = useRef(zoomMotionBlur); + const connectZoomsRef = useRef(connectZooms); + const zoomInDurationMsRef = useRef(zoomInDurationMs); + const zoomInOverlapMsRef = useRef(zoomInOverlapMs); + const zoomOutDurationMsRef = useRef(zoomOutDurationMs); + const connectedZoomGapMsRef = useRef(connectedZoomGapMs); + const connectedZoomDurationMsRef = useRef(connectedZoomDurationMs); + const zoomInEasingRef = useRef(zoomInEasing); + const zoomOutEasingRef = useRef(zoomOutEasing); + const connectedZoomEasingRef = useRef(connectedZoomEasing); + const videoReadyRafRef = useRef(null); + const cursorOverlayRef = useRef(null); + const cursorEffectsCanvasRef = useRef(null); + const cursorTelemetryRef = useRef(cursorTelemetry); + const showCursorRef = useRef(showCursor); + const cursorSizeRef = useRef(cursorSize); + const cursorStyleRef = useRef(cursorStyle); + const cursorSmoothingRef = useRef(cursorSmoothing); + const cursorMotionBlurRef = useRef(cursorMotionBlur); + const cursorClickBounceRef = useRef(cursorClickBounce); + const cursorClickBounceDurationRef = useRef(cursorClickBounceDuration); + const cursorSwayRef = useRef(cursorSway); + const lastEmittedClickTimeMsRef = useRef(-1); + const springScaleRef = useRef(createSpringState(1)); + const springXRef = useRef(createSpringState(0)); + const springYRef = useRef(createSpringState(0)); + const lastTickTimeRef = useRef(null); + const zoomSmoothnessRef = useRef(zoomSmoothness); + const zoomClassicModeRef = useRef(zoomClassicMode); + const cursorFollowCameraRef = useRef(createCursorFollowCameraState()); + const motionBlurStateRef = useRef(createMotionBlurState()); + + const refsRef = useRef(null); + if (!refsRef.current) { + refsRef.current = { + videoRef, + containerRef, + appRef, + videoSpriteRef, + videoContainerRef, + cursorContainerRef, + cameraContainerRef, + timeUpdateAnimationRef, + overlayRef, + focusIndicatorRef, + webcamVideoRef, + webcamBubbleRef, + webcamBubbleInnerRef, + captionBoxRef, + currentTimeRef, + zoomRegionsRef, + selectedZoomIdRef, + animationStateRef, + blurFilterRef, + motionBlurFilterRef, + isDraggingFocusRef, + stageSizeRef, + videoSizeRef, + baseScaleRef, + baseOffsetRef, + baseMaskRef, + cropBoundsRef, + maskGraphicsRef, + frameSpriteRef, + frameContainerRef, + frameIdRef, + isPlayingRef, + isSeekingRef, + allowPlaybackRef, + lockedVideoDimensionsRef, + layoutVideoContentRef, + trimRegionsRef, + speedRegionsRef, + lastWebcamSyncTimeRef, + bgVideoRef, + zoomMotionBlurRef, + connectZoomsRef, + zoomInDurationMsRef, + zoomInOverlapMsRef, + zoomOutDurationMsRef, + connectedZoomGapMsRef, + connectedZoomDurationMsRef, + zoomInEasingRef, + zoomOutEasingRef, + connectedZoomEasingRef, + videoReadyRafRef, + cursorOverlayRef, + cursorEffectsCanvasRef, + cursorTelemetryRef, + showCursorRef, + cursorSizeRef, + cursorStyleRef, + cursorSmoothingRef, + cursorMotionBlurRef, + cursorClickBounceRef, + cursorClickBounceDurationRef, + cursorSwayRef, + lastEmittedClickTimeMsRef, + springScaleRef, + springXRef, + springYRef, + lastTickTimeRef, + zoomSmoothnessRef, + zoomClassicModeRef, + cursorFollowCameraRef, + motionBlurStateRef, + }; + } + + const refs = refsRef.current; return { refs, diff --git a/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackSync.ts b/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackSync.ts index 34e5f248..8b19597f 100644 --- a/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackSync.ts +++ b/src/components/video-editor/videoPlaybackComponent/useVideoPlaybackSync.ts @@ -296,6 +296,7 @@ export function useVideoPlaybackSync({ const videoStage = refs.videoContainerRef.current; const sprite = refs.videoSpriteRef.current; const currentApp = refs.appRef.current; + const layout = refs.layoutVideoContentRef.current; if (!container || !videoStage || !sprite || !currentApp) return; container.scale.set(1); @@ -305,7 +306,7 @@ export function useVideoPlaybackSync({ sprite.scale.set(1); sprite.position.set(0, 0); - layoutVideoContent(); + layout?.(); requestAnimationFrame(() => { const finalApp = refs.appRef.current; @@ -317,7 +318,12 @@ export function useVideoPlaybackSync({ } }); }); - }, [layoutVideoContent, pixiReady, refs, videoReady]); + }, [pixiReady, refs, videoPath, videoReady]); + + useEffect(() => { + if (!pixiReady || !videoReady) return; + layoutVideoContent(); + }, [layoutVideoContent, pixiReady, videoReady]); useEffect(() => { if (!pixiReady || !videoReady) return;