mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(producer): capture page-side shader composites via screenshot (#1656)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
+139
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>
|
||||
Binary file not shown.
Vendored
+39
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user