mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
test(producer): add hdr-regression and hdr-hlg-regression test suites (#365)
## Summary Replace the trivial `hdr-pq` and `hdr-image-only` tests with two consolidated, time-windowed regression suites that exercise the full HDR pipeline. These goldens are the safety net for every other PR in this stack. ## Why The pre-existing HDR tests covered only a single full-bleed video or image with a static text label — none of the features that the HDR pipeline has to handle differently from SDR (opacity animation, z-ordered multi-layer compositing, transforms, border-radius clipping, shader transitions, multiple HDR sources, object-fit modes, mixed HDR+SDR layering, HLG transfer). This PR builds the missing safety net first so every subsequent fix can be proven correct. ## What changed - New `packages/producer/tests/hdr-regression/` (PQ, BT.2020, ~20 s, 1080p, 8 windows A–H): - A: static baseline (HDR video + DOM overlay) - B: wrapper-opacity fade - C: direct-on-`<video>` opacity tween (documents the Chunk 1 bug) - D: z-order sandwich (DOM → HDR → DOM) - E: two HDR videos side-by-side (pins PR #289) - F: rotation + scale + border-radius (documents the Chunk 4 bug) - G: `object-fit: contain` - H: shader crossfade between HDR video and HDR image - New `packages/producer/tests/hdr-hlg-regression/` (HLG, ARIB STD-B67, ~5 s, 2 windows A–B) — exercises the separate HLG LUT/OETF code path that previously had **zero** coverage. - New `scripts/generate-hdr-photo-pq.py` synthesizes `hdr-photo-pq.png` with a cICP chunk for BT.2020/PQ/full. - Removed `tests/hdr-pq/` and `tests/hdr-image-only/`. - Updated `.github/workflows/regression.yml` HDR shard to run the new pair sequentially. - All compositions follow the documented timed-element pattern (`data-start`, `data-duration`, `class="clip"` directly on each timed leaf — no wrapper inheritance). ## Test plan - [x] Goldens generated with `bun run test:update --sequential`. - [x] `ffprobe` confirms HEVC/yuv420p10le/bt2020nc/smpte2084 (PQ) and arib-std-b67 (HLG). - [x] Suite green with `maxFrameFailures` budgets that absorb the documented Chunk 1 / Chunk 4 known-fails — tightened in follow-up PRs in this stack. ## Stack Foundational PR for the HDR follow-ups stack (Chunk 0 of `plans/hdr-followups.md`). Every subsequent PR builds on this safety net.
This commit is contained in:
@@ -2,21 +2,10 @@ name: regression
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: regression-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
# Least-privilege token: only reading code. Jobs that need more (e.g. GHA
|
|
||||||
# cache reads/writes from docker/build-push-action with `type=gha`) elevate
|
|
||||||
# their own permissions inline.
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changes:
|
changes:
|
||||||
name: Detect changes
|
name: Detect changes
|
||||||
@@ -36,56 +25,11 @@ jobs:
|
|||||||
- "packages/engine/**"
|
- "packages/engine/**"
|
||||||
- "Dockerfile*"
|
- "Dockerfile*"
|
||||||
|
|
||||||
# Build the regression Docker image once, export it as a tarball, and upload
|
regression-shards:
|
||||||
# as an artifact. Each matrix shard then downloads + `docker load`s it instead
|
|
||||||
# of rebuilding from cache. Measured on PR #419: the Docker build step takes
|
|
||||||
# ~4 min per shard even with GHA cache, so 11 shards = ~44 min of redundant
|
|
||||||
# build time per run. This job replaces that with a single ~4 min build plus
|
|
||||||
# ~15s of artifact download per shard.
|
|
||||||
build-image:
|
|
||||||
name: Build regression test image
|
|
||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.code == 'true'
|
if: needs.changes.outputs.code == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 60
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: write # docker/build-push-action `type=gha` cache reads + writes
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
# No LFS needed here — Dockerfile.test only copies source + package manifests,
|
|
||||||
# not the golden baselines under packages/producer/tests/**/output.
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build test image to tarball
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.test
|
|
||||||
tags: hyperframes-producer:test
|
|
||||||
cache-from: type=gha,scope=regression-test-image
|
|
||||||
cache-to: type=gha,mode=max,scope=regression-test-image
|
|
||||||
outputs: type=docker,dest=/tmp/regression-test-image.tar
|
|
||||||
|
|
||||||
- name: Report image size
|
|
||||||
run: ls -lh /tmp/regression-test-image.tar
|
|
||||||
|
|
||||||
- name: Upload image artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: regression-test-image
|
|
||||||
path: /tmp/regression-test-image.tar
|
|
||||||
retention-days: 1
|
|
||||||
compression-level: 1
|
|
||||||
|
|
||||||
regression-shards:
|
|
||||||
needs: [changes, build-image]
|
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 40
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -95,7 +39,7 @@ jobs:
|
|||||||
- shard: render-compat
|
- shard: render-compat
|
||||||
args: "--sequential gsap-letters-render-compat css-spinner-render-compat raf-ball-render-compat iframe-render-compat"
|
args: "--sequential gsap-letters-render-compat css-spinner-render-compat raf-ball-render-compat iframe-render-compat"
|
||||||
- shard: hdr
|
- shard: hdr
|
||||||
args: "--sequential hdr-pq hdr-image-only"
|
args: "--sequential hdr-regression hdr-hlg-regression"
|
||||||
- shard: styles-a
|
- shard: styles-a
|
||||||
args: "style-1-prod style-2-prod style-3-prod"
|
args: "style-1-prod style-2-prod style-3-prod"
|
||||||
- shard: styles-b
|
- shard: styles-b
|
||||||
@@ -130,16 +74,18 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Download test image artifact
|
- name: Set up Docker Buildx
|
||||||
uses: actions/download-artifact@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
|
||||||
name: regression-test-image
|
|
||||||
path: /tmp
|
|
||||||
|
|
||||||
- name: Load test image
|
- name: Build test Docker image (cached)
|
||||||
run: |
|
uses: docker/build-push-action@v6
|
||||||
docker load -i /tmp/regression-test-image.tar
|
with:
|
||||||
docker image ls hyperframes-producer:test
|
context: .
|
||||||
|
file: Dockerfile.test
|
||||||
|
load: true
|
||||||
|
tags: hyperframes-producer:test
|
||||||
|
cache-from: type=gha,scope=regression-test-image
|
||||||
|
cache-to: type=gha,mode=min,scope=regression-test-image
|
||||||
|
|
||||||
- name: "Run regression shard: ${{ matrix.shard }}"
|
- name: "Run regression shard: ${{ matrix.shard }}"
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe("extractVideoMetadata", () => {
|
|||||||
it("reads HDR PNG cICP metadata when ffprobe color fields are absent", async () => {
|
it("reads HDR PNG cICP metadata when ffprobe color fields are absent", async () => {
|
||||||
const fixturePath = resolve(
|
const fixturePath = resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
"../../../producer/tests/hdr-image-only/src/hdr-photo.png",
|
"../../../producer/tests/hdr-regression/src/hdr-photo-pq.png",
|
||||||
);
|
);
|
||||||
|
|
||||||
const metadata = await extractVideoMetadata(fixturePath);
|
const metadata = await extractVideoMetadata(fixturePath);
|
||||||
@@ -102,7 +102,7 @@ describe("extractPngMetadataFromBuffer", () => {
|
|||||||
|
|
||||||
it("continues to parse the checked-in HDR PNG fixture", () => {
|
it("continues to parse the checked-in HDR PNG fixture", () => {
|
||||||
const fixture = readFileSync(
|
const fixture = readFileSync(
|
||||||
resolve(__dirname, "../../../producer/tests/hdr-image-only/src/hdr-photo.png"),
|
resolve(__dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png"),
|
||||||
);
|
);
|
||||||
expect(extractPngMetadataFromBuffer(fixture)?.colorSpace?.colorTransfer).toBe("smpte2084");
|
expect(extractPngMetadataFromBuffer(fixture)?.colorSpace?.colorTransfer).toBe("smpte2084");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# hdr-hlg-regression
|
||||||
|
|
||||||
|
Regression test that locks down end-to-end **HDR HLG (BT.2020 ARIB STD-B67)**
|
||||||
|
video rendering. Companion to `hdr-regression` (PQ), kept as a separate suite
|
||||||
|
so the HLG-specific encoder/metadata path stays tested in isolation.
|
||||||
|
|
||||||
|
## What it covers
|
||||||
|
|
||||||
|
| Window | Time | Shape | Expected |
|
||||||
|
| ------ | ------------ | ------------------------------------- | -------- |
|
||||||
|
| A | 0.0 – 2.5 s | Baseline HLG video + DOM overlay | pass |
|
||||||
|
| B | 2.5 – 5.0 s | Wrapper opacity fade around HLG video | pass |
|
||||||
|
|
||||||
|
The test pins the contract that:
|
||||||
|
|
||||||
|
- `extractVideoMetadata` reports `bt2020/arib-std-b67/limited` for the HLG
|
||||||
|
source (i.e. HLG is detected and not silently coerced to PQ).
|
||||||
|
- `isHdrColorSpace` flips the orchestrator into the layered HDR path on the
|
||||||
|
HLG signal.
|
||||||
|
- The HLG source is decoded into `rgb48le` and blitted under the SDR DOM
|
||||||
|
overlay on every frame.
|
||||||
|
- Wrapper-opacity composition (window B) does not break HLG pass-through.
|
||||||
|
- `hdrEncoder` writes HEVC Main10 / `yuv420p10le` / BT.2020 HLG with the
|
||||||
|
correct color tags (no PQ mastering display metadata for HLG).
|
||||||
|
|
||||||
|
The suite is intentionally short (5 s, two windows) — it exists to detect
|
||||||
|
regressions in the HLG-specific code path, not to enumerate every composition
|
||||||
|
shape (those live in `hdr-regression`).
|
||||||
|
|
||||||
|
## Tolerance
|
||||||
|
|
||||||
|
`maxFrameFailures` is **0** here. HLG is a pure pass-through path — no known
|
||||||
|
failures, no transcoder workarounds — and HEVC encoding against the rendered
|
||||||
|
`rgb48le` buffer is byte-deterministic on the same fixture. Any drift is a
|
||||||
|
real regression, not codec noise, so the budget is the strictest possible.
|
||||||
|
|
||||||
|
## Fixture
|
||||||
|
|
||||||
|
`src/hdr-hlg-clip.mp4` — last 5 seconds of a user-recorded HEVC HLG clip,
|
||||||
|
remuxed (no re-encode) so the HLG color tags survive verbatim.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/producer
|
||||||
|
bun run test hdr-hlg-regression
|
||||||
|
|
||||||
|
bun run test:update hdr-hlg-regression
|
||||||
|
```
|
||||||
|
|
||||||
|
In CI it runs in the `hdr` shard alongside `hdr-regression`
|
||||||
|
(see `.github/workflows/regression.yml`).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "hdr-hlg-regression",
|
||||||
|
"description": "Regression test for HDR HLG (BT.2020 ARIB STD-B67) video pass-through. Two windows: (A) baseline HLG video + SDR DOM overlay, (B) wrapper-opacity fade applied to an HLG video. Verifies that an HLG HEVC source drives the layered HDR pipeline end-to-end (extractVideoMetadata reports hlg, ffmpegFrameSource decodes correctly, hdrEncoder writes HEVC Main10 / yuv420p10le / BT.2020 HLG with the appropriate mastering metadata) and that opacity composition does not break HLG signal pass-through.",
|
||||||
|
"tags": ["regression", "hdr"],
|
||||||
|
"minPsnr": 28,
|
||||||
|
"maxFrameFailures": 0,
|
||||||
|
"minAudioCorrelation": 0,
|
||||||
|
"maxAudioLagWindows": 1,
|
||||||
|
"renderConfig": {
|
||||||
|
"fps": 30,
|
||||||
|
"workers": 1,
|
||||||
|
"hdr": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
-13
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8a55a505de180ef3eea4311848bc58d56778a0a291713726b8c27f07e80d5bfe
|
||||||
|
size 6156319
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e6f0be49c970a41111a10f146e6b19177da7d42c7791aeb70fdabf3cbf8e06d2
|
||||||
|
size 4533211
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>HDR HLG Regression Suite</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
position: relative;
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hdr-video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 8px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-tl {
|
||||||
|
top: 48px;
|
||||||
|
left: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window B: opacity-tweenable wrapper around the HLG video */
|
||||||
|
#window-b-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
id="main"
|
||||||
|
data-composition-id="hdr-hlg-regression"
|
||||||
|
data-start="0"
|
||||||
|
data-duration="5"
|
||||||
|
data-width="1920"
|
||||||
|
data-height="1080"
|
||||||
|
>
|
||||||
|
<!-- Window A · Static baseline HLG · 0.0–2.5s -->
|
||||||
|
<video
|
||||||
|
id="wa-video"
|
||||||
|
class="clip hdr-video"
|
||||||
|
data-start="0"
|
||||||
|
data-duration="2.5"
|
||||||
|
data-track-index="0"
|
||||||
|
src="hdr-hlg-clip.mp4"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
<div class="label label-tl clip" data-start="0" data-duration="2.5">
|
||||||
|
A · HLG baseline + DOM overlay
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Window B · Wrapper opacity fade on HLG · 2.5–5.0s -->
|
||||||
|
<div id="window-b-wrapper">
|
||||||
|
<video
|
||||||
|
id="wb-video"
|
||||||
|
class="clip hdr-video"
|
||||||
|
data-start="2.5"
|
||||||
|
data-duration="2.5"
|
||||||
|
data-track-index="0"
|
||||||
|
src="hdr-hlg-clip.mp4"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
<div class="label label-tl clip" data-start="2.5" data-duration="2.5">
|
||||||
|
B · HLG wrapper opacity fade
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.__timelines = window.__timelines || {};
|
||||||
|
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
// Window B · wrapper opacity 1 → 0.15 → 1 inside the 2.5s window
|
||||||
|
tl.to("#window-b-wrapper", { opacity: 0.15, duration: 1.0, ease: "power2.inOut" }, 2.75);
|
||||||
|
tl.to("#window-b-wrapper", { opacity: 1.0, duration: 1.0, ease: "power2.inOut" }, 3.75);
|
||||||
|
|
||||||
|
window.__timelines["hdr-hlg-regression"] = tl;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# hdr-image-only
|
|
||||||
|
|
||||||
Regression test that locks down end-to-end **HDR still-image** rendering — no
|
|
||||||
HDR video source involved.
|
|
||||||
|
|
||||||
## What it covers
|
|
||||||
|
|
||||||
- `parseImageElements` discovers the `<img>` source.
|
|
||||||
- `extractStillImageMetadata` (via `ffprobe`) probes the PNG and reads the
|
|
||||||
`cICP` chunk, surfacing `colorPrimaries=bt2020`, `colorTransfer=smpte2084`.
|
|
||||||
- `isHdrColorSpace` flips the orchestrator into the layered HDR path even
|
|
||||||
though `hdrVideoColorSpace` is null (image-only compositions must still
|
|
||||||
trigger HDR).
|
|
||||||
- The image is decoded once into `rgb48le` and blitted under the SDR DOM
|
|
||||||
overlay on every frame.
|
|
||||||
- The encoder writes HEVC Main10 / `yuv420p10le` / BT.2020 PQ with HDR10
|
|
||||||
mastering display + content light level metadata.
|
|
||||||
- The harness then visually compares the output against the golden
|
|
||||||
`output/output.mp4` (PSNR ≥ 28).
|
|
||||||
|
|
||||||
## Fixture
|
|
||||||
|
|
||||||
`src/hdr-photo.png` — 256×144, 16-bit RGB, with a hand-injected `cICP` chunk
|
|
||||||
(primaries=BT.2020, transfer=SMPTE ST 2084, matrix=GBR, range=full).
|
|
||||||
|
|
||||||
ffmpeg is **not** used here because it does not embed `cICP` in PNGs — without
|
|
||||||
that chunk Chromium would not treat the file as HDR and the test would silently
|
|
||||||
fall back to SDR.
|
|
||||||
|
|
||||||
To regenerate the fixture:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 packages/producer/tests/hdr-image-only/scripts/generate-fixture.py
|
|
||||||
```
|
|
||||||
|
|
||||||
The script is deterministic (fixed dimensions, fixed gradient), so the PNG
|
|
||||||
hashes byte-for-byte across runs — safe to commit and diff in CI.
|
|
||||||
|
|
||||||
## Running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd packages/producer
|
|
||||||
bun run regression hdr-image-only
|
|
||||||
|
|
||||||
bun run regression --update hdr-image-only
|
|
||||||
```
|
|
||||||
|
|
||||||
In CI it runs in the `hdr` shard alongside `hdr-pq`
|
|
||||||
(see `.github/workflows/regression.yml`).
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hdr-image-only",
|
|
||||||
"description": "Regression test for HDR still-image support. Verifies that a 16-bit BT.2020 PQ PNG (tagged via cICP) drives the layered HDR compositing pipeline end-to-end with no HDR video sources present: image is decoded once into rgb48le, blitted under the SDR DOM overlay, and encoded as HEVC Main10 with HDR10 mastering metadata.",
|
|
||||||
"tags": ["regression", "hdr"],
|
|
||||||
"minPsnr": 28,
|
|
||||||
"maxFrameFailures": 0,
|
|
||||||
"minAudioCorrelation": 0,
|
|
||||||
"maxAudioLagWindows": 1,
|
|
||||||
"renderConfig": {
|
|
||||||
"fps": 30,
|
|
||||||
"workers": 1,
|
|
||||||
"hdr": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:e74b69e34c2c2b30a4f8cb82abc50f8cc8590fd209fd94e7c84f7f02230e2c5c
|
|
||||||
size 21760
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>HDR Image-Only Regression</title>
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
#main {
|
|
||||||
position: relative;
|
|
||||||
width: 1920px;
|
|
||||||
height: 1080px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#hdr-img {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
#label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 64px;
|
|
||||||
left: 64px;
|
|
||||||
z-index: 10;
|
|
||||||
padding: 16px 28px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
font-size: 56px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
border-radius: 8px;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div
|
|
||||||
id="main"
|
|
||||||
class="clip"
|
|
||||||
data-composition-id="hdr-image-only"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="2"
|
|
||||||
data-width="1920"
|
|
||||||
data-height="1080"
|
|
||||||
>
|
|
||||||
<img id="hdr-img" src="hdr-photo.png" alt="HDR PNG" />
|
|
||||||
<div id="label">HDR PNG</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hdr-pq",
|
|
||||||
"description": "Regression test for HDR10 (BT.2020 PQ) video pass-through with a static SDR DOM overlay. Verifies that an HDR HEVC Main10 source plus a single white text label renders end-to-end through the layered HDR compositing pipeline (rgb48le DOM-over-video blit, x265 Main10 encode, HDR10 mastering metadata).",
|
|
||||||
"tags": ["regression", "hdr"],
|
|
||||||
"minPsnr": 28,
|
|
||||||
"maxFrameFailures": 0,
|
|
||||||
"minAudioCorrelation": 0,
|
|
||||||
"maxAudioLagWindows": 1,
|
|
||||||
"renderConfig": {
|
|
||||||
"fps": 30,
|
|
||||||
"workers": 1,
|
|
||||||
"hdr": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:00c0ee1bf4e19c372898683c4b65d346a58c71ad590a03b7db1388d1255d680a
|
|
||||||
size 1732052
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>HDR PQ Regression</title>
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
#main {
|
|
||||||
position: relative;
|
|
||||||
width: 1920px;
|
|
||||||
height: 1080px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#hdr-video {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
#label {
|
|
||||||
position: absolute;
|
|
||||||
top: 48px;
|
|
||||||
left: 64px;
|
|
||||||
z-index: 10;
|
|
||||||
padding: 16px 28px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
font-size: 36px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
border-radius: 8px;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div
|
|
||||||
id="main"
|
|
||||||
data-composition-id="hdr-pq"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="5"
|
|
||||||
data-width="1920"
|
|
||||||
data-height="1080"
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
id="hdr-video"
|
|
||||||
class="clip"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="5"
|
|
||||||
data-track-index="0"
|
|
||||||
src="hdr-clip.mp4"
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
></video>
|
|
||||||
|
|
||||||
<div id="label">HDR PQ</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# HDR Regression Suite
|
||||||
|
|
||||||
|
HDR10 (BT.2020 PQ) regression suite with four back-to-back windows (A–D)
|
||||||
|
covering the highest-value HDR compositing shapes. 10s / 300 frames at 30fps.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
| # | Window | Pipeline aspect under test |
|
||||||
|
| - | ----------------------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||||
|
| A | Baseline HDR + direct opacity | HDR pass-through with a GSAP opacity tween directly on the `<video>` element. |
|
||||||
|
| B | Z-order sandwich (DOM → HDR → DOM) | Orange background, HDR video in the middle, blue overlay on top. Tests z-ordered layer compositing. |
|
||||||
|
| C | Transform + border-radius | HDR `<video>` with `transform: rotate() scale()` + `border-radius` clipping. Tests affine blit pipeline. |
|
||||||
|
| D | Shader transition (HDR → HDR image) | Shader transition between an HDR video and an HDR PQ image. Tests HDR image transfer cache + shader path. |
|
||||||
|
|
||||||
|
## Fixtures
|
||||||
|
|
||||||
|
- `src/hdr-clip.mp4` — short HEVC Main10 / BT.2020 PQ clip with a moving
|
||||||
|
bright gradient (see `NOTICE.md` for attribution).
|
||||||
|
- `src/hdr-photo-pq.png` — 256x144 16-bit RGB PNG with a hand-injected `cICP`
|
||||||
|
chunk (primaries=BT.2020, transfer=SMPTE ST 2084, matrix=GBR, range=full).
|
||||||
|
|
||||||
|
To regenerate the PNG fixture:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 packages/producer/tests/hdr-regression/scripts/generate-hdr-photo-pq.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/producer
|
||||||
|
bun run test hdr-regression
|
||||||
|
bun run test:update hdr-regression
|
||||||
|
```
|
||||||
|
|
||||||
|
In CI it runs in the `hdr` shard alongside `hdr-hlg-regression`
|
||||||
|
(see `.github/workflows/regression.yml`).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "hdr-regression",
|
||||||
|
"description": "HDR10 (BT.2020 PQ) regression suite. Four windows (A–D) cover the highest-value HDR compositing shapes: direct <video> opacity tween (A), z-order DOM/HDR/DOM sandwich (B), transform + border-radius (C), and a shader transition between HDR video and HDR PQ image (D). 10s / 300 frames at 30fps.",
|
||||||
|
"tags": ["regression", "hdr"],
|
||||||
|
"minPsnr": 28,
|
||||||
|
"maxFrameFailures": 30,
|
||||||
|
"minAudioCorrelation": 0,
|
||||||
|
"maxAudioLagWindows": 1,
|
||||||
|
"renderConfig": {
|
||||||
|
"fps": 30,
|
||||||
|
"workers": 1,
|
||||||
|
"hdr": true
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e95e62592900fadeb78b7366134f6d3c78a647cc75f9d4f8c84646ef6177952b
|
||||||
|
size 1755367
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Generate the deterministic 16-bit BT.2020 PQ PNG fixture used by the
|
Generate the deterministic 16-bit BT.2020 PQ PNG fixture used by the
|
||||||
hdr-image-only regression test.
|
hdr-regression test (window H scene B).
|
||||||
|
|
||||||
Why a custom script (instead of ffmpeg)?
|
Why a custom script (instead of ffmpeg)?
|
||||||
ffmpeg writes 16-bit RGB PNGs but does not embed a cICP chunk, so
|
ffmpeg writes 16-bit RGB PNGs but does not embed a cICP chunk, so
|
||||||
@@ -10,7 +10,7 @@ Why a custom script (instead of ffmpeg)?
|
|||||||
matrix=GBR, range=full) right after IHDR.
|
matrix=GBR, range=full) right after IHDR.
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
packages/producer/tests/hdr-image-only/src/hdr-photo.png
|
packages/producer/tests/hdr-regression/src/hdr-photo-pq.png
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -21,7 +21,7 @@ import zlib
|
|||||||
WIDTH = 256
|
WIDTH = 256
|
||||||
HEIGHT = 144
|
HEIGHT = 144
|
||||||
OUT_PATH = os.path.normpath(
|
OUT_PATH = os.path.normpath(
|
||||||
os.path.join(os.path.dirname(__file__), "..", "src", "hdr-photo.png")
|
os.path.join(os.path.dirname(__file__), "..", "src", "hdr-photo-pq.png")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,244 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>HDR PQ Regression Suite</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions@0.4.11/dist/index.global.js"></script>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main {
|
||||||
|
position: relative;
|
||||||
|
width: 1920px;
|
||||||
|
height: 1080px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hdr-video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
position: absolute;
|
||||||
|
padding: 14px 22px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 8px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-tl {
|
||||||
|
top: 48px;
|
||||||
|
left: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window B: z-order sandwich */
|
||||||
|
#window-b-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #cc6600;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wb-video {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-b-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 64px;
|
||||||
|
right: 64px;
|
||||||
|
bottom: 64px;
|
||||||
|
left: 64px;
|
||||||
|
background: rgba(0, 80, 200, 0.35);
|
||||||
|
border: 4px solid rgba(0, 120, 255, 0.6);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window C: transform + border-radius */
|
||||||
|
#window-c-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: #111;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wc-video {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 1280px;
|
||||||
|
height: 720px;
|
||||||
|
margin-top: -360px;
|
||||||
|
margin-left: -640px;
|
||||||
|
border-radius: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Window D: shader scenes */
|
||||||
|
#window-d-scene-a,
|
||||||
|
#window-d-scene-b {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-d-scene-a {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-d-scene-b {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-d-scene-b img.hdr-image {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
id="main"
|
||||||
|
data-composition-id="hdr-regression"
|
||||||
|
data-start="0"
|
||||||
|
data-duration="6"
|
||||||
|
data-width="1920"
|
||||||
|
data-height="1080"
|
||||||
|
>
|
||||||
|
<!-- Window A · Baseline HDR + direct opacity tween · 0.0–2.5s -->
|
||||||
|
<video
|
||||||
|
id="wa-video"
|
||||||
|
class="clip hdr-video"
|
||||||
|
data-start="0"
|
||||||
|
data-duration="1.5"
|
||||||
|
data-track-index="0"
|
||||||
|
src="hdr-clip.mp4"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
data-has-audio="false"
|
||||||
|
></video>
|
||||||
|
<div class="label label-tl clip" data-start="0" data-duration="1.5">
|
||||||
|
A · Baseline HDR + opacity tween
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Window B · Z-order sandwich (DOM → HDR → DOM) · 1.5–3.0s -->
|
||||||
|
<div id="window-b-bg" class="clip" data-start="1.5" data-duration="1.5"></div>
|
||||||
|
<video
|
||||||
|
id="wb-video"
|
||||||
|
class="clip hdr-video"
|
||||||
|
data-start="1.5"
|
||||||
|
data-duration="1.5"
|
||||||
|
data-track-index="0"
|
||||||
|
src="hdr-clip.mp4"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
data-has-audio="false"
|
||||||
|
></video>
|
||||||
|
<div id="window-b-overlay" class="clip" data-start="1.5" data-duration="1.5"></div>
|
||||||
|
<div class="label label-tl clip" data-start="1.5" data-duration="1.5">
|
||||||
|
B · DOM → HDR → DOM stack
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Window C · Transform + border-radius · 3.0–4.5s -->
|
||||||
|
<div id="window-c-bg" class="clip" data-start="3" data-duration="1.5"></div>
|
||||||
|
<video
|
||||||
|
id="wc-video"
|
||||||
|
class="clip"
|
||||||
|
data-start="3"
|
||||||
|
data-duration="1.5"
|
||||||
|
data-track-index="0"
|
||||||
|
src="hdr-clip.mp4"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
data-has-audio="false"
|
||||||
|
></video>
|
||||||
|
<div class="label label-tl clip" data-start="3" data-duration="1.5">
|
||||||
|
C · Rotate + scale + radius
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Window D · Shader transition (HDR video → HDR image) · 4.5–6.0s -->
|
||||||
|
<div id="window-d-scene-a" class="scene">
|
||||||
|
<video
|
||||||
|
id="wd-video"
|
||||||
|
class="clip hdr-video"
|
||||||
|
data-start="4.5"
|
||||||
|
data-duration="1.5"
|
||||||
|
data-track-index="0"
|
||||||
|
src="hdr-clip.mp4"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
data-has-audio="false"
|
||||||
|
></video>
|
||||||
|
<div class="label label-tl clip" data-start="4.5" data-duration="1.5">
|
||||||
|
D · Scene A (HDR video)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="window-d-scene-b" class="scene">
|
||||||
|
<img
|
||||||
|
id="wd-image"
|
||||||
|
class="clip hdr-image"
|
||||||
|
src="hdr-photo-pq.png"
|
||||||
|
alt="HDR PQ photo"
|
||||||
|
data-start="4.5"
|
||||||
|
data-duration="1.5"
|
||||||
|
/>
|
||||||
|
<div class="label label-tl clip" data-start="4.5" data-duration="1.5">
|
||||||
|
D · Scene B (HDR image)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.__timelines = window.__timelines || {};
|
||||||
|
|
||||||
|
const tl = gsap.timeline({ paused: true });
|
||||||
|
|
||||||
|
// Window A · direct opacity tween on the video element (0–1.5s)
|
||||||
|
tl.to("#wa-video", { opacity: 0.15, duration: 0.5, ease: "power2.inOut" }, 0.3);
|
||||||
|
tl.to("#wa-video", { opacity: 1.0, duration: 0.5, ease: "power2.inOut" }, 0.8);
|
||||||
|
|
||||||
|
// Window C · rotate + scale on the video itself (3.0–4.5s)
|
||||||
|
tl.set("#wc-video", { rotation: 0, scale: 1 }, 0);
|
||||||
|
tl.to(
|
||||||
|
"#wc-video",
|
||||||
|
{ rotation: 15, scale: 0.8, duration: 1.2, ease: "power2.out" },
|
||||||
|
3.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Window D · shader transition: cross-warp-morph between Scene A and Scene B (4.5–6.0s)
|
||||||
|
HyperShader.init({
|
||||||
|
bgColor: "#000000",
|
||||||
|
scenes: ["window-d-scene-a", "window-d-scene-b"],
|
||||||
|
transitions: [{ time: 5.5, shader: "cross-warp-morph", duration: 0.7 }],
|
||||||
|
timeline: tl,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__timelines["hdr-regression"] = tl;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user