Skip to content

fix(compositor): never stretch the webcam, whatever box the layout gives it - #147

Merged
EtienneLescot merged 1 commit into
feat/ai-editionfrom
claude/side-by-side-webcam-aspect
Jul 23, 2026
Merged

fix(compositor): never stretch the webcam, whatever box the layout gives it#147
EtienneLescot merged 1 commit into
feat/ai-editionfrom
claude/side-by-side-webcam-aspect

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Le symptôme

Le preset side-by-side affiche la caméra visiblement écrasée.

La cause

La branche dual-frame de computeCompositeLayout donne à la caméra un slot de colonnewebcamRect = webcamSlot, une fraction fixe de la largeur sur toute la hauteur du contenu, sans le moindre ajustement d'aspect (compositeLayout.ts:439). Le compositeur y plaquait la frame caméra entière. Une caméra 16:9 dans une boîte de ratio ~0.55 perd la moitié de sa largeur.

Le choix de la coupe source se faisait sur la forme de la webcam, sur une hypothèse que son propre commentaire énonçait : « le dst matche le ratio de la source ». Vrai pour le placement par défaut (fit_cam_aspect façonne la boîte depuis l'aspect caméra) et pour square/circle (center-crop écrit à la main). Faux dès que l'app fournit le rect — side-by-side, ou une webcam déplacée par l'utilisateur. L'invariant vivait chez les appelants, donc n'importe quel preset pouvait le violer en silence. C'est arrivé.

Le correctif — au niveau de la primitive, pas du cas

cover_crop_uv dérive le sous-rect source du ratio réel de la boîte : le plus grand rect centré ayant cette forme, soit exactement object-fit: cover — ce que la preview web affiche déjà. La couche ne peut plus être déformée, quel que soit le producteur du rect. Le center-crop carré de square/circle en devient le cas 1:1 et cesse d'être traité à part.

À ne pas confondre avec l'anisotropie retirée par la refonte du render target : celle-là venait du ratio de sortie (canvas → output), celle-ci du fit source → boîte. Axe différent, d'où sa survie. Elle est antérieure à la refonte — l'ancienne paire inverse_undistort + apply_undistort autour de ce chemin composait à l'identité.

Avant / après

Slot en colonne, même frame source, ratio de boîte ~0.55 :

avant après
toute la frame 16:9 comprimée dans la colonne proportions naturelles, rogné et centré

Pourquoi ça ne peut pas casser les cas déjà bons

Propriété de sûreté : quand la boîte a déjà le ratio de la caméra, la coupe est la frame entière. Les placements corrects (PiP par défaut, vertical-stack, square/circle) sont donc inchangés à l'octet près.

  • 6/6 hashes du golden identiques à feat/ai-edition
  • 3 tests unitaires épinglent l'invariant : ratio de coupe == ratio de boîte sur un balayage de caméras × boîtes ; coupe toujours dans la frame visible et centrée (jamais dans le padding décodeur) ; frame entière quand ça matche déjà
  • 1 golden qui rend le cas du slot en colonne pour inspection visuelle

cargo test : 17 unitaires + 2 golden, verts.

Non couvert

Pas encore validé dans l'app — vérifié par rendu golden hors-app (même chemin que l'export : Compositor + Player). Le rendu réel du side-by-side dans l'éditeur reste à confirmer visuellement.

…ves it

The side-by-side preset renders the camera visibly squashed. `computeCompositeLayout`'s
dual-frame branch hands the webcam a column slot — `webcamRect = webcamSlot`, a fixed
fraction of the width at full content height, with no aspect adjustment at all — and the
compositor mapped the whole camera frame onto it. A 16:9 camera in a ~0.55 ratio box is
squeezed to about half its width.

The source crop was chosen from the webcam SHAPE, on the assumption stated in its own
comment: "le dst matche le ratio de la source". That holds for the default placement,
where `fit_cam_aspect` shapes the box from the camera's aspect, and for square/circle,
which got a hand-written centre crop. It does not hold the moment the app supplies the
rect — side-by-side, or a webcam dragged anywhere by the user. The invariant lived in the
callers, so any preset could break it silently, and did.

Fixed at the primitive instead: `cover_crop_uv` derives the source sub-rect from the
box's ACTUAL aspect — the largest centred crop with that shape, i.e. `object-fit: cover`,
which is also what the web preview already shows. The layer cannot be distorted no matter
who produced the rect. The square/circle centre crop falls out as the 1:1 case and stops
being special.

This is not the anisotropy that the render-target refactor removed: that one came from
the output ratio (canvas -> output), this one from source -> box fit. Different axis,
which is why it survived. It predates the refactor — the old `inverse_undistort` +
`apply_undistort` pair around this path composed to the identity.

Safety property, and the reason this cannot regress the good cases: when the box already
matches the camera aspect, the crop is the whole frame. All six golden formats hash
bit-for-bit identical. Three unit tests pin the invariant (crop aspect always equals box
aspect, crop stays inside the visible frame and centred, whole-frame when already
matching), plus a golden that renders the column-slot case for eyeballing.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af32b79c-29e2-487b-b96b-1f20986c64ea

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/side-by-side-webcam-aspect

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.

@EtienneLescot
EtienneLescot merged commit ec7caa3 into feat/ai-edition Jul 23, 2026
2 checks passed
@EtienneLescot
EtienneLescot deleted the claude/side-by-side-webcam-aspect branch July 23, 2026 21:27
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