Skip to content

Commit 19e2596

Browse files
committed
fix(engine): use getBoundingClientRect for video frame replacement geometry
Fixes #1009 — portrait (1080×1920) video compositions show a bottom gap for the first ~37 seconds when rendered with pooled browsers on macOS. Root cause: injectVideoFramesBatch() positioned the replacement <img> using video.offsetWidth/offsetHeight/offsetLeft/offsetTop. On macOS Chrome with pooled browsers (enabled in v0.6.11), these offset* properties can report stale compositor-dependent values before Chrome's video decoder fully settles, causing the <img> to be sized smaller than the CSS box. Fix: use getBoundingClientRect() which always reflects the CSS layout dimensions regardless of compositor state. Position is computed relative to offsetParent's rect for correct absolute positioning. Also adds portrait-video-fullbleed regression test (1080×1920 video with position:absolute;inset:0) to catch future bottom-edge coverage issues.
1 parent ee4e088 commit 19e2596

6 files changed

Lines changed: 88 additions & 10 deletions

File tree

packages/engine/src/services/screenshotService.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -436,22 +436,28 @@ export async function injectVideoFramesBatch(
436436
// stack vertically — the <img> lands below the video and gets clipped
437437
// by any overflow:hidden ancestor (e.g., border-radius wrappers).
438438
//
439-
// Apply this after visual style copying so the measured used box is
440-
// the final authority for replacement frame geometry.
439+
// Use getBoundingClientRect for replacement geometry — it reflects the
440+
// CSS layout box reliably across platforms. offset* properties can
441+
// report stale or compositor-dependent values on macOS Chrome when the
442+
// browser pool shares one process across workers, causing the <img> to
443+
// be sized smaller than the CSS box (bottom-gap regression #1009).
441444
{
442445
const videoRect = video.getBoundingClientRect();
443-
const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
444-
const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
445-
const offsetWidth = video.offsetWidth > 0 ? video.offsetWidth : videoRect.width;
446-
const offsetHeight = video.offsetHeight > 0 ? video.offsetHeight : videoRect.height;
446+
const parentRect = video.offsetParent
447+
? video.offsetParent.getBoundingClientRect()
448+
: { left: 0, top: 0 };
449+
const relLeft = Math.round(videoRect.left - parentRect.left);
450+
const relTop = Math.round(videoRect.top - parentRect.top);
451+
const w = Math.round(videoRect.width);
452+
const h = Math.round(videoRect.height);
447453
img.style.position = "absolute";
448454
img.style.inset = "auto";
449-
img.style.left = `${offsetLeft}px`;
450-
img.style.top = `${offsetTop}px`;
455+
img.style.left = `${relLeft}px`;
456+
img.style.top = `${relTop}px`;
451457
img.style.right = "auto";
452458
img.style.bottom = "auto";
453-
img.style.width = `${offsetWidth}px`;
454-
img.style.height = `${offsetHeight}px`;
459+
img.style.width = `${w}px`;
460+
img.style.height = `${h}px`;
455461
}
456462
img.style.objectFit = computedStyle.objectFit;
457463
img.style.objectPosition = computedStyle.objectPosition;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "Portrait video full-bleed coverage",
3+
"description": "Regression for #1009: a 1080×1920 portrait video with position:absolute;inset:0 must cover the full canvas bottom edge. The video frame replacement <img> must use getBoundingClientRect geometry, not offset* properties.",
4+
"tags": ["portrait", "video", "regression", "fullbleed"],
5+
"minPsnr": 25,
6+
"maxFrameFailures": 5,
7+
"minAudioCorrelation": 0.0,
8+
"maxAudioLagWindows": 120,
9+
"renderConfig": {
10+
"fps": 30
11+
}
12+
}

packages/producer/tests/portrait-video-fullbleed/output/compiled.html

Lines changed: 34 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:436b95c2d4cd370b2e782eca8ec09341cdc1af4a1780184d3871d1bf0ffbd460
3+
size 320046
Binary file not shown.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=1080, height=1920" />
6+
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
7+
<style>
8+
*, *::before, *::after { box-sizing: border-box; }
9+
html, body { margin: 0; width: 1080px; height: 1920px; overflow: hidden; background: #000; }
10+
#main { position: relative; width: 1080px; height: 1920px; overflow: hidden; background: #000; }
11+
#bg-video { position: absolute; inset: 0; width: 1080px; height: 1920px; z-index: 0; }
12+
</style>
13+
</head>
14+
<body>
15+
<div id="main" data-composition-id="main" data-start="0" data-duration="10" data-width="1080" data-height="1920">
16+
<video id="bg-video" class="clip" data-start="0" data-duration="10" data-track-index="0" muted playsinline src="assets/video/sample.mp4"></video>
17+
</div>
18+
<script>
19+
window.__timelines = window.__timelines || {};
20+
window.__timelines["main"] = gsap.timeline({ paused: true });
21+
</script>
22+
</body>
23+
</html>

0 commit comments

Comments
 (0)