From 9567b9cbb2d12511f6309a719a6b3109b359e4a2 Mon Sep 17 00:00:00 2001 From: surim0n Date: Mon, 15 Jun 2026 12:48:39 -0400 Subject: [PATCH] Add dynamic webcam overlay dimensions --- .../video-editor/projectPersistence.ts | 7 ++++- src/lib/exporter/frameRenderer.ts | 5 ++-- src/lib/exporter/modernFrameRenderer.ts | 26 +++++++++++++------ ...rnVideoExporter.nativeStaticLayout.test.ts | 23 ++++++++++++++++ src/lib/exporter/modernVideoExporter.ts | 15 ++++++++--- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index edc36012a..f9191fe3c 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -825,6 +825,11 @@ export function normalizeProjectEditor(editor: Partial): Pro ) ? (webcam as Partial<{ zoomScaleEffect: number }>).zoomScaleEffect : null; + const normalizedWebcamSize = isFiniteNumber(webcam.width) + ? clamp(webcam.width, 10, 100) + : isFiniteNumber(webcam.size) + ? clamp(webcam.size, 10, 100) + : DEFAULT_WEBCAM_SIZE; const normalizedCursorStyle = typeof editor.cursorStyle === "string" && editor.cursorStyle.trim().length > 0 ? editor.cursorStyle === "mono" @@ -1031,7 +1036,7 @@ export function normalizeProjectEditor(editor: Partial): Pro webcam.corner === "bottom-right" ? webcam.corner : DEFAULT_WEBCAM_OVERLAY.corner, - size: isFiniteNumber(webcam.size) ? clamp(webcam.size, 10, 100) : DEFAULT_WEBCAM_SIZE, + size: normalizedWebcamSize, width: isFiniteNumber(webcam.width) ? clamp(webcam.width, 10, 100) : isFiniteNumber(webcam.size) diff --git a/src/lib/exporter/frameRenderer.ts b/src/lib/exporter/frameRenderer.ts index 3fa2c1f37..4e14d5c5a 100644 --- a/src/lib/exporter/frameRenderer.ts +++ b/src/lib/exporter/frameRenderer.ts @@ -19,6 +19,7 @@ import type { import { BASE_PREVIEW_HEIGHT, BASE_PREVIEW_WIDTH, + DEFAULT_WEBCAM_SIZE, ZOOM_DEPTH_SCALES, } from "@/components/video-editor/types"; import { DEFAULT_FOCUS } from "@/components/video-editor/videoPlayback/constants"; @@ -2459,10 +2460,10 @@ export class FrameRenderer { ? webcamFrameSource.videoHeight : webcamFrameSource.height) || sourceWidth; const margin = webcam.margin ?? 24; - const widthPercent = webcam.width ?? webcam.size ?? 50; + const widthPercent = webcam.width ?? webcam.size ?? DEFAULT_WEBCAM_SIZE; const heightPercent = getCropMatchedWebcamHeightPercent( widthPercent, - webcam.height ?? webcam.size ?? 50, + webcam.height ?? webcam.size ?? DEFAULT_WEBCAM_SIZE, sourceWidth, sourceHeight, webcam.cropRegion, diff --git a/src/lib/exporter/modernFrameRenderer.ts b/src/lib/exporter/modernFrameRenderer.ts index 529b2109e..a5e93e498 100644 --- a/src/lib/exporter/modernFrameRenderer.ts +++ b/src/lib/exporter/modernFrameRenderer.ts @@ -26,7 +26,11 @@ import type { ZoomRegion, ZoomTransitionEasing, } from "@/components/video-editor/types"; -import { getDefaultCaptionFontFamily, ZOOM_DEPTH_SCALES } from "@/components/video-editor/types"; +import { + DEFAULT_WEBCAM_SIZE, + getDefaultCaptionFontFamily, + ZOOM_DEPTH_SCALES, +} from "@/components/video-editor/types"; import { DEFAULT_FOCUS } from "@/components/video-editor/videoPlayback/constants"; import { type CursorFollowCameraState, @@ -2916,21 +2920,27 @@ export class FrameRenderer { } const margin = webcam.margin ?? 24; - const widthPercent = webcam.width ?? webcam.size ?? 50; - const aspectSourceWidth = + const widthPercent = webcam.width ?? webcam.size ?? DEFAULT_WEBCAM_SIZE; + const cropMatchSourceWidth = liveSourceDimensions.width > 0 ? liveSourceDimensions.width : renderableWebcamSource.width; - const aspectSourceHeight = + const cropMatchSourceHeight = liveSourceDimensions.height > 0 ? liveSourceDimensions.height : renderableWebcamSource.height; + const cropMatchRegion = + liveSourceDimensions.width > 0 && liveSourceDimensions.height > 0 + ? webcam.cropRegion + : isWebcamCropRegionDefault(webcam.cropRegion) + ? webcam.cropRegion + : null; const heightPercent = getCropMatchedWebcamHeightPercent( widthPercent, - webcam.height ?? webcam.size ?? 50, - aspectSourceWidth, - aspectSourceHeight, - webcam.cropRegion, + webcam.height ?? webcam.size ?? DEFAULT_WEBCAM_SIZE, + cropMatchSourceWidth, + cropMatchSourceHeight, + cropMatchRegion, ); const dimensions = getWebcamOverlayDimensionsPx({ containerWidth: this.config.width, diff --git a/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts b/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts index f0e57d13d..5f993d543 100644 --- a/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts +++ b/src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts @@ -739,6 +739,29 @@ describe("ModernVideoExporter native static-layout eligibility", () => { ).toBe("unsupported-rectangular-webcam-overlay"); }); + it("skips native static layout for non-finite webcam dimensions", () => { + const exporter = createExporter({ + webcam: { + enabled: true, + sourcePath: "C:\\recordly\\webcam.mp4", + size: Number.NaN, + width: Number.NaN, + height: 40, + }, + }); + + expect( + exporter.getNativeStaticLayoutSkipReason( + { + audioMode: "edited-track", + strategy: "offline-render-fallback", + }, + videoInfo, + 60, + ), + ).toBe("unsupported-rectangular-webcam-overlay"); + }); + it("allows native speed timelines with a resolvable webcam source", () => { const speedRegions: SpeedRegion[] = [ { id: "speed-1", startMs: 1_000, endMs: 4_000, speed: 1.5 }, diff --git a/src/lib/exporter/modernVideoExporter.ts b/src/lib/exporter/modernVideoExporter.ts index a1983599d..4f010d6e7 100644 --- a/src/lib/exporter/modernVideoExporter.ts +++ b/src/lib/exporter/modernVideoExporter.ts @@ -17,7 +17,7 @@ import type { ZoomRegion, ZoomTransitionEasing, } from "@/components/video-editor/types"; -import { ZOOM_DEPTH_SCALES } from "@/components/video-editor/types"; +import { DEFAULT_WEBCAM_SIZE, ZOOM_DEPTH_SCALES } from "@/components/video-editor/types"; import { DEFAULT_FOCUS } from "@/components/video-editor/videoPlayback/constants"; import { computeCursorFollowFocus, @@ -1502,8 +1502,17 @@ export class ModernVideoExporter { return false; } - const width = webcam.width ?? webcam.size ?? 40; - const height = webcam.height ?? webcam.size ?? 40; + if ( + (webcam.size != null && !Number.isFinite(webcam.size)) || + (webcam.width != null && !Number.isFinite(webcam.width)) || + (webcam.height != null && !Number.isFinite(webcam.height)) + ) { + return true; + } + + const fallbackSize = Number.isFinite(webcam.size) ? webcam.size : DEFAULT_WEBCAM_SIZE; + const width = Number.isFinite(webcam.width) ? webcam.width : fallbackSize; + const height = Number.isFinite(webcam.height) ? webcam.height : fallbackSize; return Math.abs(width - height) > 0.001 || !isWebcamCropRegionDefault(webcam.cropRegion); }