Skip to content

fix(engine): copy border-width/style/color onto the replacement video frame - #3993

Open
miga-heygen wants to merge 4 commits into
mainfrom
fix-video-border-clip-paint
Open

miga-heygen wants to merge 4 commits into
mainfrom
fix-video-border-clip-paint

Conversation

@miga-heygen

@miga-heygen miga-heygen commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • injectVideoFramesBatch substitutes each <video> with a sibling <img> holding an extracted still frame, and copies an allow-listed set of CSS properties from the video's computed style onto that image. border-width, border-style, and border-color were missing from the allow-list, so any border authored directly on a <video> never reached the replacement image and disappeared from render/snapshot output.
  • border-radius and clip-path were already on the allow-list. A real-Chromium test confirms they already clip a replaced element's content correctly without needing overflow: hidden — the visible symptom traced entirely to the missing border properties, not a separate radius/clip-path bug.
  • Copying a border onto the replacement image changed its geometry: injectVideoFramesBatch measured the video after inserting the image as an in-flow sibling, so in flex-centred layouts the freshly bordered sibling shrank the video's box by the border width before it was read, and in content-box layouts the border pushed the image outward. The video's used box is now measured before the image is created, and the image is forced to box-sizing: border-box since that measurement is always a border-box value. With both changes the replacement image's rectangle equals the video's in flex and absolute layouts under either box-sizing.
  • Also corrected a stale comment in frameCapture.ts that claimed a CSS-effect risk detector can return "clip-path" — it structurally cannot; that value only ever comes from a separate, animation-only gate elsewhere in the same file.

Test plan

  • New test launches real headless Chromium, calls the actual injectVideoFramesBatch function against a styled <video class="clip"> (and a data-start timed variant), takes a real screenshot, and reads real pixel values via an in-page canvas to confirm the border renders and the rounded/clipped corner stays clipped.
  • Verified with a genuine red/green cycle: the test fails against the original property list and passes once the three properties are added.
  • Confirmed no regression for videos with no authored border: the copied border-style: none / border-width: 0px is a no-op, verified with an additional pixel check.
  • Registered the new test in the producer test-classification manifest so CI routes it to the integration lane (it needs a real browser, like its coreRuntimeBrowser.test.ts sibling).
  • Added a flex-centred regression test asserting the replacement image's getBoundingClientRect() equals the video's; it fails with the old measure-after-insert order (width 484 instead of 500) and passes with the fix.
  • Regenerated the style-9-prod golden: its 16 px video border was previously invisible, so the committed frames were stale. The regenerated frames keep the video box where it was and show the border ring; the compiled.html timing attributes are unchanged (the remaining diff there is compiler output drift accumulated since the golden was last regenerated).
  • Full parityContract, screenshotService, and videoFrameInjector suites pass unchanged.

miga-heygen and others added 4 commits September 16, 2026 10:03
… frame

injectVideoFramesBatch substitutes each <video> with a sibling <img> holding
an extracted still frame, copying an allow-listed set of CSS properties from
the video's computed style onto the img. border-width/border-style/
border-color were absent from that allow-list, so any border authored
directly on a <video> never reached the replacement image and disappeared
from render/snapshot output. border-radius and clip-path were already on the
list and already clip a replaced element's content correctly without
needing overflow:hidden, confirmed by a real-Chromium test that exercises
the actual capture path and reads real screenshot pixels.

Also corrected a stale comment in frameCapture.ts claiming
detectCssEffectRisk can return "clip-path" as a risk value -- it can't; that
string only ever comes from a separate, animation-only at-risk-props gate
further down the same file.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
style-9-prod's video has an authored 8px border + 16px border-radius
(design_review.md calls out the "floating" look) that never rendered before
this fix, so its committed golden baked in the pre-fix (borderless) output.
Regenerated via `tsx src/regression-harness.ts style-9-prod --update`;
verified the diff is exactly the border/radius becoming visible (extracted
frames before/after) and the fixture passes its own visual/audio checks
again (100/100 checkpoints, correlation 1.000).

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
…rame

injectVideoFramesBatch measured the video's used box (offsetLeft/Top/
Width/Height) after creating, inserting, and styling the replacement
<img> sibling -- once that copy loop started carrying border-width
(added for the border-visibility fix), the freshly-inserted, still
in-flow, now-bordered <img> competed for space in a flex row before
the video's own box was ever read. In a flex-centered layout (video
width:100%, sharing a row with the img sibling), that shrank the
measured video box by the exact width of the border, so the img ended
up sized from the video's post-shrink box instead of its authored one.

Moved the measurement to before the <img> is created/inserted/styled
at all, and set img.style.boxSizing = "border-box" explicitly after
the style-copy loop, since offsetWidth/offsetHeight (and the
getBoundingClientRect fallback) are always a border-box measurement
regardless of what box-sizing value that loop copies from the video's
own computed style.

Added a flex-centered regression test asserting the replacement img's
box stays identical to the video's box; confirmed it fails with the
old measurement order (a 16px border shrinks a 500px box to 484px) and
passes with the fix. Regenerated the one fixture (style-9-prod) whose
committed golden had baked in the shrunk-frame geometry.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
Follow-up to c888903: group the four measurement locals into a
single videoBox object instead of four loose consts, and trim the
surrounding comments down to their load-bearing content. No behavior
change -- same measurement order, same fallback conditions, same
applied style values; re-verified red (reverting the measurement order
still reproduces the 500px -> 484px shrink) and green.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

APPROVE at 213ecf08 — border-carry via shared allow-list; box-parity captured pre-substitute + box-sizing:border-box pin on the img

Head verified: 213ecf08091ba2545d245068a4b5f078d1e14d6a. Commit co-authors: miga-heygen (bot carrier) + miguel-heygen (trust-listed).

Attribute carry is via a shared allow-list, not ad-hoc local copy

MEDIA_VISUAL_STYLE_PROPERTIES in parityContract.ts now lists border-width/border-style/border-color alongside the already-present border-radius/clip-path/overflow/filter/mix-blend-mode/backdrop-filter. Narrow-but-principled: the three added properties are exactly what makes border: shorthand round-trip. Other visual companions (background-color, box-shadow, outline) aren't required by the reported symptom and are natural follow-ups if a future case surfaces them — using the same allow-list means any such addition is one line, not a rework.

Box-parity fix is correct at the invariant level

videoBox is captured from offsetWidth/offsetHeight (always a border-box measurement) before the <img> sibling is created, so the freshly-bordered in-flow img can no longer shrink the video's flex box. The explicit img.style.boxSizing = "border-box" after the copy loop is the right pin — it defends against the copy loop inheriting content-box from the video, which would otherwise push the img's content area past videoBox by border-width.

Test pins both axes at real pixels

videoFrameBorderClip.test.ts launches real headless Chromium, screenshots a border:8px solid red; border-radius:24px; clip-path:inset(0 round 24px) video, and reads border/center/corner pixel channels via an in-page canvas — confirms border paints, content fills, and the corner is transparent. The third case (flex-centered, width:100%, box-sizing:border-box reset) asserts imgBox equals videoBox via getBoundingClientRect() rounded, and the commit narrative documents the red/green (500px → 484px shrink reproduces without the fix, disappears with it).

No unintended surface widening

screenshotService.ts change is scoped to the injectVideoFramesBatch substitution path; frameCapture.ts diff is pure comment correction (documents that detectCssEffectRisk structurally can't return "clip-path"); test-classification.mjs adds the new file to the integration lane so it gets a real browser.

Golden regeneration is defensible

style-9-prod MP4 shows the previously-invisible 16px border ring re-appearing; compiler-output drift in compiled.html is unrelated timing-attributes churn (called out in commit body); harness reports 100/100 checkpoints, correlation 1.000.

CI

All non-skipped checks SUCCESS across CI, CodeQL, all 9 regression shards ×2 runs, Windows render, Player perf, Preview parity, Typecheck, Lint, Build, Test. Skipped jobs are path-scoped (Skills, Codex plugin, CLI npx shim, GCP BeginFrame — unrelated). mergeStateStatus: BLOCKED on REVIEW_REQUIRED only.

— Review by tai (pr-review)

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.

2 participants