fix(producer): extract HDR video frames a window at a time to bound scratch disk (#6304)

* fix(producer): extract HDR video frames a window at a time so scratch disk stays bounded

* fix(producer): drop the added compositor comment for the Comments gate

* fix(producer): keep the HDR scratch read plain and fail the render when a window cannot be extracted

* fix(producer): settle a looped HDR source's frame count before capture starts

* chore(producer): raise the packed size budget by the windowed HDR extraction's measured delta
This commit is contained in:
Miguel Ángel
2026-10-25 15:13:31 +00:00
committed by GitHub
parent 9763b68a33
commit 7569ec1be5
9 changed files with 755 additions and 286 deletions
@@ -73,7 +73,7 @@ describe("blitHdrVideoLayer", () => {
{ lead: 7, mediaStart: 6.5, painted: true },
])(
"paints at 1 s of a clip from $mediaStart s whose stream starts $lead s in: $painted",
({ lead, mediaStart, painted }) => {
async ({ lead, mediaStart, painted }) => {
const dir = mkdtempSync(join(tmpdir(), "hf-hdr-lead-"));
const rawPath = join(dir, "frames.rgb48le");
const frameSize = 2 * 2 * 6;
@@ -90,8 +90,15 @@ describe("blitHdrVideoLayer", () => {
});
const source = {
dir,
rawPath,
fd,
windows: {
totalFrames: 2,
lease: async (index: number) => ({
fd,
offset: index * frameSize,
release: () => {},
}),
close: () => {},
},
width: 2,
height: 2,
frameSize,
@@ -100,7 +107,16 @@ describe("blitHdrVideoLayer", () => {
mediaStart,
videoStreamLeadSeconds: lead,
};
blitHdrVideoLayer(canvas, el, 1, 1, new Map([["v", source]]), new Map([["v", 0]]), 2, 2);
await blitHdrVideoLayer(
canvas,
el,
1,
1,
new Map([["v", source]]),
new Map([["v", 0]]),
2,
2,
);
expect(canvas.some((byte) => byte !== 0)).toBe(painted);
} finally {
closeSync(fd);
@@ -108,4 +124,37 @@ describe("blitHdrVideoLayer", () => {
}
},
);
it("fails the render when a window cannot be extracted, instead of dropping the layer", async () => {
const frameSize = 2 * 2 * 6;
const source = {
dir: "unused",
windows: {
totalFrames: 2,
lease: async () => {
throw new Error("HDR frame extraction failed");
},
close: () => {},
},
width: 2,
height: 2,
frameSize,
frameCount: 2,
scratch: Buffer.alloc(frameSize),
};
const el = makeEl("v", { width: 2, height: 2, layoutWidth: 2, layoutHeight: 2, isHdr: true });
await expect(
blitHdrVideoLayer(
Buffer.alloc(frameSize),
el,
1,
1,
new Map([["v", source]]),
new Map([["v", 0]]),
2,
2,
),
).rejects.toThrow("HDR frame extraction failed");
});
});
+47 -16
View File
@@ -11,7 +11,8 @@
* `captureHdrResources.ts`.
*/
import { readSync, closeSync } from "fs";
import { readSync } from "fs";
import type { HdrFrameLease, HdrFrameWindows } from "./hdrFrameWindows.js";
import { join } from "path";
import {
type CaptureSession,
@@ -133,14 +134,13 @@ function cropRgb48le(
*/
export interface HdrVideoFrameSource {
dir: string;
rawPath: string;
fd: number;
windows: HdrFrameWindows;
width: number;
height: number;
frameSize: number;
frameCount: number;
readonly frameCount: number;
scratch: Buffer;
/** The raw file contains one playable source cycle and must wrap at EOF. */
/** The playable range is one source cycle and must wrap at its end. */
loop?: boolean;
mediaStart?: number;
videoStreamLeadSeconds?: number;
@@ -148,10 +148,10 @@ export interface HdrVideoFrameSource {
export function closeHdrVideoFrameSource(source: HdrVideoFrameSource, log?: ProducerLogger): void {
try {
closeSync(source.fd);
source.windows.close();
} catch (err) {
log?.warn("Failed to close HDR raw frame file", {
rawPath: source.rawPath,
log?.warn("Failed to close HDR raw frame windows", {
dir: source.dir,
error: err instanceof Error ? err.message : String(err),
});
}
@@ -169,8 +169,7 @@ export function resolveHdrVideoFrameIndex(
return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
}
// fallow-ignore-next-line complexity
export function blitHdrVideoLayer(
export async function blitHdrVideoLayer(
canvas: Buffer,
el: ElementStackingInfo,
time: number,
@@ -183,7 +182,7 @@ export function blitHdrVideoLayer(
sourceTransfer?: HdrTransfer,
targetTransfer?: HdrTransfer,
hdrPerf?: HdrPerfCollector,
): void {
): Promise<void> {
const frameSource = hdrVideoFrameSources.get(el.id);
const startTime = hdrStartTimes.get(el.id);
if (!frameSource || startTime === undefined || el.opacity <= 0) {
@@ -203,12 +202,44 @@ export function blitHdrVideoLayer(
if (effectiveIndex === null) return;
const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
const frameOffset = effectiveIndex * frameSource.frameSize;
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
const lease = await frameSource.windows.lease(effectiveIndex);
if (!lease) return;
try {
blitScratchFrameWithoutYielding(
canvas,
el,
frameSource,
lease,
width,
height,
log,
sourceTransfer,
targetTransfer,
hdrPerf,
);
} finally {
lease.release();
}
}
// fallow-ignore-next-line complexity
function blitScratchFrameWithoutYielding(
canvas: Buffer,
el: ElementStackingInfo,
frameSource: HdrVideoFrameSource,
lease: HdrFrameLease,
width: number,
height: number,
log: ProducerLogger | undefined,
sourceTransfer: HdrTransfer | undefined,
targetTransfer: HdrTransfer | undefined,
hdrPerf: HdrPerfCollector | undefined,
): void {
try {
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
const bytesRead = timeHdrPhase(hdrPerf, "hdrVideoReadDecodeMs", () =>
readSync(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset),
readSync(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset),
);
if (bytesRead !== frameSource.frameSize) return;
const hdrRgb = frameSource.scratch;
@@ -583,7 +614,7 @@ export async function compositeHdrFrame(
hdrPerf,
);
} else {
blitHdrVideoLayer(
await blitHdrVideoLayer(
canvas,
layer.element,
time,
@@ -627,7 +658,7 @@ export async function compositeHdrFrame(
startTime,
localTime: localTime.toFixed(3),
hdrFrameNum: frameNum,
rawPath: frameSource?.rawPath ?? null,
dir: frameSource?.dir ?? null,
frameCount: frameSource?.frameCount ?? null,
});
}
@@ -0,0 +1,166 @@
import { mkdtempSync, readdirSync, readSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createHdrFrameWindows, type HdrWindowSpec } from "./hdrFrameWindows.js";
const FRAME_SIZE = 4;
const dirs: string[] = [];
afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
function fixture(opts: {
totalFrames: number;
windowFrames: number;
maxResident: number;
/** Frames the source really has; fewer than totalFrames models a short source. */
sourceFrames?: number;
}) {
const dir = mkdtempSync(join(tmpdir(), "hf-hdr-windows-"));
dirs.push(dir);
const extracted: number[] = [];
const windows = createHdrFrameWindows({
dir,
frameSize: FRAME_SIZE,
totalFrames: opts.totalFrames,
windowFrames: opts.windowFrames,
maxResident: opts.maxResident,
extract: async (spec: HdrWindowSpec) => {
extracted.push(spec.firstFrame);
const available = Math.max(0, (opts.sourceFrames ?? opts.totalFrames) - spec.firstFrame);
const frames = Math.min(spec.frames, available);
const data = Buffer.alloc(frames * FRAME_SIZE);
for (let j = 0; j < frames; j++) data.fill(spec.firstFrame + j, j * FRAME_SIZE);
writeFileSync(spec.path, data);
return frames;
},
});
return { dir, windows, extracted };
}
async function frameByte(
windows: ReturnType<typeof createHdrFrameWindows>,
index: number,
): Promise<number | null> {
const lease = await windows.lease(index);
if (!lease) return null;
try {
const buf = Buffer.alloc(FRAME_SIZE);
readSync(lease.fd, buf, 0, FRAME_SIZE, lease.offset);
return buf[0] ?? null;
} finally {
lease.release();
}
}
const windowFiles = (dir: string) => readdirSync(dir).filter((f) => f.startsWith("window-"));
describe("createHdrFrameWindows", () => {
it("extracts a window only when a frame inside it is read, and reads that frame's bytes", async () => {
const { windows, extracted } = fixture({ totalFrames: 12, windowFrames: 4, maxResident: 3 });
expect(extracted).toEqual([]);
expect(await frameByte(windows, 5)).toBe(5);
expect(extracted).toEqual([4]);
windows.close();
});
it("keeps no more than maxResident windows on disk while the capture walks the whole clip", async () => {
const { dir, windows } = fixture({ totalFrames: 40, windowFrames: 4, maxResident: 2 });
let mostFiles = 0;
for (let i = 0; i < 40; i++) {
expect(await frameByte(windows, i)).toBe(i);
mostFiles = Math.max(mostFiles, windowFiles(dir).length);
}
expect(mostFiles).toBeLessThanOrEqual(2);
windows.close();
});
it("never evicts a window that is leased", async () => {
const { windows } = fixture({ totalFrames: 16, windowFrames: 4, maxResident: 2 });
const held = await windows.lease(0);
expect(held).not.toBeNull();
for (const i of [4, 8, 12]) await frameByte(windows, i);
const buf = Buffer.alloc(FRAME_SIZE);
expect(readSync(held!.fd, buf, 0, FRAME_SIZE, held!.offset)).toBe(FRAME_SIZE);
expect(buf[0]).toBe(0);
held!.release();
windows.close();
});
it("holds the last frame when the source ends before the planned length", async () => {
const { windows } = fixture({
totalFrames: 10,
windowFrames: 4,
maxResident: 3,
sourceFrames: 6,
});
expect(await frameByte(windows, 9)).toBe(5);
expect(windows.totalFrames).toBe(6);
windows.close();
});
it("holds the last frame when the source ends exactly at a window boundary", async () => {
const { windows } = fixture({
totalFrames: 12,
windowFrames: 4,
maxResident: 3,
sourceFrames: 4,
});
expect(await frameByte(windows, 9)).toBe(3);
expect(windows.totalFrames).toBe(4);
windows.close();
});
it("reports no frame for a source that produced none", async () => {
const { windows } = fixture({
totalFrames: 4,
windowFrames: 4,
maxResident: 2,
sourceFrames: 0,
});
expect(await frameByte(windows, 0)).toBeNull();
windows.close();
});
it("retries a window whose extraction failed", async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-hdr-windows-"));
dirs.push(dir);
let attempts = 0;
const windows = createHdrFrameWindows({
dir,
frameSize: FRAME_SIZE,
totalFrames: 4,
windowFrames: 4,
maxResident: 2,
extract: async (spec) => {
attempts += 1;
if (attempts === 1) throw new Error("ffmpeg failed");
writeFileSync(spec.path, Buffer.alloc(spec.frames * FRAME_SIZE, 7));
return spec.frames;
},
});
await expect(windows.lease(0)).rejects.toThrow("ffmpeg failed");
expect(await frameByte(windows, 0)).toBe(7);
windows.close();
});
it("removes its window files on close", async () => {
const { dir, windows } = fixture({ totalFrames: 8, windowFrames: 4, maxResident: 3 });
await frameByte(windows, 0);
await frameByte(windows, 5);
expect(windowFiles(dir).length).toBeGreaterThan(0);
windows.close();
expect(windowFiles(dir)).toEqual([]);
});
});
@@ -0,0 +1,124 @@
/**
* Bounded raw-frame storage for one HDR video: fixed windows of frames, extracted on first lease,
* least-recently-used evicted past `maxResident`. A leased window is never evicted.
*/
import { closeSync, constants, openSync, rmSync } from "node:fs";
import { join } from "node:path";
export interface HdrFrameLease {
fd: number;
offset: number;
release(): void;
}
export interface HdrWindowSpec {
firstFrame: number;
frames: number;
path: string;
}
export interface HdrFrameWindows {
readonly totalFrames: number;
lease(frameIndex: number): Promise<HdrFrameLease | null>;
close(): void;
}
interface Window {
firstFrame: number;
path: string;
frames: number;
fd: number;
pins: number;
ready: Promise<void>;
}
export function createHdrFrameWindows(opts: {
dir: string;
frameSize: number;
totalFrames: number;
windowFrames: number;
maxResident: number;
extract: (spec: HdrWindowSpec) => Promise<number>;
}): HdrFrameWindows {
const { dir, frameSize, windowFrames, maxResident, extract } = opts;
let totalFrames = opts.totalFrames;
// Map order is recency order: a leased window moves to the end.
const windows = new Map<number, Window>();
const drop = (index: number, w: Window): void => {
windows.delete(index);
if (w.fd >= 0) closeSync(w.fd);
rmSync(w.path, { force: true });
};
const open = (index: number): Window => {
const known = windows.get(index);
if (known) {
windows.delete(index);
windows.set(index, known);
return known;
}
for (const [i, w] of windows) {
if (windows.size < maxResident) break;
if (w.pins === 0) drop(i, w);
}
const firstFrame = index * windowFrames;
const w: Window = {
firstFrame,
path: join(dir, `window-${index}.rgb48le`),
frames: 0,
fd: -1,
pins: 0,
ready: Promise.resolve(),
};
w.ready = extract({
firstFrame,
frames: Math.min(windowFrames, totalFrames - firstFrame),
path: w.path,
}).then((produced) => {
if (produced < 1) return;
w.fd = openSync(w.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
w.frames = produced;
});
windows.set(index, w);
return w;
};
return {
get totalFrames() {
return totalFrames;
},
async lease(frameIndex) {
for (;;) {
if (totalFrames < 1) return null;
const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
const key = Math.floor(index / windowFrames);
const w = open(key);
w.pins += 1;
try {
await w.ready;
} catch (error) {
w.pins -= 1;
windows.delete(key);
throw error;
}
const local = index - w.firstFrame;
if (local < w.frames) {
return {
fd: w.fd,
offset: local * frameSize,
release: () => {
w.pins -= 1;
},
};
}
// The source ended inside or before this window: hold its last frame.
w.pins -= 1;
totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
}
},
close() {
for (const [i, w] of [...windows]) drop(i, w);
},
};
}
@@ -219,7 +219,7 @@ export async function captureSceneIntoBuffer(a: CaptureSceneArgs): Promise<void>
hdrPerf,
);
} else {
blitHdrVideoLayer(
await blitHdrVideoLayer(
sceneBuf,
el,
time,
@@ -1,12 +1,4 @@
import {
closeSync,
constants,
existsSync,
mkdtempSync,
openSync,
rmSync,
writeFileSync,
} from "node:fs";
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { spawnSync } from "node:child_process";
@@ -23,7 +15,6 @@ import { createRenderJob } from "../../renderOrchestrator.js";
import { resolveHdrVideoFrameIndex } from "../../hdrCompositor.js";
import {
cleanupHdrVideoFrameSource,
estimateHdrExtractionBytes,
extractHdrVideoFrames,
getHdrExtractionReservedBytes,
planHdrResources,
@@ -52,6 +43,25 @@ function ffmpegResult(success: boolean): RunFfmpegResult {
const HAS_FFMPEG = spawnSync("ffmpeg", ["-version"]).status === 0;
/**
* A mock FFmpeg: answers the first-frame probe and fills each extraction's output with its frames,
* stopping where the source's `realFrames` (at the fixture's 2 fps) run out.
*/
function windowWriter(calls: string[][], realFrames = Infinity) {
return async (args: string[]): Promise<RunFfmpegResult> => {
if (args.at(-1) === "-") return { ...ffmpegResult(true), stderr: "pts_time:0.000000" };
calls.push(args);
const out = args.at(-1);
if (!out) throw new Error("mock FFmpeg output path missing");
const filter = args[args.indexOf("-vf") + 1] ?? "";
const lead = Number(/trim=start_frame=(\d+)/.exec(filter)?.[1] ?? 0);
const first = Math.round(Number(args[args.indexOf("-ss") + 1]) * 2) + lead;
const wanted = Number(args[args.indexOf("-frames:v") + 1]);
writeFileSync(out, Buffer.alloc(Math.max(0, Math.min(wanted, realFrames - first)) * 6));
return ffmpegResult(true);
};
}
function hdrVideo(id: string, overrides: Partial<VideoElement> = {}): VideoElement {
return {
id,
@@ -125,7 +135,17 @@ function videoComposition(src: string): CompositionMetadata {
height: 1080,
audios: [],
images: [],
videos: [{ id: "a-roll", src, start: 0, end: 5, mediaStart: 0, loop: false, hasAudio: false }],
videos: [
{
id: "a-roll",
src,
start: 0,
end: 5,
mediaStart: 0,
loop: false,
hasAudio: false,
},
],
};
}
@@ -169,41 +189,11 @@ describe("planHdrResources non-ASCII src resolution (PRINFRA-349)", () => {
});
});
describe("estimateHdrExtractionBytes", () => {
it("sums 6 bytes per pixel per frame across videos", () => {
// 10s @ 30fps of 1920x1080 = 300 frames * 1920*1080*6
expect(
estimateHdrExtractionBytes([{ durationSeconds: 10, width: 1920, height: 1080 }], 30),
).toBe(300 * 1920 * 1080 * 6);
});
it("accumulates multiple videos and rounds frame counts up", () => {
const bytes = estimateHdrExtractionBytes(
[
{ durationSeconds: 1.5, width: 100, height: 100 },
{ durationSeconds: 0.05, width: 100, height: 100 },
],
30,
);
expect(bytes).toBe((45 + 2) * 100 * 100 * 6);
});
it("treats negative durations as empty", () => {
expect(estimateHdrExtractionBytes([{ durationSeconds: -3, width: 100, height: 100 }], 30)).toBe(
0,
);
});
});
describe("resolveHdrExtractionWindow", () => {
it("bounds a two-second composition backed by an unbounded HDR source to 60 raw frames", () => {
const window = resolveHdrExtractionWindow(hdrVideo("long-hdr"), 2, videoMetadata(120));
if (!window) throw new Error("expected a visible HDR extraction window");
const { durationSeconds } = window;
expect(durationSeconds).toBe(2);
expect(estimateHdrExtractionBytes([{ durationSeconds, width: 3840, height: 2160 }], 30)).toBe(
60 * 3840 * 2160 * 6,
);
expect(window.durationSeconds).toBe(2);
});
it("independently caps a stale finite media end at the composition duration", () => {
@@ -248,7 +238,12 @@ describe("resolveHdrExtractionWindow", () => {
});
it.each([
{ start: -5, compositionDuration: 2, expected: null, label: "already ended" },
{
start: -5,
compositionDuration: 2,
expected: null,
label: "already ended",
},
{
start: -2,
compositionDuration: 15,
@@ -260,7 +255,11 @@ describe("resolveHdrExtractionWindow", () => {
({ start, compositionDuration, expected }) => {
expect(
resolveHdrExtractionWindow(
hdrVideo("open-held", { start, end: Number.POSITIVE_INFINITY, loop: false }),
hdrVideo("open-held", {
start,
end: Number.POSITIVE_INFINITY,
loop: false,
}),
compositionDuration,
videoMetadata(3),
),
@@ -269,7 +268,11 @@ describe("resolveHdrExtractionWindow", () => {
);
it.each([
{ loop: true, preservation: { preserveTimelinePhase: true }, label: "loop" },
{
loop: true,
preservation: { preserveTimelinePhase: true },
label: "loop",
},
{
loop: false,
preservation: { preserveTimelineEnd: true, ensureFinalFrame: true },
@@ -308,7 +311,12 @@ describe("resolveHdrExtractionWindow", () => {
it("rejects mediaStart at source EOF before HDR budgeting", () => {
expect(() =>
resolveHdrExtractionWindow(hdrVideo("past-eof", { mediaStart: 3 }), 60, videoMetadata(3)),
).toThrowError(expect.objectContaining({ kind: "media_start_out_of_range", retryable: false }));
).toThrowError(
expect.objectContaining({
kind: "media_start_out_of_range",
retryable: false,
}),
);
});
it("skips HDR media with no interval inside the composition", () => {
@@ -385,8 +393,18 @@ describe.skipIf(!HAS_FFMPEG)("raw HDR held tails on sparse-timestamp sources", (
});
it.each([
{ label: "sub-1fps CFR", src: cfrFixture, expectedVfr: false, streamStart: 0 },
{ label: "sparse VFR", src: vfrFixture, expectedVfr: true, streamStart: 0 },
{
label: "sub-1fps CFR",
src: cfrFixture,
expectedVfr: false,
streamStart: 0,
},
{
label: "sparse VFR",
src: vfrFixture,
expectedVfr: true,
streamStart: 0,
},
{
label: "non-zero stream start",
src: nonZeroStartFixture,
@@ -518,20 +536,20 @@ describe("extractHdrVideoFrames", () => {
try {
const extracted = await extractHdrVideoFrames({
...fixture,
runFfmpegImpl: async (args) => {
calls.push(args);
const rawPath = args.at(-1);
if (!rawPath) throw new Error("mock FFmpeg output path missing");
// The visible [0, 2] interval is two seconds at 2fps = 4 rgb48le 1x1 frames.
writeFileSync(rawPath, Buffer.alloc(4 * 6));
return ffmpegResult(true);
},
// The visible [0, 2] interval is two seconds at 2fps = 4 rgb48le 1x1 frames.
runFfmpegImpl: windowWriter(calls),
});
try {
expect(calls).toHaveLength(1);
const args = calls[0] ?? [];
expect(args.slice(args.indexOf("-ss"), args.indexOf("-ss") + 2)).toEqual(["-ss", "60"]);
expect(args.slice(args.indexOf("-t"), args.indexOf("-t") + 2)).toEqual(["-t", "2"]);
expect(args.slice(args.indexOf("-ss"), args.indexOf("-ss") + 2)).toEqual([
"-ss",
"60.000000",
]);
expect(args.slice(args.indexOf("-frames:v"), args.indexOf("-frames:v") + 2)).toEqual([
"-frames:v",
"4",
]);
expect(fixture.prep.hdrVideoStartTimes.get("preroll")).toBe(0);
expect(extracted.sources.get("preroll")?.frameCount).toBe(4);
expect(extracted.estimatedBytes).toBe(24);
@@ -549,32 +567,27 @@ describe("extractHdrVideoFrames", () => {
{
loop: true,
expectedFrameIndex: 4,
expectedFrameCount: 6,
expectedStart: -5,
expectedSeek: "0",
expectedDuration: "3",
expectedSeek: "0.000000",
expectedFrames: "7",
label: "loop phase",
},
{
loop: false,
expectedFrameIndex: 0,
expectedFrameCount: 1,
expectedStart: 0,
expectedSeek: "2",
expectedDuration: undefined,
expectedFrames: "1",
label: "held final frame",
},
])("preserves $label after materially negative preroll", async (testCase) => {
const {
loop,
expectedFrameIndex,
expectedFrameCount,
expectedStart,
expectedSeek,
expectedDuration,
} = testCase;
const { loop, expectedFrameIndex, expectedStart, expectedSeek, expectedFrames } = testCase;
const framesDir = mkdtempSync(join(tmpdir(), "hf-hdr-negative-source-"));
const video = hdrVideo(`negative-${String(loop)}`, { start: -5, end: 10, loop });
const video = hdrVideo(`negative-${String(loop)}`, {
start: -5,
end: 10,
loop,
});
const fixture = hdrExtractionFixture([video], framesDir);
const calls: string[][] = [];
@@ -593,13 +606,7 @@ describe("extractHdrVideoFrames", () => {
finalFrameOnly: true,
}
: window,
runFfmpegImpl: async (args) => {
calls.push(args);
const rawPath = args.at(-1);
if (!rawPath) throw new Error("mock FFmpeg output path missing");
writeFileSync(rawPath, Buffer.alloc(expectedFrameCount * 6));
return ffmpegResult(true);
},
runFfmpegImpl: windowWriter(calls, 6),
});
try {
const args = calls[0] ?? [];
@@ -607,18 +614,10 @@ describe("extractHdrVideoFrames", () => {
"-ss",
expectedSeek,
]);
if (expectedDuration === undefined) {
expect(args).not.toContain("-t");
expect(args.slice(args.indexOf("-frames:v"), args.indexOf("-frames:v") + 2)).toEqual([
"-frames:v",
"1",
]);
} else {
expect(args.slice(args.indexOf("-t"), args.indexOf("-t") + 2)).toEqual([
"-t",
expectedDuration,
]);
}
expect(args.slice(args.indexOf("-frames:v"), args.indexOf("-frames:v") + 2)).toEqual([
"-frames:v",
expectedFrames,
]);
expect(fixture.prep.hdrVideoStartTimes.get(video.id)).toBe(expectedStart);
const source = extracted.sources.get(video.id);
expect(source?.loop).toBe(loop);
@@ -652,18 +651,18 @@ describe("extractHdrVideoFrames", () => {
// A valid mux can have a short video stream and a much longer audio
// stream/container. Scratch planning must follow video EOF.
extractMediaMetadataImpl: async () => videoMetadata(60, 3),
runFfmpegImpl: async (args) => {
calls.push(args);
const rawPath = args.at(-1);
if (!rawPath) throw new Error("mock FFmpeg output path missing");
writeFileSync(rawPath, Buffer.alloc(6 * 6));
return ffmpegResult(true);
},
runFfmpegImpl: windowWriter(calls, 6),
});
try {
const args = calls[0] ?? [];
expect(args.slice(args.indexOf("-ss"), args.indexOf("-ss") + 2)).toEqual(["-ss", "0"]);
expect(args.slice(args.indexOf("-t"), args.indexOf("-t") + 2)).toEqual(["-t", "3"]);
expect(args.slice(args.indexOf("-ss"), args.indexOf("-ss") + 2)).toEqual([
"-ss",
"0.000000",
]);
expect(args.slice(args.indexOf("-frames:v"), args.indexOf("-frames:v") + 2)).toEqual([
"-frames:v",
loop ? "7" : "6",
]);
expect(extracted.estimatedBytes).toBe(36);
expect(getHdrExtractionReservedBytes()).toBe(36);
expect(fixture.prep.hdrVideoStartTimes.get(video.id)).toBe(0);
@@ -693,6 +692,7 @@ describe("extractHdrVideoFrames", () => {
extractHdrVideoFrames({
...fixture,
runFfmpegImpl: async (args) => {
if (args.at(-1) === "-") return ffmpegResult(true);
call += 1;
const rawPath = args.at(-1);
if (!rawPath) throw new Error("mock FFmpeg output path missing");
@@ -715,50 +715,113 @@ describe("extractHdrVideoFrames", () => {
});
describe("cleanupHdrVideoFrameSource", () => {
it("closes the raw descriptor and immediately removes its directory", () => {
const dir = mkdtempSync(join(tmpdir(), "hf-hdr-cleanup-"));
const rawPath = join(dir, "frames.rgb48le");
writeFileSync(rawPath, Buffer.alloc(12));
const fd = openSync(rawPath, constants.O_RDONLY);
cleanupHdrVideoFrameSource({
dir,
rawPath,
fd,
width: 1,
height: 2,
frameSize: 12,
frameCount: 1,
scratch: Buffer.alloc(12),
});
expect(existsSync(dir)).toBe(false);
expect(() => closeSync(fd)).toThrow();
const sourceIn = (dir: string, close: () => void) => ({
dir,
windows: { totalFrames: 1, lease: async () => null, close },
width: 1,
height: 2,
frameSize: 12,
frameCount: 1,
scratch: Buffer.alloc(12),
});
it("closes the descriptor but retains raw files with KEEP_TEMP=1", () => {
it("closes the windows and immediately removes their directory", () => {
const dir = mkdtempSync(join(tmpdir(), "hf-hdr-cleanup-"));
writeFileSync(join(dir, "window-0.rgb48le"), Buffer.alloc(12));
const close = vi.fn();
cleanupHdrVideoFrameSource(sourceIn(dir, close));
expect(existsSync(dir)).toBe(false);
expect(close).toHaveBeenCalledOnce();
});
it("closes the windows but retains raw files with KEEP_TEMP=1", () => {
vi.stubEnv("KEEP_TEMP", "1");
const dir = mkdtempSync(join(tmpdir(), "hf-hdr-keep-temp-"));
const rawPath = join(dir, "frames.rgb48le");
const rawPath = join(dir, "window-0.rgb48le");
writeFileSync(rawPath, Buffer.alloc(12));
const fd = openSync(rawPath, constants.O_RDONLY);
const close = vi.fn();
try {
cleanupHdrVideoFrameSource({
dir,
rawPath,
fd,
width: 1,
height: 2,
frameSize: 12,
frameCount: 1,
scratch: Buffer.alloc(12),
});
cleanupHdrVideoFrameSource(sourceIn(dir, close));
expect(existsSync(rawPath)).toBe(true);
expect(() => closeSync(fd)).toThrow();
expect(close).toHaveBeenCalledOnce();
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
describe("extractHdrVideoFrames scratch bound", () => {
it("holds a long clip in a few windows instead of the whole clip", async () => {
const framesDir = mkdtempSync(join(tmpdir(), "hf-hdr-bounded-"));
const video = hdrVideo("long", { start: 0, end: 20, mediaStart: 0 });
const fixture = hdrExtractionFixture([video], framesDir);
fixture.composition.duration = 20;
const calls: string[][] = [];
try {
const extracted = await extractHdrVideoFrames({
...fixture,
maxResidentWindows: 2,
windowBytes: 8 * 6,
extractMediaMetadataImpl: async () => videoMetadata(20),
runFfmpegImpl: windowWriter(calls),
});
try {
const source = extracted.sources.get("long");
// 20 s at 2 fps is 40 one-pixel frames, held 8 per window.
expect(source?.frameCount).toBe(40);
expect(extracted.estimatedBytes).toBe(2 * 8 * 6);
let mostFiles = 0;
for (let i = 0; i < 40; i++) {
const lease = await source?.windows.lease(i);
lease?.release();
mostFiles = Math.max(mostFiles, readdirSync(source?.dir ?? "").length);
}
expect(mostFiles).toBeLessThanOrEqual(2);
expect(calls.length).toBe(5);
// The second window starts at frame 8 (4 s): seek a whole second before it, trim the lead-in.
const second = calls[1] ?? [];
expect(second[second.indexOf("-ss") + 1]).toBe("3.000000");
expect(second[second.indexOf("-vf") + 1]).toContain("fps=2:start_time=0.000000");
expect(second[second.indexOf("-vf") + 1]).toContain("trim=start_frame=2");
} finally {
for (const source of extracted.sources.values()) cleanupHdrVideoFrameSource(source);
extracted.releaseReservation();
}
} finally {
rmSync(framesDir, { recursive: true, force: true });
}
});
});
describe("extractHdrVideoFrames loop frame count", () => {
it("knows a looped source's real frame count before capture starts", async () => {
const framesDir = mkdtempSync(join(tmpdir(), "hf-hdr-loop-count-"));
const video = hdrVideo("loop", { start: 0, end: 20, mediaStart: 0, loop: true });
const fixture = hdrExtractionFixture([video], framesDir);
fixture.composition.duration = 20;
const realFrames = 37;
try {
const extracted = await extractHdrVideoFrames({
...fixture,
windowBytes: 8 * 6,
extractMediaMetadataImpl: async () => videoMetadata(20),
runFfmpegImpl: windowWriter([], realFrames),
});
try {
// 40 planned frames plus one so the real end is observable; the source really has 37.
expect(extracted.sources.get("loop")?.frameCount).toBe(realFrames);
} finally {
for (const source of extracted.sources.values()) cleanupHdrVideoFrameSource(source);
extracted.releaseReservation();
}
} finally {
rmSync(framesDir, { recursive: true, force: true });
}
});
});
@@ -16,17 +16,7 @@
* exist, leaving the caller with empty maps that the hot loop tolerates.
*/
import {
closeSync,
constants,
fstatSync,
mkdirSync,
mkdtempSync,
openSync,
readFileSync,
rmSync,
statfsSync,
} from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, statfsSync } from "node:fs";
import { join } from "node:path";
import {
type CaptureSession,
@@ -52,10 +42,9 @@ import {
} from "../../hdrCompositor.js";
import type { HdrDiagnostics, RenderJob } from "../../renderOrchestrator.js";
import type { CompositionMetadata } from "../shared.js";
import { createHdrFrameWindows } from "../../hdrFrameWindows.js";
import { encoderFailureError } from "../encoderInterruption.js";
const NO_FOLLOW_FLAG = constants.O_NOFOLLOW ?? 0;
function tempDirSafePrefix(id: string): string {
const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
return safe || "video";
@@ -148,10 +137,16 @@ export async function probeHdrExtractionDims(args: {
el.layoutHeight > 0 &&
!prep.hdrExtractionDims.has(el.id)
) {
prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
prep.hdrExtractionDims.set(el.id, {
width: el.layoutWidth,
height: el.layoutHeight,
});
}
if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
prep.hdrImageFitInfo.set(el.id, {
fit: el.objectFit,
position: el.objectPosition,
});
}
}
}
@@ -170,9 +165,15 @@ export async function probeHdrExtractionDims(args: {
const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
for (const el of retryStacking) {
if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
prep.hdrExtractionDims.set(el.id, {
width: el.layoutWidth,
height: el.layoutHeight,
});
if (!prep.hdrImageFitInfo.has(el.id)) {
prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
prep.hdrImageFitInfo.set(el.id, {
fit: el.objectFit,
position: el.objectPosition,
});
}
break;
}
@@ -180,26 +181,6 @@ export async function probeHdrExtractionDims(args: {
}
}
/**
* Total bytes the raw rgb48le pre-extraction below will write: 6 bytes/px ×
* frame area × frame count, summed across HDR videos. Exposed for the
* disk-headroom gate — a plain iPhone HLG clip auto-detected as HDR filled a
* near-full disk with 16-bit raw frames before the user found --sdr (wild
* report, CLI 0.7.52), so the commitment must be checked and surfaced BEFORE
* FFmpeg starts writing.
*/
export function estimateHdrExtractionBytes(
videos: Array<{ durationSeconds: number; width: number; height: number }>,
fps: number,
): number {
let total = 0;
for (const v of videos) {
const frames = Math.ceil(Math.max(0, v.durationSeconds) * fps);
total += frames * v.width * v.height * 6;
}
return total;
}
const HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
const HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
const HDR_EXTRACTION_WARN_BYTES = 10e9;
@@ -299,18 +280,13 @@ export function resolveHdrExtractionWindow(
return window;
}
function cleanupHdrFrameDirectory(
frameDir: string,
rawPath: string | undefined,
log?: ProducerLogger,
): void {
function cleanupHdrFrameDirectory(frameDir: string, log?: ProducerLogger): void {
if (process.env.KEEP_TEMP === "1") return;
try {
rmSync(frameDir, { recursive: true, force: true });
} catch (err) {
log?.warn("Failed to clean up HDR raw frame directory", {
frameDir,
rawPath,
error: err instanceof Error ? err.message : String(err),
});
}
@@ -322,7 +298,7 @@ export function cleanupHdrVideoFrameSource(
log?: ProducerLogger,
): void {
closeHdrVideoFrameSource(source, log);
cleanupHdrFrameDirectory(source.dir, source.rawPath, log);
cleanupHdrFrameDirectory(source.dir, log);
}
/**
@@ -336,11 +312,9 @@ export function cleanupHdrVideoFrameSource(
*/
function assertHdrExtractionDiskHeadroom(
framesDir: string,
plannedVideos: Array<{ durationSeconds: number; width: number; height: number }>,
fps: number,
estimatedBytes: number,
log: ProducerLogger,
): { estimatedBytes: number; budgetBytes: number | undefined } {
const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
const configuredBudget = resolveHdrExtractionBudgetBytes(
process.env[HDR_EXTRACTION_MAX_BYTES_ENV],
);
@@ -389,11 +363,36 @@ export interface HdrVideoExtractionResult {
releaseReservation: () => void;
}
const DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
/** Frames per raw window: the byte budget over one frame, never below a few frames. */
function hdrWindowFrames(frameSize: number, windowBytes: number): number {
return Math.max(8, Math.floor(windowBytes / frameSize));
}
/**
* Extract each HDR video into a raw rgb48le frame file via a single FFmpeg
* pass per video, and open a file descriptor for each. The caller owns both
* source teardown and the aggregate scratch reservation, which intentionally
* remains held until the capture-stage finally block.
* Seconds from the extraction start to the first decoded frame, in output ticks. FFmpeg's `fps`
* grid is anchored to that frame, so every window has to be told the same offset.
*/
async function probeFirstTick(
srcPath: string,
mediaStart: number,
fps: number,
runFfmpegImpl: typeof runFfmpeg,
abortSignal: AbortSignal | undefined,
): Promise<number> {
const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
signal: abortSignal,
});
const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
}
/**
* Plan each HDR video's frame windows; a source extracts a window with one FFmpeg pass when the
* capture first reaches it. The caller owns teardown and the scratch reservation (resident
* windows only), held until the capture-stage finally block.
*/
// fallow-ignore-next-line complexity
export async function extractHdrVideoFrames(args: {
@@ -406,19 +405,24 @@ export async function extractHdrVideoFrames(args: {
height: number;
abortSignal: AbortSignal | undefined;
hdrDiagnostics: HdrDiagnostics;
maxResidentWindows?: number;
windowBytes?: number;
runFfmpegImpl?: typeof runFfmpeg;
extractMediaMetadataImpl?: typeof extractMediaMetadata;
resolveFinalFrameExtractionWindowImpl?: typeof resolveFinalFrameExtractionWindow;
}): Promise<HdrVideoExtractionResult> {
const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } =
args;
const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata;
const resolveFinalFrameExtractionWindowImpl =
args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
const fps = fpsToNumber(job.config.fps);
const out = new Map<string, HdrVideoFrameSource>();
mkdirSync(framesDir, { recursive: true });
const plannedVideos: Array<{ durationSeconds: number; width: number; height: number }> = [];
let scratchBytes = 0;
const extractionWindows = new Map<
string,
{ window: HdrExtractionWindow; videoStreamLeadSeconds?: number }
@@ -442,16 +446,14 @@ export async function extractHdrVideoFrames(args: {
videoStreamLeadSeconds: metadata.videoStreamLeadSeconds,
});
prep.hdrVideoStartTimes.set(videoId, window.compositionStart);
plannedVideos.push({
durationSeconds: window.durationSeconds,
width: dims.width,
height: dims.height,
});
const frameSize = dims.width * dims.height * 6;
const frames = window.finalFrameOnly ? 1 : Math.ceil(window.durationSeconds * fps);
scratchBytes +=
Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
}
const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
framesDir,
plannedVideos,
fpsToNumber(job.config.fps),
scratchBytes,
log,
);
const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -466,79 +468,83 @@ export async function extractHdrVideoFrames(args: {
const frameDir = mkdtempSync(join(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
createdFrameDirs.add(frameDir);
const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
const rawPath = join(frameDir, "frames.rgb48le");
const frameSize = dims.width * dims.height * 6;
const extractionMediaStart = window.extractionMediaStart ?? window.mediaStart;
const extractionStart = String(extractionMediaStart);
const ffmpegArgs: string[] = [];
if (window.finalFrameOnly) {
// Decode before seeking for the one-frame path. Input-side seeking can
// return zero frames for valid unindexed/negative-base transports.
ffmpegArgs.push("-i", srcPath, "-ss", extractionStart, "-frames:v", "1");
} else {
ffmpegArgs.push(
"-ss",
extractionStart,
"-i",
srcPath,
"-t",
String(window.durationSeconds),
const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
const firstTick = window.finalFrameOnly
? 0
: await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
const totalFrames = window.finalFrameOnly
? 1
: Math.max(1, Math.ceil(window.durationSeconds * fps - firstTick - 1e-9)) +
Number(video.loop);
const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
const windows = createHdrFrameWindows({
dir: frameDir,
frameSize,
totalFrames,
windowFrames,
maxResident,
extract: async (spec) => {
const ffmpegArgs = window.finalFrameOnly
? // Decode before seeking for the one-frame path. Input-side seeking can
// return zero frames for valid unindexed/negative-base transports.
["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
: hdrWindowFfmpegArgs({
srcPath,
mediaStart: extractionMediaStart,
fps,
fpsArg: fpsToFfmpegArg(job.config.fps),
firstTick,
firstFrame: spec.firstFrame,
frames: spec.frames,
scale,
});
const result = await runFfmpegImpl(
[...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
{ signal: abortSignal },
);
if (!result.success) {
hdrDiagnostics.videoExtractionFailures += 1;
log.error("HDR frame extraction failed; aborting render", {
videoId,
srcPath,
firstFrame: spec.firstFrame,
stderr: result.stderr.slice(-400),
});
throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
failureReason: result.failureReason,
});
}
return Math.min(Math.floor(statSync(spec.path).size / frameSize), spec.frames);
},
});
// The first window is extracted now so a bad source fails before capture starts.
const first = await windows.lease(0);
if (!first) {
hdrDiagnostics.videoExtractionFailures += 1;
throw new Error(
`HDR frame extraction produced no frames for video "${videoId}". ` +
`Aborting render to avoid shipping black HDR layers.`,
);
}
if (!window.finalFrameOnly) {
ffmpegArgs.push("-r", fpsToFfmpegArg(job.config.fps));
}
ffmpegArgs.push(
"-vf",
`scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
"-pix_fmt",
"rgb48le",
"-f",
"rawvideo",
"-y",
rawPath,
);
const result = await runFfmpegImpl(ffmpegArgs, { signal: abortSignal });
if (!result.success) {
hdrDiagnostics.videoExtractionFailures += 1;
log.error("HDR frame pre-extraction failed; aborting render", {
videoId,
srcPath,
stderr: result.stderr.slice(-400),
});
throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
failureReason: result.failureReason,
});
}
const frameSize = dims.width * dims.height * 6;
const fd = openSync(rawPath, constants.O_RDONLY | NO_FOLLOW_FLAG);
let handedOff = false;
try {
const frameCount = Math.floor(fstatSync(fd).size / frameSize);
if (frameCount < 1) {
hdrDiagnostics.videoExtractionFailures += 1;
throw new Error(
`HDR frame extraction produced no frames for video "${videoId}". ` +
`Aborting render to avoid shipping black HDR layers.`,
);
}
out.set(videoId, {
dir: frameDir,
rawPath,
fd,
width: dims.width,
height: dims.height,
frameSize,
frameCount,
scratch: Buffer.allocUnsafe(frameSize),
loop: video.loop,
mediaStart: extractionMediaStart,
videoStreamLeadSeconds: planned.videoStreamLeadSeconds,
});
handedOff = true;
} finally {
if (!handedOff) closeSync(fd);
}
first.release();
if (video.loop) (await windows.lease(totalFrames - 1))?.release();
out.set(videoId, {
dir: frameDir,
windows,
width: dims.width,
height: dims.height,
frameSize,
get frameCount() {
return windows.totalFrames;
},
scratch: Buffer.allocUnsafe(frameSize),
loop: video.loop,
mediaStart: extractionMediaStart,
videoStreamLeadSeconds: planned.videoStreamLeadSeconds,
});
}
return { sources: out, estimatedBytes, releaseReservation };
} catch (error) {
@@ -547,13 +553,38 @@ export async function extractHdrVideoFrames(args: {
createdFrameDirs.delete(source.dir);
}
for (const frameDir of createdFrameDirs) {
cleanupHdrFrameDirectory(frameDir, undefined, log);
cleanupHdrFrameDirectory(frameDir, log);
}
releaseReservation();
throw error;
}
}
/**
* One window as one FFmpeg pass on the clip's own tick grid: seek to a whole second before the
* window (so timestamp rounding matches the clip's), pin `fps` to the clip's first tick, trim the lead-in.
*/
function hdrWindowFfmpegArgs(a: {
srcPath: string;
mediaStart: number;
fps: number;
fpsArg: string;
firstTick: number;
firstFrame: number;
frames: number;
scale: string;
}): string[] {
const unit = Math.ceil(a.fps);
const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
const trim = a.firstFrame - seekFrame;
const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
if (trim > 0) filters.push(`trim=start_frame=${trim}`);
return [
...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
...["-vf", filters.join(","), "-frames:v", String(a.frames)],
];
}
/**
* Decode each HDR image into an rgb48le buffer, resampling to the element's
* layout box if known. Failures abort the render to avoid shipping missing
@@ -584,7 +615,11 @@ export function decodeHdrImageBuffers(args: {
fit,
fitInfo?.position,
);
out.set(imageId, { data: resampled, width: layout.width, height: layout.height });
out.set(imageId, {
data: resampled,
width: layout.width,
height: layout.height,
});
} else {
out.set(imageId, {
data: Buffer.from(decoded.data),
@@ -298,6 +298,7 @@ export async function runCaptureHdrStage(
height,
abortSignal,
hdrDiagnostics,
maxResidentWindows: Math.max(1, workerCount) + 1,
});
releaseHdrExtractionReservation = extracted.releaseReservation;
for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
+1 -1
View File
@@ -36,7 +36,7 @@
"unpacked": 541601
},
"@hyperframes/producer": {
"packed": 768601,
"packed": 770990,
"unpacked": 2808835
},
"@hyperframes/sdk": {