fix(producer): capture page-side shader composites via screenshot (#1656)

This commit is contained in:
Miguel Ángel
2026-06-22 18:34:01 -04:00
committed by GitHub
parent 60d3eeb1f7
commit 8c13a72696
10 changed files with 403 additions and 5 deletions
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join, win32 } from "node:path";
import { tmpdir } from "node:os";
import type { EngineConfig, ExtractedFrames } from "@hyperframes/engine";
@@ -13,6 +13,8 @@ import {
findMissingFrameRanges,
getNextRetryWorkerCount,
isRecoverableParallelCaptureError,
resolveCaptureForceScreenshotForPageSideCompositing,
shouldDiscardProbeSessionForPageSideCompositing,
shouldUseStreamingEncode,
} from "./renderOrchestrator.js";
import { resolveCompositeTransfer, shouldUseLayeredComposite } from "./hdrCompositor.js";
@@ -333,7 +335,10 @@ describe("writeCompiledArtifacts — external assets on Windows drive-letter pat
});
it("rejects a maliciously crafted key that tries to escape compileDir", () => {
const workDir = makeWorkDir();
const sandboxRoot = mkdtempSync(join(tmpdir(), "hf-orch-root-"));
tempDirs.push(sandboxRoot);
const workDir = join(sandboxRoot, "work", "inner");
mkdirSync(workDir, { recursive: true });
const sourceDir = mkdtempSync(join(tmpdir(), "hf-src-"));
tempDirs.push(sourceDir);
const srcFile = join(sourceDir, "evil.wav");
@@ -359,7 +364,7 @@ describe("writeCompiledArtifacts — external assets on Windows drive-letter pat
writeCompiledArtifacts(compiled, workDir, false);
const escapeTarget = join(workDir, "..", "..", "etc", "passwd");
const escapeTarget = join(workDir, "etc", "passwd");
expect(existsSync(escapeTarget)).toBe(false);
});
});
@@ -694,6 +699,58 @@ describe("resolveRenderWorkerCount", () => {
});
});
describe("resolveCaptureForceScreenshotForPageSideCompositing", () => {
it("forces screenshot capture when page-side shader compositing is active", () => {
expect(
resolveCaptureForceScreenshotForPageSideCompositing({
forceScreenshot: false,
usePageSideCompositing: true,
}),
).toBe(true);
});
it("preserves the existing capture mode when page-side compositing is inactive", () => {
expect(
resolveCaptureForceScreenshotForPageSideCompositing({
forceScreenshot: false,
usePageSideCompositing: false,
}),
).toBe(false);
expect(
resolveCaptureForceScreenshotForPageSideCompositing({
forceScreenshot: true,
usePageSideCompositing: false,
}),
).toBe(true);
});
});
describe("shouldDiscardProbeSessionForPageSideCompositing", () => {
it("discards a previously-loaded probe page when page-side compositing is selected", () => {
expect(
shouldDiscardProbeSessionForPageSideCompositing({
hasProbeSession: true,
usePageSideCompositing: true,
}),
).toBe(true);
});
it("reuses the probe session when no page-side pre-head script is required", () => {
expect(
shouldDiscardProbeSessionForPageSideCompositing({
hasProbeSession: true,
usePageSideCompositing: false,
}),
).toBe(false);
expect(
shouldDiscardProbeSessionForPageSideCompositing({
hasProbeSession: false,
usePageSideCompositing: true,
}),
).toBe(false);
});
});
describe("estimateCaptureCostMultiplier", () => {
it("weights shader transitions and render mode hints without charging static media cost", () => {
const cost = estimateCaptureCostMultiplier({
@@ -767,6 +767,20 @@ export function shouldUseStreamingEncode(
return workerCount === 1;
}
export function resolveCaptureForceScreenshotForPageSideCompositing(args: {
forceScreenshot: boolean;
usePageSideCompositing: boolean;
}): boolean {
return args.usePageSideCompositing ? true : args.forceScreenshot;
}
export function shouldDiscardProbeSessionForPageSideCompositing(args: {
hasProbeSession: boolean;
usePageSideCompositing: boolean;
}): boolean {
return args.hasProbeSession && args.usePageSideCompositing;
}
/**
* Main render pipeline
*/
@@ -1381,9 +1395,28 @@ export async function executeRenderJob(
!needsAlpha;
if (usePageSideCompositingForTransitions) {
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
if (
shouldDiscardProbeSessionForPageSideCompositing({
hasProbeSession: probeSession !== null,
usePageSideCompositing: true,
}) &&
probeSession
) {
lastBrowserConsole = probeSession.browserConsoleBuffer;
await closeCaptureSession(probeSession);
probeSession = null;
log.info(
"[Render] Recreating capture session so page-side compositing pre-head script is loaded.",
);
}
captureForceScreenshot = resolveCaptureForceScreenshotForPageSideCompositing({
forceScreenshot: captureForceScreenshot,
usePageSideCompositing: true,
});
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
log.info(
"[Render] Page-side compositing enabled — bypassing Node-side layered " +
"shader-blend path. Engine will capture one opaque RGB frame per output frame.",
"shader-blend path. Engine will capture one opaque RGB screenshot per output frame.",
);
}
const useLayeredComposite =
@@ -0,0 +1,13 @@
{
"name": "page-side-shader-compositor-render-compat",
"description": "Regression guard for page-side shader transition capture when the producer reuses a browser probe session. The scripted audio volume forces the probe path; the shader transition requires the final capture page to be recreated after the page-side compositor pre-head script is installed and to use screenshot capture instead of BeginFrame. Without that fix, the rendered MP4 captures the fallback DOM state instead of the shader warp.",
"tags": ["regression", "render-compat", "shader"],
"minPsnr": 45,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1
}
}
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:adf207334fd405162b2989f861819f176116661de4aa3432344a123d42bdd23a
size 1029195
@@ -0,0 +1,113 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Page-Side Shader Compositor Regression</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="vendor/hyper-shader.global.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
width: 640px;
height: 360px;
overflow: hidden;
background: #05070c;
}
#main {
position: relative;
width: 640px;
height: 360px;
overflow: hidden;
background: #05070c;
font-family: Arial, sans-serif;
}
.scene {
position: absolute;
inset: 0;
display: grid;
place-items: center;
opacity: 0;
overflow: hidden;
}
#scene-a {
opacity: 1;
background:
radial-gradient(circle at 22% 24%, rgba(255, 255, 255, 0.78) 0 42px, transparent 43px),
linear-gradient(135deg, #ff3f6e 0%, #6812d6 52%, #0b1030 100%);
}
#scene-b {
background:
radial-gradient(circle at 78% 70%, rgba(255, 255, 255, 0.72) 0 54px, transparent 55px),
linear-gradient(135deg, #03131f 0%, #06966d 45%, #e0fb55 100%);
}
.word {
position: relative;
z-index: 2;
color: #ffffff;
font-size: 92px;
font-weight: 900;
line-height: 0.9;
letter-spacing: 0;
text-shadow: 0 8px 28px rgba(0, 0, 0, 0.42);
}
.stripes {
position: absolute;
inset: -80px;
background: repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.42) 0 18px,
transparent 18px 46px
);
transform: rotate(-18deg);
}
</style>
</head>
<body>
<div
id="main"
data-composition-id="page-side-shader-compositor-render-compat"
data-width="640"
data-height="360"
data-start="0"
data-duration="2"
>
<audio id="probe-audio" src="silent.wav" data-start="0" data-duration="2"></audio>
<section id="scene-a" class="scene clip" data-start="0" data-duration="2">
<div class="stripes"></div>
<div class="word">ALPHA</div>
</section>
<section id="scene-b" class="scene clip" data-start="0" data-duration="2">
<div class="stripes"></div>
<div class="word">BETA</div>
</section>
</div>
<script>
const probeAudio = document.getElementById("probe-audio");
probeAudio.volume = 0.25;
const tl = gsap.timeline({ paused: true });
HyperShader.init({
bgColor: "#05070c",
accentColor: "#7dd3fc",
scenes: ["scene-a", "scene-b"],
transitions: [{ time: 0.75, shader: "glitch", duration: 0.85, ease: "none" }],
timeline: tl,
compositionId: "page-side-shader-compositor-render-compat",
});
window.__timelines = window.__timelines || {};
window.__timelines["page-side-shader-compositor-render-compat"] = tl;
</script>
</body>
</html>
File diff suppressed because one or more lines are too long