fix(engine): copy border-width/style/color onto the replacement video frame - #3993
miga-heygen wants to merge 4 commits into
Conversation
… 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
left a comment
There was a problem hiding this comment.
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)
Summary
injectVideoFramesBatchsubstitutes 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, andborder-colorwere 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-radiusandclip-pathwere already on the allow-list. A real-Chromium test confirms they already clip a replaced element's content correctly without needingoverflow: hidden— the visible symptom traced entirely to the missing border properties, not a separate radius/clip-path bug.injectVideoFramesBatchmeasured 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 incontent-boxlayouts the border pushed the image outward. The video's used box is now measured before the image is created, and the image is forced tobox-sizing: border-boxsince 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.frameCapture.tsthat 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
injectVideoFramesBatchfunction against a styled<video class="clip">(and adata-starttimed 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.border-style: none/border-width: 0pxis a no-op, verified with an additional pixel check.coreRuntimeBrowser.test.tssibling).getBoundingClientRect()equals the video's; it fails with the old measure-after-insert order (width 484 instead of 500) and passes with the fix.style-9-prodgolden: 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; thecompiled.htmltiming attributes are unchanged (the remaining diff there is compiler output drift accumulated since the golden was last regenerated).parityContract,screenshotService, andvideoFrameInjectorsuites pass unchanged.