fix(producer): avoid reviving hidden DOM in HDR layers (#1935)

* fix(producer): avoid reviving hidden DOM in HDR layers

* fix(producer): filter transition HDR DOM masks

* fix(producer): keep hidden timed descendants masked
This commit is contained in:
Miguel Ángel
2026-07-04 15:30:56 -07:00
committed by GitHub
parent 16fe1368ff
commit bb066077b4
9 changed files with 923 additions and 19 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import type { ElementStackingInfo } from "@hyperframes/engine";
import { selectDomLayerShowIds } from "./hdrCompositor.js";
function makeEl(id: string, overrides?: Partial<ElementStackingInfo>): ElementStackingInfo {
return {
id,
zIndex: 0,
x: 0,
y: 0,
width: 1920,
height: 1080,
layoutWidth: 1920,
layoutHeight: 1080,
opacity: 1,
visible: true,
renderFrameVisible: false,
isHdr: false,
transform: "none",
borderRadius: [0, 0, 0, 0],
objectFit: "fill",
objectPosition: "50% 50%",
clipRect: null,
...overrides,
};
}
describe("selectDomLayerShowIds", () => {
it("does not re-show hidden DOM elements while preserving visible injected video frames", () => {
expect(
selectDomLayerShowIds(
["visible-overlay", "hidden-later-scene", "hidden-sdr-video"],
[
makeEl("visible-overlay"),
makeEl("hidden-later-scene", { visible: false }),
makeEl("hidden-sdr-video", {
visible: false,
renderFrameVisible: true,
}),
],
),
).toEqual(["visible-overlay", "hidden-sdr-video"]);
});
it("does not re-show opacity-zero scene members or their injected frames", () => {
expect(
selectDomLayerShowIds(
["active-overlay", "inactive-scene-video", "inactive-scene-label"],
[
makeEl("active-overlay"),
makeEl("inactive-scene-video", {
opacity: 0,
visible: false,
renderFrameVisible: true,
}),
makeEl("inactive-scene-label", {
opacity: 0,
visible: true,
}),
],
),
).toEqual(["active-overlay"]);
});
});
@@ -416,6 +416,17 @@ export function resolveCompositeTransfer(
return hasHdrContent && effectiveHdr ? effectiveHdr.transfer : "srgb";
}
export function selectDomLayerShowIds(
layerElementIds: string[],
fullStacking: ElementStackingInfo[],
): string[] {
const byId = new Map(fullStacking.map((el) => [el.id, el]));
return layerElementIds.filter((id) => {
const el = byId.get(id);
return !!el && el.opacity > 0 && (el.visible || el.renderFrameVisible === true);
});
}
export interface HdrCompositeContext {
log: ProducerLogger;
domSession: CaptureSession;
@@ -607,10 +618,10 @@ export async function compositeHdrFrame(
//
// The mask:
// - mass-hides every body descendant via stylesheet
// - re-shows the layer's elements (and their descendants and
// their injected `__render_frame_*` siblings) so deep-nested
// content stays visible even though intermediate ancestors
// are hidden
// - re-shows the layer's currently paintable elements (and their
// descendants and injected `__render_frame_*` siblings) so
// deep-nested content stays visible even though intermediate
// ancestors are hidden
// - inline-hides every other data-start element so they don't
// paint when they happen to be descendants of a layer element
// (most importantly: HDR videos and other-layer SDR videos
@@ -621,6 +632,17 @@ export async function compositeHdrFrame(
// border/box-shadow of cards, etc.) and the resulting opaque
// pixels overwrite previously composited HDR content beneath.
const layerIds = new Set(layer.elementIds);
const showIds = selectDomLayerShowIds(layer.elementIds, fullStacking);
if (showIds.length === 0) {
if (shouldLog) {
log.info("[diag] dom layer skipped, all elements not paintable", {
frame: debugFrameIndex,
layerIdx,
layerIds: layer.elementIds,
});
}
continue;
}
const hideIds = allElementIds.filter((id) => !layerIds.has(id));
if (hdrPerf) hdrPerf.domLayerCaptures += 1;
@@ -640,7 +662,7 @@ export async function compositeHdrFrame(
// 3. Install the mask (mass-hide stylesheet + inline-hide non-layer ids)
await timeHdrPhaseAsync(hdrPerf, "domMaskApplyMs", () =>
applyDomLayerMask(domSession.page, layer.elementIds, hideIds),
applyDomLayerMask(domSession.page, showIds, hideIds),
);
// 4. Screenshot
@@ -669,6 +691,7 @@ export async function compositeHdrFrame(
frame: debugFrameIndex,
layerIdx,
layerIds: layer.elementIds,
showIds,
hideCount: hideIds.length,
pngBytes: domPng.length,
alphaPixels,
@@ -0,0 +1,121 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ElementStackingInfo } from "@hyperframes/engine";
import {
applyDomLayerMask,
blitRgba8OverRgb48le,
captureAlphaPng,
decodePng,
removeDomLayerMask,
} from "@hyperframes/engine";
import type { ProducerLogger } from "../../../logger.js";
import type { HdrCompositeContext } from "../../hdrCompositor.js";
import { captureSceneIntoBuffer } from "./captureHdrFrameShared.js";
vi.mock("@hyperframes/engine", async (importOriginal) => {
const actual = await importOriginal<typeof import("@hyperframes/engine")>();
return {
...actual,
applyDomLayerMask: vi.fn(async () => undefined),
captureAlphaPng: vi.fn(async () => Buffer.from("png")),
decodePng: vi.fn(() => ({ data: Buffer.alloc(4), width: 1, height: 1 })),
removeDomLayerMask: vi.fn(async () => undefined),
blitRgba8OverRgb48le: vi.fn(),
};
});
function makeEl(id: string, overrides?: Partial<ElementStackingInfo>): ElementStackingInfo {
return {
id,
zIndex: 0,
x: 0,
y: 0,
width: 1,
height: 1,
layoutWidth: 1,
layoutHeight: 1,
opacity: 1,
visible: true,
renderFrameVisible: false,
isHdr: false,
transform: "none",
borderRadius: [0, 0, 0, 0],
objectFit: "fill",
objectPosition: "50% 50%",
clipRect: null,
...overrides,
};
}
function makeContext(): HdrCompositeContext {
return {
log: makeLogger(),
domSession: { page: { evaluate: vi.fn() } } as never,
beforeCaptureHook: null,
width: 1,
height: 1,
fps: 30,
compositeTransfer: "srgb",
nativeHdrImageIds: new Set(),
hdrImageBuffers: new Map(),
hdrImageTransferCache: new Map(),
hdrVideoFrameSources: new Map(),
hdrVideoStartTimes: new Map(),
imageTransfers: new Map(),
videoTransfers: new Map(),
debugDumpEnabled: false,
debugDumpDir: null,
};
}
function makeLogger(): ProducerLogger {
return {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
} as unknown as ProducerLogger;
}
describe("captureSceneIntoBuffer", () => {
beforeEach(() => {
vi.mocked(applyDomLayerMask).mockClear();
vi.mocked(captureAlphaPng).mockClear();
vi.mocked(decodePng).mockClear();
vi.mocked(removeDomLayerMask).mockClear();
vi.mocked(blitRgba8OverRgb48le).mockClear();
});
it("does not re-show hidden transition-scene members in the DOM mask", async () => {
const page = { evaluate: vi.fn(async () => undefined) };
await captureSceneIntoBuffer({
session: { page, onBeforeCapture: null } as never,
sceneBuf: Buffer.alloc(6),
sceneIds: new Set(["visible-overlay", "hidden-inner", "hidden-sdr-video"]),
stackingInfo: [
makeEl("visible-overlay"),
makeEl("hidden-inner", { visible: false }),
makeEl("hidden-sdr-video", { visible: false, renderFrameVisible: true }),
makeEl("outside-scene"),
],
time: 0.5,
width: 1,
height: 1,
nativeHdrIds: new Set(),
nativeHdrImageIds: new Set(),
beforeCaptureHook: null,
hdrCompositeCtx: makeContext(),
compositeTransfer: "srgb",
hdrTargetTransfer: undefined,
hdrPerf: undefined,
log: makeLogger(),
frameIdx: 15,
});
expect(applyDomLayerMask).toHaveBeenCalledWith(
page,
["visible-overlay", "hidden-sdr-video"],
["outside-scene"],
);
});
});
@@ -29,6 +29,7 @@ import {
blitHdrImageLayer,
blitHdrVideoLayer,
closeHdrVideoFrameSource,
selectDomLayerShowIds,
} from "../../hdrCompositor.js";
import {
type HdrPerfCollector,
@@ -234,10 +235,11 @@ export async function captureSceneIntoBuffer(a: CaptureSceneArgs): Promise<void>
);
}
}
const showIds = Array.from(sceneIds);
const showIds = selectDomLayerShowIds(Array.from(sceneIds), stackingInfo);
const hideIds = stackingInfo
.map((e) => e.id)
.filter((id) => !sceneIds.has(id) || nativeHdrIds.has(id));
if (showIds.length === 0) return;
if (hdrPerf) hdrPerf.domLayerCaptures += 1;
await timeHdrPhaseAsync(hdrPerf, "domMaskApplyMs", () =>
applyDomLayerMask(session.page, showIds, hideIds),