Skip to content

feat: add dual frame webcam layout preset#347

Open
shreyas-makes wants to merge 6 commits intosiddharthvaddem:mainfrom
shreyas-makes:feature/dual-frame-preset
Open

feat: add dual frame webcam layout preset#347
shreyas-makes wants to merge 6 commits intosiddharthvaddem:mainfrom
shreyas-makes:feature/dual-frame-preset

Conversation

@shreyas-makes
Copy link
Copy Markdown

@shreyas-makes shreyas-makes commented Apr 5, 2026

In Openscreen, we only have one preset called the "Picture in picture" mode. I was looking at a way in which there could be other presets too. A preset I was thinking of adding was a way in which the camera view could also be scaled and zoomed out to give equal representation for both the Camera and the Screenshare. This is the PR to enable this.

Summary

  • add a new Dual Frame webcam layout preset with a 2:1 screen-to-camera split for landscape compositions
  • preserve aspect ratios by rendering the screen share to fill the left pane and center-cropping the webcam to fit the right pane in preview and export
  • update editor preset selection, persistence, localized labels, and layout tests for the new preset

Testing

  • npx vitest --run src/lib/compositeLayout.test.ts src/components/video-editor/projectPersistence.test.ts
  • npx biome check src/lib/compositeLayout.ts src/components/video-editor/videoPlayback/layoutUtils.ts src/lib/exporter/frameRenderer.ts src/lib/compositeLayout.test.ts

Summary by CodeRabbit

  • New Features

    • Added "Dual Frame" webcam layout preset and UI labels (EN/ES/ZH).
  • Bug Fixes

    • Smarter webcam-layout switching when canvas aspect ratio changes (presets adapt for portrait vs. landscape).
    • Preserve or clear webcam position correctly when changing presets.
    • Respect layout-provided container border radius.
    • Improved webcam cropping to better match source aspect ratio.
  • Tests

    • Added unit and persistence tests covering layout, normalization, and position behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4941f4e0-dd33-445c-9502-27e068476e12

📥 Commits

Reviewing files that changed from the base of the PR and between a30526e and e2147c4.

📒 Files selected for processing (1)
  • src/components/video-editor/projectPersistence.ts

📝 Walkthrough

Walkthrough

Adds a new "dual-frame" (split) webcam layout preset and wires support through layout computation, rendering (center-crop and screenBorderRadius), settings/editor behavior, project normalization/tests, and i18n translations.

Changes

Cohort / File(s) Summary
Layout Computation
src/lib/compositeLayout.ts, src/lib/compositeLayout.test.ts
Introduce dual-frame (split) preset and SplitTransform; compute split layout, expose screenBorderRadius, refactor centering helper, and add unit test validating dual-frame geometry.
Settings & Editor Wiring
src/components/video-editor/SettingsPanel.tsx, src/components/video-editor/VideoEditor.tsx
Filter available presets by portrait/landscape via isPortraitCanvas; vertical-stack only for portrait, dual-frame for landscape. Preserve/clear webcamPosition only for picture-in-picture; adjust aspect-ratio change logic to switch preset to picture-in-picture only when incompatible.
Project Persistence & Tests
src/components/video-editor/projectPersistence.ts, src/components/video-editor/projectPersistence.test.ts
Normalize aspectRatio early, compute normalized webcamLayoutPreset (including dual-frame rules), clamp/retain webcamPosition only for picture-in-picture (else null/default), and update tests to reflect these behaviors.
Frame Rendering & Layout Utils
src/lib/exporter/frameRenderer.ts, src/components/video-editor/videoPlayback/layoutUtils.ts
Respect compositeLayout.screenBorderRadius in mask/border-radius calculations; perform center-crop source selection for webcam drawing (use 9-arg drawImage).
Localization
src/i18n/locales/en/settings.json, src/i18n/locales/es/settings.json, src/i18n/locales/zh-CN/settings.json
Add layout.dualFrame translations: "Dual Frame" (en), "Marco dual" (es), "双画框" (zh-CN).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem

Poem

🐰 split-screen delight, kinda cursed but neat,
two frames hugging, cropped and sweet.
rounded edges, centered cheer,
the webcam’s doubled — lowkey premier. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main feature: adding a new dual frame webcam layout preset. It's concise, specific, and clearly summarizes the primary change.
Description check ✅ Passed The description covers the motivation, a clear summary of changes, and testing commands. However, it's missing several template sections: explicit Type of Change, Related Issue(s), Screenshots/Video demonstration, and a checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdc71ca49b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +498 to +500
compositeLayout.screenBorderRadius != null
? compositeLayout.screenBorderRadius * canvasScaleFactor
: compositeLayout.screenCover
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid double-scaling dual-frame screen border radius

In updateLayout, compositeLayout.screenBorderRadius is already computed by computeCompositeLayout in the current export canvas coordinate space, so multiplying it by canvasScaleFactor scales it a second time when export dimensions differ from preview dimensions. In dual-frame exports this makes the screen corner radius much larger than the webcam corner radius (which is not similarly scaled), so the two panes no longer match visually.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/video-editor/SettingsPanel.tsx (1)

660-670: Make the preset mapping explicit.

These branches currently treat every non-"picture-in-picture" / non-"vertical-stack" preset as dual-frame. That works with today’s three presets, but a future addition would be silently mislabeled and filtered into the landscape bucket. A small allow-list plus label map would fail closed here.

♻️ Suggested cleanup
+	const visiblePresets = new Set<WebcamLayoutPreset>(
+		isPortraitCanvas
+			? ["picture-in-picture", "vertical-stack"]
+			: ["picture-in-picture", "two-timer"],
+	);
+	const presetLabels: Record<WebcamLayoutPreset, string> = {
+		"picture-in-picture": t("layout.pictureInPicture"),
+		"vertical-stack": t("layout.verticalStack"),
+		"two-timer": t("layout.twoTimer"),
+	};
+
-											{WEBCAM_LAYOUT_PRESETS.filter((preset) => {
-												if (preset.value === "picture-in-picture") return true;
-												if (preset.value === "vertical-stack") return isPortraitCanvas;
-												return !isPortraitCanvas;
-											}).map((preset) => (
+											{WEBCAM_LAYOUT_PRESETS.filter((preset) =>
+												visiblePresets.has(preset.value),
+											).map((preset) => (
 												<SelectItem key={preset.value} value={preset.value} className="text-xs">
-													{preset.value === "picture-in-picture"
-														? t("layout.pictureInPicture")
-														: preset.value === "vertical-stack"
-															? t("layout.verticalStack")
-															: t("layout.twoTimer")}
+													{presetLabels[preset.value]}
 												</SelectItem>
 											))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 660 - 670,
Current filter/map over WEBCAM_LAYOUT_PRESETS treats any unknown preset as the
“dual-frame” (landscape) case; change the code to explicitly allow-list known
preset values and use a label map or switch for translations so unknown presets
are excluded and not silently mis-labeled. Update the filter to only include
preset.value values in an explicit set (e.g., "picture-in-picture",
"vertical-stack", "two-timer") and replace the nested ternary in the SelectItem
rendering with a small lookup (or switch) keyed by preset.value that returns the
correct t(...) key for each known preset; ensure any preset.value not in the
allow-list is skipped and not rendered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/projectPersistence.ts`:
- Around line 354-359: Persisted webcamLayoutPreset is accepted unconditionally
and can rehydrate an invalid layout for portrait projects; first derive the
normalized aspectRatio from the saved editor (use the same
AspectRatio/ASPECT_RATIOS/isPortraitAspectRatio utilities) and then coerce
editor.webcamLayoutPreset: if the aspect ratio is portrait and
editor.webcamLayoutPreset === "two-timer" (or any preset unsupported for
portrait), replace it with DEFAULT_WEBCAM_LAYOUT_PRESET; update the
normalization logic around webcamLayoutPreset (referencing webcamLayoutPreset,
editor.webcamLayoutPreset, DEFAULT_WEBCAM_LAYOUT_PRESET) and add a negative
regression test next to the existing "two-timer" test to assert portrait files
get coerced to the default preset.

---

Nitpick comments:
In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 660-670: Current filter/map over WEBCAM_LAYOUT_PRESETS treats any
unknown preset as the “dual-frame” (landscape) case; change the code to
explicitly allow-list known preset values and use a label map or switch for
translations so unknown presets are excluded and not silently mis-labeled.
Update the filter to only include preset.value values in an explicit set (e.g.,
"picture-in-picture", "vertical-stack", "two-timer") and replace the nested
ternary in the SelectItem rendering with a small lookup (or switch) keyed by
preset.value that returns the correct t(...) key for each known preset; ensure
any preset.value not in the allow-list is skipped and not rendered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60e5416c-9c48-4077-a727-ee70c712ee09

📥 Commits

Reviewing files that changed from the base of the PR and between 5340272 and cdc71ca.

📒 Files selected for processing (11)
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/projectPersistence.test.ts
  • src/components/video-editor/projectPersistence.ts
  • src/components/video-editor/videoPlayback/layoutUtils.ts
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/lib/compositeLayout.test.ts
  • src/lib/compositeLayout.ts
  • src/lib/exporter/frameRenderer.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/video-editor/SettingsPanel.tsx (1)

660-671: Consider an explicit mapping for layout preset labels to improve maintainability.

The nested ternary on lines 666-670 maps labels by process of elimination — if it's not "picture-in-picture" and not "vertical-stack", it must be "dual-frame". While this works correctly for the current three presets, adding a fourth preset would require updating both the type definition and this ternary; the ternary change could easily be overlooked, silently producing incorrect labels.

A more explicit approach would prevent this:

♻️ Suggested refactor
const PRESET_LABEL_KEYS: Record<WebcamLayoutPreset, string> = {
  "picture-in-picture": "layout.pictureInPicture",
  "vertical-stack": "layout.verticalStack",
  "dual-frame": "layout.dualFrame",
};

Then replace the nested ternary with:

{t(PRESET_LABEL_KEYS[preset.value])}

This makes the mapping explicit and ensures TypeScript will require adding an entry for any new preset added to the type union.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/SettingsPanel.tsx` around lines 660 - 671, The
nested ternary that chooses translation keys for WEBCAM_LAYOUT_PRESETS inside
the SelectItem maps by exclusion and is fragile if new presets are added;
replace that logic with an explicit mapping constant (e.g., PRESET_LABEL_KEYS:
Record<WebcamLayoutPreset, string>) that maps each preset.value to its
translation key, then use t(PRESET_LABEL_KEYS[preset.value]) in the SelectItem
render (affecting the mapping where SelectItem and preset.value are used) so
TypeScript will force updates when new presets are introduced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 1684-1688: normalizeProjectEditor currently preserves
webcamPosition even when webcamLayoutPreset is not "picture-in-picture", causing
inconsistency with runtime behavior; update normalizeProjectEditor to set
webcamPosition to null whenever the normalized webcamLayoutPreset !==
"picture-in-picture". Locate the normalizeProjectEditor function and after you
determine/normalize webcamLayoutPreset, add logic to validate webcamPosition and
assign null unless webcamLayoutPreset === "picture-in-picture" (preserving the
existing position only for PiP). Ensure you reference the same property names
(webcamLayoutPreset, webcamPosition) and return the normalized editor object
with webcamPosition adjusted accordingly.

---

Nitpick comments:
In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 660-671: The nested ternary that chooses translation keys for
WEBCAM_LAYOUT_PRESETS inside the SelectItem maps by exclusion and is fragile if
new presets are added; replace that logic with an explicit mapping constant
(e.g., PRESET_LABEL_KEYS: Record<WebcamLayoutPreset, string>) that maps each
preset.value to its translation key, then use t(PRESET_LABEL_KEYS[preset.value])
in the SelectItem render (affecting the mapping where SelectItem and
preset.value are used) so TypeScript will force updates when new presets are
introduced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a92e207d-e51d-444f-ba3a-999c8e42c149

📥 Commits

Reviewing files that changed from the base of the PR and between cdc71ca and 978b6e9.

📒 Files selected for processing (10)
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/projectPersistence.test.ts
  • src/components/video-editor/projectPersistence.ts
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/zh-CN/settings.json
  • src/lib/compositeLayout.test.ts
  • src/lib/compositeLayout.ts
  • src/lib/exporter/frameRenderer.ts
✅ Files skipped from review due to trivial changes (3)
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/en/settings.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/components/video-editor/projectPersistence.ts
  • src/lib/compositeLayout.test.ts
  • src/lib/exporter/frameRenderer.ts
  • src/components/video-editor/projectPersistence.test.ts
  • src/lib/compositeLayout.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/components/video-editor/projectPersistence.ts (1)

181-192: Consider refactoring the nested ternary for readability.

The logic is correct, but the 5-level nested ternary is difficult to follow. A switch statement or helper function would improve maintainability.

♻️ Suggested refactor using switch
-	const normalizedWebcamLayoutPreset: WebcamLayoutPreset =
-		editor.webcamLayoutPreset === "picture-in-picture"
-			? editor.webcamLayoutPreset
-			: editor.webcamLayoutPreset === "vertical-stack"
-				? isPortraitAspectRatio(normalizedAspectRatio)
-					? editor.webcamLayoutPreset
-					: DEFAULT_WEBCAM_LAYOUT_PRESET
-				: editor.webcamLayoutPreset === "dual-frame"
-					? isPortraitAspectRatio(normalizedAspectRatio)
-						? DEFAULT_WEBCAM_LAYOUT_PRESET
-						: editor.webcamLayoutPreset
-					: DEFAULT_WEBCAM_LAYOUT_PRESET;
+	const isPortrait = isPortraitAspectRatio(normalizedAspectRatio);
+	let normalizedWebcamLayoutPreset: WebcamLayoutPreset;
+	switch (editor.webcamLayoutPreset) {
+		case "picture-in-picture":
+			normalizedWebcamLayoutPreset = "picture-in-picture";
+			break;
+		case "vertical-stack":
+			normalizedWebcamLayoutPreset = isPortrait ? "vertical-stack" : DEFAULT_WEBCAM_LAYOUT_PRESET;
+			break;
+		case "dual-frame":
+			normalizedWebcamLayoutPreset = isPortrait ? DEFAULT_WEBCAM_LAYOUT_PRESET : "dual-frame";
+			break;
+		default:
+			normalizedWebcamLayoutPreset = DEFAULT_WEBCAM_LAYOUT_PRESET;
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-editor/projectPersistence.ts` around lines 181 - 192,
The nested 5-level ternary computing normalizedWebcamLayoutPreset is hard to
read—replace it with a small helper function (e.g.,
computeNormalizedWebcamLayoutPreset) or a switch over editor.webcamLayoutPreset
that implements the same rules: if "picture-in-picture" return
editor.webcamLayoutPreset; if "vertical-stack" return editor.webcamLayoutPreset
only when isPortraitAspectRatio(normalizedAspectRatio) else
DEFAULT_WEBCAM_LAYOUT_PRESET; if "dual-frame" return
DEFAULT_WEBCAM_LAYOUT_PRESET when isPortraitAspectRatio(normalizedAspectRatio)
else editor.webcamLayoutPreset; default to DEFAULT_WEBCAM_LAYOUT_PRESET; call
this helper to set normalizedWebcamLayoutPreset for clarity and maintainability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/video-editor/projectPersistence.ts`:
- Around line 181-192: The nested 5-level ternary computing
normalizedWebcamLayoutPreset is hard to read—replace it with a small helper
function (e.g., computeNormalizedWebcamLayoutPreset) or a switch over
editor.webcamLayoutPreset that implements the same rules: if
"picture-in-picture" return editor.webcamLayoutPreset; if "vertical-stack"
return editor.webcamLayoutPreset only when
isPortraitAspectRatio(normalizedAspectRatio) else DEFAULT_WEBCAM_LAYOUT_PRESET;
if "dual-frame" return DEFAULT_WEBCAM_LAYOUT_PRESET when
isPortraitAspectRatio(normalizedAspectRatio) else editor.webcamLayoutPreset;
default to DEFAULT_WEBCAM_LAYOUT_PRESET; call this helper to set
normalizedWebcamLayoutPreset for clarity and maintainability.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 300476a2-1428-4223-b60b-267e28bc9c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 978b6e9 and a30526e.

📒 Files selected for processing (2)
  • src/components/video-editor/projectPersistence.test.ts
  • src/components/video-editor/projectPersistence.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/video-editor/projectPersistence.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant