Skip to content

feat(camera): several cameras in the picture, with layout sections - #1026

Draft
christian-wr wants to merge 22 commits into
getopenscreen:mainfrom
christian-wr:feat/multi-camera-layouts-pr
Draft

christian-wr wants to merge 22 commits into
getopenscreen:mainfrom
christian-wr:feat/multi-camera-layouts-pr

Conversation

@christian-wr

Copy link
Copy Markdown
Contributor

Depends on #1025 (several cameras recorded) and #989 (desk view). Until those are merged, this branch also carries their commits; only the top commit (feat(camera): several cameras in the picture, with layout sections) is new here. I'll rebase once they land. Opened as a draft for that reason.

Summary

Put several recorded cameras in the picture at once — the desk camera full with your face small on top, two cameras side by side, or the screen with two small cameras — and switch between such arrangements along the timeline with a glide.

  • Layout sections on the timeline, in the same lane as Full Camera sections: an "Add layout" menu offers four templates (screen + small cameras; one camera full + small ones; side by side; another camera full). Templates that cannot work are disabled with a hint (too few cameras on the clip, PiP templates under the dual-frame / vertical-stack presets).
  • Inspector: template and the camera for each place ("Camera 2 · Logitech BRIO"), reset windows; a Full Camera section can be turned into any template and back. Small camera windows can be moved and resized in the preview (aspect kept, round windows stay round).
  • Per camera ("Cameras" in the layout pane): rotation, mirror and crop for cameras 2–4, and a perspective correction for every camera — a homography calibrated once with four corner handles (loupe, keyboard nudging, target format A4 / 16:9 / 4:3 / 1:1 / free, margin, live rectified preview). "Detect markers" places the handles on four ArUco markers; "Print marker sheet" prints them on A4.
  • Compositor: a list of camera layers per frame (planned in camera_layers.rs: glide and fade between neighbouring sections, also directly to and from an adjacent Full Camera section), extra camera decoders opened only for cameras a layout shows (preview and export; a camera that ends early never shortens the clip), and a homography + transparency lane appended to LayerCB (HLSL, WGSL and MSL agree; pinned by the size/offset test). Projects without layout sections render exactly as before.
  • Storage: Full Camera regions stay where and how they are (camera 1, incl. the desk view); every other layout lives in cameraLayoutRegions, clip-anchored like Full Camera regions, with no overlap across the two lists (also enforced for the AI agent's Full Camera tools). Per-camera settings in cameraSettings. All additive — no schema bump.
  • Dependency: js-aruco2 2.0.0 (MIT, pure JS) for marker detection; it embeds OpenCV's 4×4 dictionary codes (BSD-3). Both are listed in THIRD-PARTY-NOTICES.md, since the code ships inside the renderer bundle.

Related issue

None — new feature.

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

Can follow on request (desk camera full + face small, side by side, the calibration dialog).

Testing

  • npm run test (≈4430 passed; LeftPanel.copyMessage.test.tsx occasionally times out under full-suite load and passes alone — untouched here), both tsc configs, npm run lint (0 errors), npm run i18n:check.
  • cargo test -p openscreen-compositor --lib on Windows ARM64 (the 8 pipeline_windows::tests that need hardware video decode fail on this host with or without this change); Linux and macOS compositor tests incl. new wgpu pixel tests passed in CI on a fork.
  • Headless export with synthetic cameras, measured from the pixels: template rects within 1.2 px, the glide between adjacent sections never passes through the default PiP, side-by-side halves exact, a skewed checkerboard rectified to square fields (≈120 × 120 px, ≤1.2 px off the grid), camera 1 with a perspective also square; export time 1 vs 3 cameras 2.46 s vs 2.82 s.
  • In the real app (Playwright _electron, on a copy of a real Brio + built-in camera recording): add a "camera full + small" section, choose and swap cameras, convert a Full Camera section and back, undo/redo, drag a camera window, calibrate a perspective, save and reopen — 27/27 checks, live preview inside the sections, camera 1 large fills the frame edge to edge (measured). Script: scripts/editor-multicam-smoke.mjs.
  • Marker detection tested on rendered marker images (tilted and rotated sheets).

Not covered yet (in the checklist): the printed marker sheet and a real desk camera under real light, and an export of a real-camera project with layout sections. Translations other than English and German would benefit from a native speaker's look.

Each camera of the webcams list (or the legacy single-camera fields) gets its own capture and encoder on the recording's T0. A camera that cannot be opened is dropped with an indexed webcam-unavailable warning; one whose samples fail mid-take is disabled on its own. recording-stopped keeps webcamPath and adds webcamPaths.

MFEncoder now only balances an MFStartup it made: a dropped camera's never-initialized encoder ran an unmatched MFShutdown from its destructor and stopped every other encoder in the process.
A camera whose encoder initialize() or capture start() fails is now warned about with the indexed webcam-unavailable event and dropped, instead of ending the take; its encoder is finalized and its empty file removed. The screen encoder's failure stays fatal.

mf_encoder_color_test pins the MFEncoder fix: finalizing a never-initialized encoder must leave a live one able to write and finalize.
Two webcams of the same model report the same name, and the browser id never matches a device path, so every such camera selected the first device and the second open failed as busy. The take now owns a claim set: each camera that opens adds its device (MF symbolic link or DirectShow DevicePath, normalized so both paths agree), and later cameras pick the best unclaimed match. The selection rule lives in device_selection.{h,cpp} with its own unit test.
A label already used by camera 1 or an earlier extra gets " (2)", " (3)" by occurrence, so unavailable and dropped cameras of the same model can be told apart.
The recorder caps at three extra cameras, but a validator that ships with a
cap cannot be loosened later for older builds. The HUD and Electron still cap.
Two webcams of the same model share a name. When an extra carries a deviceId
and camera 1 does not, the name says nothing about whether they are the same
device, so the extra is no longer dropped as a duplicate of camera 1.
The helper deletes the file of a camera it drops at start, but ignored a
failed DeleteFileW. It now logs a WARNING with the path and GetLastError, and
Electron keeps the dropped cameras' paths so stop and discard remove a 0-byte
stub left behind.
A camera the helper disables mid-take keeps its partial file in the take, but
nobody was told. A camera whose file was kept (size > 0) yet is missing from
recording-stopped.webcamPaths is now named in a "Stopped early" notice after
the take, camera 1 included. Paths compare case-insensitively with either
separator. Only an event that carries webcamPaths can say so, so the helper now
prints the list, possibly empty, whenever a camera wrote a file of its own; an
older helper or a missing event never produces the notice.
The checklist now notes that with several identical cameras plugged in the
recorded ones follow Windows' enumeration order, adds a 1-vs-2-camera screen
pacing comparison at 4K (getopenscreen#945) and a stopped-early check. The helper README
says the camera index counts after entries without camPath are skipped, and
the extras-need-camera-1 assumption (R6) is noted where links drop them.
A failed ReadSample was counted and retried forever, so an unplugged camera
kept its file running to the end on its last picture and stayed in
webcamPaths. The Media Foundation capture now latches lost on a device
invalidated or hardware start failure, on end of stream during the take, or
after a second of consecutive read failures; the DirectShow fallback latches
it on EC_DEVICE_LOST (removal), EC_ERRORABORT or EC_STREAM_ERROR_STOPPED. The
writer loop disables a lost camera like any mid-take failure, so its file ends
at the loss and the app names it as stopped early.
A Full Camera section can now show a webcam tilted down onto the desk: "Desk view"
in the inspector turns the camera 180°, switches the mirror off for that section
(otherwise text on the page reads back to front) and shows the whole camera frame
instead of the face crop. The timeline marks such sections with a rotate icon.

The moment the camera is tilted is covered at both ends of the section: the whole
camera picture is blurred and dimmed for the Full Camera grow (and the shrink),
fading in and out around it, with a translated "Desk mode" label over it that can be
switched off per section. Preview and export render it identically through the
compositor's frame plan.

- Rust: per-frame orientation (u/v bound swaps, crop bypass) and cover strength,
  a `LayerCB.cover` lane (HLSL, WGSL, MSL), the blur clamped to the camera's valid
  area so aligned decoder padding never bleeds in; the label is drawn at the
  cover strength of the same frame, so it fades exactly with the cover.
- App: `rotation` / `mirror` / `deskLabel` on Full Camera regions (defaults are not
  stored), store and persistence, inspector controls, one generated label annotation
  per projected piece, translations in every locale.
- Tests for each layer; WGSL is validated with naga on every host.
Up to four recorded cameras can now be on screen together. Layout sections on the
timeline choose a template — screen + small cameras, one camera full + small ones,
two cameras side by side, or another camera full — and which camera goes into each
place; the picture glides between sections. Each camera can be turned, mirrored,
cropped and perspective-corrected (a homography calibrated once with four corner
handles, optionally placed by detected ArUco markers from a printable sheet).

- Compositor: a list of camera layers per frame instead of one webcam rect,
  planned in `camera_layers.rs` (glide and fade between neighbouring sections,
  incl. a direct glide to and from an adjacent Full Camera section), extra camera
  decoders opened only for cameras a layout shows (preview and export, a camera
  that ends early never shortens the clip), a homography and a transparency lane in
  `LayerCB` (HLSL, WGSL, MSL), the corrected camera cover-fitted into its box.
- Editor: Full Camera regions stay as they are (camera 1, desk view); every other
  layout lives in `cameraLayoutRegions`, clip-anchored like Full Camera regions,
  in the same timeline lane with no overlap across the two lists. "Add layout"
  menu, a layout inspector (template, camera per place, reset windows), PiP places
  movable and resizable in the preview, a "Cameras" section with per-camera
  settings, and a calibration dialog (perspective and crop) with a loupe, target
  format, margin and a live rectified preview. Marker detection uses js-aruco2
  (MIT; the OpenCV 4x4 dictionary codes are BSD-3, both listed in
  THIRD-PARTY-NOTICES.md).
- Every change is one undo step; projects without layout sections render exactly
  as before.
@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

This branch has not been deployed

No deployments
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