diff --git a/electron/ipc/cursor/telemetry.ts b/electron/ipc/cursor/telemetry.ts index 9191542a..2c14705a 100644 --- a/electron/ipc/cursor/telemetry.ts +++ b/electron/ipc/cursor/telemetry.ts @@ -34,8 +34,9 @@ export function normalizeCursorTelemetrySamples(rawSamples: unknown): CursorTele : Array.isArray((rawSamples as { samples?: unknown[] } | null | undefined)?.samples) ? ((rawSamples as { samples: unknown[] }).samples ?? []) : []; + const boundedSamples = samples.slice(0, MAX_CURSOR_SAMPLES); - return samples + return boundedSamples .filter((sample: unknown) => Boolean(sample && typeof sample === "object")) .map((sample: unknown) => { const point = sample as Partial; diff --git a/electron/ipc/register/project.ts b/electron/ipc/register/project.ts index a18d263d..7074d97f 100644 --- a/electron/ipc/register/project.ts +++ b/electron/ipc/register/project.ts @@ -541,25 +541,27 @@ export function registerProjectHandlers() { timeOffsetMs: 0, } - setCurrentRecordingSession({ + const nextSession = { ...resolvedSession, hideOverlayCursorByDefault: normalizeBoolean(options?.hideOverlayCursorByDefault) || normalizeBoolean(resolvedSession.hideOverlayCursorByDefault), - }) + } + + setCurrentRecordingSession(nextSession) await replaceApprovedSessionLocalReadPaths([ resolvedSession.videoPath, resolvedSession.webcamPath, ]) - if (resolvedSession.webcamPath) { - await persistRecordingSessionManifest(resolvedSession) + if (nextSession.webcamPath) { + await persistRecordingSessionManifest(nextSession) } if (!options?.preserveProjectPath) { setCurrentProjectPath(null) } - return { success: true, webcamPath: resolvedSession.webcamPath ?? null } + return { success: true, webcamPath: nextSession.webcamPath ?? null } }) ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean }, options?: { preserveProjectPath?: boolean }) => { diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 0b680c20..c857f942 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -1448,9 +1448,9 @@ export function SettingsPanel({ const resetZoomSection = () => { onZoomTemporalMotionBlurChange?.(initialEditorPreferences.zoomTemporalMotionBlur); - onZoomMotionBlurSampleCountChange?.(initialEditorPreferences.zoomMotionBlurSampleCount); + onZoomMotionBlurSampleCountChange?.(TEMPORAL_MOTION_BLUR_DEFAULT_SAMPLE_COUNT); onZoomMotionBlurShutterFractionChange?.( - initialEditorPreferences.zoomMotionBlurShutterFraction, + TEMPORAL_MOTION_BLUR_DEFAULT_SHUTTER_FRACTION, ); onZoomInDurationMsChange?.(initialEditorPreferences.zoomInDurationMs); onZoomOutDurationMsChange?.(initialEditorPreferences.zoomOutDurationMs); @@ -1481,7 +1481,6 @@ export function SettingsPanel({ MOTION_PRESET_ORDER.find((presetId) => { const preset = CURSOR_MOTION_PRESETS[presetId]; return ( - preset.zoomSmoothness === DEFAULT_ZOOM_SMOOTHNESS && preset.zoomInDurationMs === zoomInDurationMs && preset.zoomOutDurationMs === zoomOutDurationMs && preset.cursorSize === cursorSize && diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index 864110bb..48214b92 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -77,6 +77,7 @@ import { getAspectRatioLabel, getAspectRatioValue, } from "@/utils/aspectRatioUtils"; +import { cn } from "@/lib/utils"; import { ExtensionIcon } from "./ExtensionIcon"; const PhCursorFill = (props: { className?: string; weight?: "fill" | "regular" }) => ( @@ -103,7 +104,10 @@ import { resolveAutoCaptionSourcePath } from "./autoCaptionSource"; import { CropControl } from "./CropControl"; import { ExportSettingsMenu } from "./ExportSettingsMenu"; import ExtensionManager from "./ExtensionManager"; -import { loadEditorPreferences, saveEditorPreferences } from "./editorPreferences"; +import { + loadEditorPreferences, + saveEditorPreferences, +} from "./editorPreferences"; import ProjectBrowserDialog, { type ProjectLibraryEntry } from "./ProjectBrowserDialog"; import { createProjectData, @@ -622,6 +626,18 @@ export default function VideoEditor() { const [cursorSmoothing, setCursorSmoothing] = useState( initialEditorPreferences.cursorSmoothing, ); + const [cursorSpringStiffnessMultiplier, setCursorSpringStiffnessMultiplier] = useState( + initialEditorPreferences.cursorSpringStiffnessMultiplier, + ); + const [cursorSpringDampingMultiplier, setCursorSpringDampingMultiplier] = useState( + initialEditorPreferences.cursorSpringDampingMultiplier, + ); + const [cursorSpringMassMultiplier, setCursorSpringMassMultiplier] = useState( + initialEditorPreferences.cursorSpringMassMultiplier, + ); + const [sessionShowCursorOverride, setSessionShowCursorOverride] = useState( + null, + ); const [zoomSmoothness, setZoomSmoothness] = useState(0.5); const [zoomClassicMode, setZoomClassicMode] = useState(false); const [cursorMotionBlur, setCursorMotionBlur] = useState( @@ -685,6 +701,7 @@ export default function VideoEditor() { const [sourceAudioFallbackPaths, setSourceAudioFallbackPaths] = useState([]); const [sourceAudioFallbackStartDelayMsByPath, setSourceAudioFallbackStartDelayMsByPath] = useState>({}); + const effectiveShowCursor = sessionShowCursorOverride ?? showCursor; const [aspectRatio, setAspectRatio] = useState( initialEditorPreferences.aspectRatio, ); @@ -917,10 +934,13 @@ export default function VideoEditor() { previewWidth, previewHeight, cursorTelemetry, - showCursor, + effectiveShowCursor, cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, zoomSmoothness, zoomClassicMode, cursorMotionBlur, @@ -996,6 +1016,9 @@ export default function VideoEditor() { cursorMotionBlur, cursorSize, cursorSmoothing, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, + cursorSpringStiffnessMultiplier, zoomSmoothness, cursorStyle, cursorSway, @@ -1004,7 +1027,7 @@ export default function VideoEditor() { padding, resolvedWebcamVideoUrl, shadowIntensity, - showCursor, + effectiveShowCursor, speedRegions, wallpaper, webcam, @@ -1032,6 +1055,11 @@ export default function VideoEditor() { })); }, []); + const handleShowCursorChange = useCallback((nextShowCursor: boolean) => { + setSessionShowCursorOverride(null); + setShowCursor(nextShowCursor); + }, []); + const remountPreview = useCallback(() => { setIsPreviewReady(false); setPreviewVersion((version) => version + 1); @@ -1323,6 +1351,9 @@ export default function VideoEditor() { cursorStyle: CursorStyle; cursorSize: number; cursorSmoothing: number; + cursorSpringStiffnessMultiplier: number; + cursorSpringDampingMultiplier: number; + cursorSpringMassMultiplier: number; zoomSmoothness: number; zoomClassicMode: boolean; cursorMotionBlur: number; @@ -1471,6 +1502,9 @@ export default function VideoEditor() { cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, zoomSmoothness, zoomClassicMode, cursorMotionBlur, @@ -1521,6 +1555,9 @@ export default function VideoEditor() { cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, zoomSmoothness, zoomClassicMode, cursorMotionBlur, @@ -1694,11 +1731,19 @@ export default function VideoEditor() { setZoomInEasing(normalizedEditor.zoomInEasing); setZoomOutEasing(normalizedEditor.zoomOutEasing); setConnectedZoomEasing(normalizedEditor.connectedZoomEasing); + setSessionShowCursorOverride(null); setShowCursor(normalizedEditor.showCursor); setLoopCursor(normalizedEditor.loopCursor); setCursorStyle(normalizedEditor.cursorStyle); setCursorSize(normalizedEditor.cursorSize); setCursorSmoothing(normalizedEditor.cursorSmoothing); + setCursorSpringStiffnessMultiplier( + normalizedEditor.cursorSpringStiffnessMultiplier, + ); + setCursorSpringDampingMultiplier( + normalizedEditor.cursorSpringDampingMultiplier, + ); + setCursorSpringMassMultiplier(normalizedEditor.cursorSpringMassMultiplier); setZoomSmoothness(normalizedEditor.zoomSmoothness); setZoomClassicMode(normalizedEditor.zoomClassicMode); setCursorMotionBlur(normalizedEditor.cursorMotionBlur); @@ -2005,9 +2050,9 @@ export default function VideoEditor() { pendingFreshRecordingAutoZoomPathRef.current = autoApplyFreshRecordingAutoZooms ? sourceVideoUrl : null; - if (sessionResult.session.hideOverlayCursorByDefault) { - setShowCursor(false); - } + setSessionShowCursorOverride( + sessionResult.session.hideOverlayCursorByDefault ? false : null, + ); setWebcam((prev) => ({ ...prev, enabled: Boolean(sessionResult.session?.webcamPath), @@ -2027,6 +2072,7 @@ export default function VideoEditor() { setCurrentProjectPath(null); setLastSavedSnapshot(null); pendingFreshRecordingAutoZoomPathRef.current = null; + setSessionShowCursorOverride(null); setWebcam((prev) => ({ ...prev, enabled: false, @@ -2097,6 +2143,9 @@ export default function VideoEditor() { cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, cursorMotionBlur, cursorClickBounce, cursorClickBounceDuration, @@ -2138,6 +2187,9 @@ export default function VideoEditor() { cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, cursorMotionBlur, cursorClickBounce, cursorClickBounceDuration, @@ -3975,10 +4027,13 @@ export default function VideoEditor() { autoCaptionSettings, zoomRegions: effectiveZoomRegions, cursorTelemetry: effectiveCursorTelemetry, - showCursor, + effectiveShowCursor, cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, zoomSmoothness, zoomClassicMode, cursorMotionBlur, @@ -4147,10 +4202,13 @@ export default function VideoEditor() { autoCaptionSettings, zoomRegions: effectiveZoomRegions, cursorTelemetry: effectiveCursorTelemetry, - showCursor, + effectiveShowCursor, cursorStyle, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, zoomSmoothness, zoomClassicMode, cursorMotionBlur, @@ -4383,11 +4441,14 @@ export default function VideoEditor() { zoomInEasing, zoomOutEasing, connectedZoomEasing, - showCursor, + effectiveShowCursor, cursorStyle, effectiveCursorTelemetry, cursorSize, cursorSmoothing, + cursorSpringStiffnessMultiplier, + cursorSpringDampingMultiplier, + cursorSpringMassMultiplier, zoomSmoothness, zoomClassicMode, cursorMotionBlur, @@ -5255,8 +5316,8 @@ export default function VideoEditor() { onZoomOutEasingChange={setZoomOutEasing} connectedZoomEasing={connectedZoomEasing} onConnectedZoomEasingChange={setConnectedZoomEasing} - showCursor={showCursor} - onShowCursorChange={setShowCursor} + showCursor={effectiveShowCursor} + onShowCursorChange={handleShowCursorChange} loopCursor={loopCursor} onLoopCursorChange={setLoopCursor} cursorStyle={cursorStyle} @@ -5265,6 +5326,16 @@ export default function VideoEditor() { onCursorSizeChange={setCursorSize} cursorSmoothing={cursorSmoothing} onCursorSmoothingChange={setCursorSmoothing} + cursorSpringStiffnessMultiplier={cursorSpringStiffnessMultiplier} + onCursorSpringStiffnessMultiplierChange={ + setCursorSpringStiffnessMultiplier + } + cursorSpringDampingMultiplier={cursorSpringDampingMultiplier} + onCursorSpringDampingMultiplierChange={ + setCursorSpringDampingMultiplier + } + cursorSpringMassMultiplier={cursorSpringMassMultiplier} + onCursorSpringMassMultiplierChange={setCursorSpringMassMultiplier} zoomClassicMode={zoomClassicMode} onZoomClassicModeChange={setZoomClassicMode} cursorMotionBlur={cursorMotionBlur} @@ -5455,10 +5526,17 @@ export default function VideoEditor() { } onAnnotationSizeChange={handleAnnotationSizeChange} cursorTelemetry={effectiveCursorTelemetry} - showCursor={showCursor} + showCursor={effectiveShowCursor} cursorStyle={cursorStyle} cursorSize={cursorSize} cursorSmoothing={cursorSmoothing} + cursorSpringStiffnessMultiplier={ + cursorSpringStiffnessMultiplier + } + cursorSpringDampingMultiplier={ + cursorSpringDampingMultiplier + } + cursorSpringMassMultiplier={cursorSpringMassMultiplier} zoomSmoothness={zoomSmoothness} zoomClassicMode={zoomClassicMode} cursorMotionBlur={cursorMotionBlur} diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 11002c6a..0721442a 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -1102,7 +1102,7 @@ const VideoPlayback = forwardRef( return; } - videoEffectsContainer.filters = null; + videoEffectsContainer.filters = [zoomBlurFilter, motionBlurFilter]; motionBlurFilter.velocity = { x: 0, y: 0 }; motionBlurFilter.kernelSize = 5; motionBlurFilter.offset = 0; @@ -1443,6 +1443,10 @@ const VideoPlayback = forwardRef( videoEffectsContainerRef.current = videoEffectsContainer; zoomBlurFilterRef.current = new ZoomBlurFilter({ strength: 0 }); motionBlurFilterRef.current = new MotionBlurFilter([0, 0], 5, 0); + videoEffectsContainer.filters = [ + zoomBlurFilterRef.current, + motionBlurFilterRef.current, + ]; cameraContainer.addChild(videoEffectsContainer); syncPreviewMotionBlurQuality(); diff --git a/src/components/video-editor/editorPreferences.ts b/src/components/video-editor/editorPreferences.ts index 84b2a0c3..d155a687 100644 --- a/src/components/video-editor/editorPreferences.ts +++ b/src/components/video-editor/editorPreferences.ts @@ -443,14 +443,16 @@ export function loadEditorPresets(): EditorPreset[] { } } -export function saveEditorPresets(presets: EditorPreset[]): void { +export function saveEditorPresets(presets: EditorPreset[]): boolean { if (typeof globalThis.localStorage === "undefined") { - return; + return false; } try { globalThis.localStorage.setItem(EDITOR_PRESETS_STORAGE_KEY, JSON.stringify(presets)); + return true; } catch { // Ignore storage failures so editor controls still work. + return false; } } diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 073242f5..740d01cb 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -1476,21 +1476,18 @@ const TimelineEditor = forwardRef( } const startPos = Math.max(0, Math.min(startMs, totalMs)); - const activeClip = clipRegions.find( - (clip) => startPos >= clip.startMs && startPos < clip.endMs, - ); - const nextClip = clipRegions.find((clip) => clip.startMs > startPos); + const activeClip = + clipRegions.length === 0 + ? { startMs: 0, endMs: totalMs } + : clipRegions.find((clip) => startPos >= clip.startMs && startPos < clip.endMs); + if (!activeClip) { + return false; + } const sorted = [...zoomRegions].sort((a, b) => a.startMs - b.startMs); const nextRegion = sorted.find((region) => region.startMs > startPos); - const gapToNextClipEdge = clipRegions.length === 0 - ? totalMs - startPos - : activeClip - ? activeClip.endMs - startPos - : nextClip - ? nextClip.startMs - startPos - : 0; - const gapToNextRegion = nextRegion ? nextRegion.startMs - startPos : totalMs - startPos; + const gapToNextClipEdge = activeClip.endMs - startPos; + const gapToNextRegion = nextRegion ? nextRegion.startMs - startPos : gapToNextClipEdge; const availableDuration = Math.min(gapToNextClipEdge, gapToNextRegion); const isOverlapping = sorted.some( diff --git a/src/components/video-editor/videoPlayback/cursorFollowCamera.test.ts b/src/components/video-editor/videoPlayback/cursorFollowCamera.test.ts index 8493623a..ecde93ad 100644 --- a/src/components/video-editor/videoPlayback/cursorFollowCamera.test.ts +++ b/src/components/video-editor/videoPlayback/cursorFollowCamera.test.ts @@ -72,9 +72,9 @@ describe("computeCursorFollowFocus", () => { { snapToEdgesRatio: 0.25 }, ); - expect(firstShift.cx).toBeCloseTo(0.7, 6); + expect(firstShift.cx).toBeCloseTo(0.575, 6); expect(firstShift.cy).toBeCloseTo(0.5, 6); - expect(secondShift.cx).toBeCloseTo(0.7, 6); + expect(secondShift.cx).toBeCloseTo(0.595, 6); expect(secondShift.cy).toBeCloseTo(0.5, 6); }); @@ -107,4 +107,45 @@ describe("computeCursorFollowFocus", () => { expect(clampedFocus).toEqual({ cx: 0.75, cy: 0.75 }); }); + + it("reinitializes focus when time goes backward during zoom-out", () => { + const state = createCursorFollowCameraState(); + const cursorSamples = [ + { timeMs: 0, cx: 0.5, cy: 0.5, interactionType: "move" as const }, + { timeMs: 100, cx: 0.8, cy: 0.5, interactionType: "move" as const }, + ]; + + computeCursorFollowFocus( + state, + cursorSamples, + 100, + 2, + 1, + { cx: 0.5, cy: 0.5 }, + { snapToEdgesRatio: 0.25 }, + ); + + const frozenFocus = computeCursorFollowFocus( + state, + cursorSamples, + 140, + 2, + 0.8, + { cx: 0.5, cy: 0.5 }, + { snapToEdgesRatio: 0.25 }, + ); + + const rewoundFocus = computeCursorFollowFocus( + state, + cursorSamples, + 0, + 2, + 0.8, + { cx: 0.5, cy: 0.5 }, + { snapToEdgesRatio: 0.25 }, + ); + + expect(frozenFocus.cx).toBeCloseTo(0.675, 6); + expect(rewoundFocus).toEqual({ cx: 0.5, cy: 0.5 }); + }); }); \ No newline at end of file diff --git a/src/components/video-editor/videoPlayback/cursorFollowCamera.ts b/src/components/video-editor/videoPlayback/cursorFollowCamera.ts index 75106a81..ced118a8 100644 --- a/src/components/video-editor/videoPlayback/cursorFollowCamera.ts +++ b/src/components/video-editor/videoPlayback/cursorFollowCamera.ts @@ -100,15 +100,15 @@ function recenterFocusWhenCursorLeavesSafeZone( let nextFocusY = currentFocus.cy; if (cursorFocus.cx < safeLeft) { - nextFocusX = cursorFocus.cx; + nextFocusX = cursorFocus.cx + halfSpan - safeZoneInset; } else if (cursorFocus.cx > safeRight) { - nextFocusX = cursorFocus.cx; + nextFocusX = cursorFocus.cx - halfSpan + safeZoneInset; } if (cursorFocus.cy < safeTop) { - nextFocusY = cursorFocus.cy; + nextFocusY = cursorFocus.cy + halfSpan - safeZoneInset; } else if (cursorFocus.cy > safeBottom) { - nextFocusY = cursorFocus.cy; + nextFocusY = cursorFocus.cy - halfSpan + safeZoneInset; } return clampFocusToScale( @@ -157,18 +157,21 @@ export function computeCursorFollowFocus( : clampedRegionFocus; } + const timeWentBackwards = state.initialized && timeMs + 0.5 < state.lastTimeMs; + if (timeWentBackwards) { + state.reachedFullZoom = false; + } + // Track when zoom reaches full strength if (zoomStrength >= 0.99) { state.reachedFullZoom = true; } // Zooming out: was fully zoomed but strength is now dropping — freeze camera - if (state.reachedFullZoom && zoomStrength < 0.99) { + if (!timeWentBackwards && state.reachedFullZoom && zoomStrength < 0.99) { return { cx: state.frozenFocusX, cy: state.frozenFocusY }; } - const timeWentBackwards = state.initialized && timeMs + 0.5 < state.lastTimeMs; - if (!state.initialized || !state.wasZoomed || timeWentBackwards) { const initialFocus = recenterFocusWhenCursorLeavesSafeZone( clampedRegionFocus, diff --git a/src/components/video-editor/videoPlayback/zoomTransform.ts b/src/components/video-editor/videoPlayback/zoomTransform.ts index ec5b2c76..b85b698b 100644 --- a/src/components/video-editor/videoPlayback/zoomTransform.ts +++ b/src/components/video-editor/videoPlayback/zoomTransform.ts @@ -174,12 +174,13 @@ function computeZoomBlurGeometry({ Math.hypot(focusStagePxX, stageSize.height - focusStagePxY), Math.hypot(stageSize.width - focusStagePxX, stageSize.height - focusStagePxY), ); + const radius = Math.max(outerRadius, 1); return { centerX: focusStagePxX, centerY: focusStagePxY, - innerRadius: Math.max(18, Math.min(outerRadius - 1, visibleHalfDiagonal)), - radius: Math.max(outerRadius, 1), + innerRadius: Math.max(0, Math.min(Math.max(18, Math.min(radius - 1, visibleHalfDiagonal)), radius - 1)), + radius, }; } diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index fd3dfae3..7e2d41f7 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -1103,6 +1103,8 @@ export function useScreenRecorder(): UseScreenRecorderReturn { } } + hideEditorOverlayCursorByDefault.current = true; + const wantsAudioCapture = microphoneEnabled || systemAudioEnabled; const browserCaptureSource = await resolveBrowserCaptureSource(selectedSource); @@ -1258,20 +1260,25 @@ export function useScreenRecorder(): UseScreenRecorderReturn { stream.current.addTrack(micAudioTrack); } } else { - const mediaStream = await mediaDevices.getDisplayMedia({ - audio: false, - video: { - displaySurface: selectedSource.id?.startsWith("window:") - ? "window" - : "monitor", - width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH }, - height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT }, - frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, - cursor: "never", - }, - selfBrowserSurface: "exclude", - surfaceSwitching: "exclude", - }); + const mediaStream = useLinuxPortal + ? await mediaDevices.getDisplayMedia({ + audio: false, + video: { + displaySurface: selectedSource.id?.startsWith("window:") + ? "window" + : "monitor", + width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH }, + height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT }, + frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, + cursor: "never", + }, + selfBrowserSurface: "exclude", + surfaceSwitching: "exclude", + }) + : await mediaDevices.getUserMedia({ + audio: false, + video: browserScreenVideoConstraints, + }); stream.current = mediaStream; videoTrack = mediaStream.getVideoTracks()[0]; diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 23f36a02..66afce25 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -234,6 +234,7 @@ interface CaptionRenderState { interface RenderSnapshot { timeMs: number; cursorTimeMs: number; + backgroundTimelineTimeMs: number; sceneTransform: { scale: number; x: number; y: number }; zoom: { scale: number; focusX: number; focusY: number; progress: number }; } @@ -2550,6 +2551,7 @@ export class FrameRenderer { return { timeMs, cursorTimeMs, + backgroundTimelineTimeMs: backgroundTimelineTimestamp / 1000, sceneTransform: { scale: this.animationState.appliedScale, x: this.animationState.x, @@ -2635,6 +2637,17 @@ export class FrameRenderer { return null; } + if (resolvedSnapshot !== lastSnapshot) { + await this.renderSceneSample( + Math.round(resolvedSnapshot.timeMs * 1000), + Math.round(resolvedSnapshot.cursorTimeMs * 1000), + Math.round(resolvedSnapshot.backgroundTimelineTimeMs * 1000), + layoutCache, + true, + false, + ); + } + const hasOverlayCanvasWork = (this.config.annotationRegions?.length ?? 0) > 0 || Boolean(this.captionCanvas && this.captionSprite?.visible);