@hyperframes/producer 0.8.145 → 0.8.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/distributed.js +44 -36
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +214 -68
- package/dist/index.js +71 -61
- package/dist/public-server.js +65 -55
- package/dist/services/htmlCompiler.d.ts +1 -0
- package/dist/services/render/stages/captureStreamingStage.d.ts +3 -1
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -292,7 +292,7 @@ function buildVirtualTimeShim(options) {
|
|
|
292
292
|
const seedRandomFromFrame = options.seedRandomFromFrame === true;
|
|
293
293
|
const seededRandomBlock = seedRandomFromFrame ? String.raw`
|
|
294
294
|
// Seeded Math.random / crypto.getRandomValues, keyed by virtual time.
|
|
295
|
-
// Mulberry32
|
|
295
|
+
// Mulberry32: single uint32 state, deterministic, fast.
|
|
296
296
|
var rngState = 0;
|
|
297
297
|
function mulberry32() {
|
|
298
298
|
rngState |= 0; rngState = (rngState + 0x6D2B79F5) | 0;
|
|
@@ -303,7 +303,7 @@ function buildVirtualTimeShim(options) {
|
|
|
303
303
|
}
|
|
304
304
|
function reseedRngFromTime(ms) {
|
|
305
305
|
var ms32 = Math.max(0, Math.floor(Number(ms) || 0)) | 0;
|
|
306
|
-
// Knuth's multiplicative hash + golden-ratio offset
|
|
306
|
+
// Knuth's multiplicative hash + golden-ratio offset gives a well-
|
|
307
307
|
// distributed seed even for frame 0 (otherwise rngState=0 degenerates
|
|
308
308
|
// the PRNG's first few outputs).
|
|
309
309
|
rngState = (Math.imul(ms32, -1640531527) + 0x9E3779B9) | 0;
|
|
@@ -545,7 +545,7 @@ function buildRenderModeScript(fps) {
|
|
|
545
545
|
};
|
|
546
546
|
window.__playerReady = true;
|
|
547
547
|
// Media-fallback player has no timeline to bind, so render-ready is immediate.
|
|
548
|
-
// init.ts defers __renderReady until the timeline is bound
|
|
548
|
+
// init.ts defers __renderReady until the timeline is bound (a different runtime).
|
|
549
549
|
window.__renderReady = true;
|
|
550
550
|
return true;
|
|
551
551
|
}
|
|
@@ -2136,7 +2136,7 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
|
|
|
2136
2136
|
if (matrix && !isTranslationOnly) {
|
|
2137
2137
|
if (clipped && log) {
|
|
2138
2138
|
log.debug(
|
|
2139
|
-
`HDR clip rect on affine-transformed element ${el.id}
|
|
2139
|
+
`HDR clip rect on affine-transformed element ${el.id}: clip not applied (affine scissor not yet supported)`
|
|
2140
2140
|
);
|
|
2141
2141
|
}
|
|
2142
2142
|
blitRgb48leAffine(
|
|
@@ -2193,7 +2193,7 @@ function blitHdrImageLayer(canvas, el, hdrImageBuffers, hdrImageTransferCache, w
|
|
|
2193
2193
|
return;
|
|
2194
2194
|
}
|
|
2195
2195
|
if (el.clipRect && log) {
|
|
2196
|
-
log.debug(`HDR clip rect on image element ${el.id}
|
|
2196
|
+
log.debug(`HDR clip rect on image element ${el.id}: clip not yet supported for images`);
|
|
2197
2197
|
}
|
|
2198
2198
|
try {
|
|
2199
2199
|
if (hdrPerf) hdrPerf.hdrImageLayerBlits += 1;
|
|
@@ -3379,12 +3379,13 @@ function buildRenderPerfSummary(input) {
|
|
|
3379
3379
|
// src/services/render/stages/captureStreamingStage.ts
|
|
3380
3380
|
var DEFAULT_CAPTURE_STALL_MS = 6e4;
|
|
3381
3381
|
var DE_STALL_POLL_MS = 5e3;
|
|
3382
|
+
var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
|
|
3382
3383
|
var ParallelCaptureStallError = class extends Error {
|
|
3383
3384
|
variant;
|
|
3384
3385
|
constructor(args) {
|
|
3385
3386
|
const phases = args.phaseSummary ? ` Last worker phases: ${args.phaseSummary}.` : "";
|
|
3386
3387
|
super(
|
|
3387
|
-
args.variant === "drain" ? `[Render] Parallel ${args.captureLabel} capture stalled: no frame progress for ${args.stallTimeoutMs}ms (stuck at ${args.lastCapturedFrames}/${args.totalFrames}).${phases}` : `[Render] Parallel ${args.captureLabel} capture stalled after ${args.stallTimeoutMs}ms with no frame progress (last frame ${args.lastCapturedFrames}/${args.totalFrames}); retrying on a fresh screenshot session.${phases}`
|
|
3388
|
+
(args.variant === "drain" ? `[Render] Parallel ${args.captureLabel} capture stalled: no frame progress for ${args.stallTimeoutMs}ms (stuck at ${args.lastCapturedFrames}/${args.totalFrames}).${phases}` : `[Render] Parallel ${args.captureLabel} capture stalled after ${args.stallTimeoutMs}ms with no frame progress (last frame ${args.lastCapturedFrames}/${args.totalFrames}); retrying on a fresh screenshot session.${phases}`) + ` ${STALL_ADVICE}`
|
|
3388
3389
|
);
|
|
3389
3390
|
this.name = "ParallelCaptureStallError";
|
|
3390
3391
|
this.variant = args.variant;
|
|
@@ -3402,12 +3403,13 @@ function captureModeLabel(mode) {
|
|
|
3402
3403
|
}
|
|
3403
3404
|
var SequentialCaptureStallError = class extends Error {
|
|
3404
3405
|
captureMode;
|
|
3405
|
-
constructor(
|
|
3406
|
+
constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
|
|
3407
|
+
const step = session.captureStep ? ` while ${session.captureStep}` : "";
|
|
3406
3408
|
super(
|
|
3407
|
-
`[Render] Sequential ${captureModeLabel(captureMode)} capture stalled: no frame progress for ${stallTimeoutMs}ms (stuck at frame ${frameIndex}/${totalFrames})
|
|
3409
|
+
`[Render] Sequential ${captureModeLabel(session.captureMode)} capture stalled: no frame progress for ${stallTimeoutMs}ms (stuck at frame ${frameIndex}/${totalFrames}${step}). ` + STALL_ADVICE
|
|
3408
3410
|
);
|
|
3409
3411
|
this.name = "SequentialCaptureStallError";
|
|
3410
|
-
this.captureMode = captureMode;
|
|
3412
|
+
this.captureMode = session.captureMode;
|
|
3411
3413
|
}
|
|
3412
3414
|
};
|
|
3413
3415
|
function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
@@ -3416,9 +3418,9 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
|
3416
3418
|
() => {
|
|
3417
3419
|
reject(
|
|
3418
3420
|
signal?.aborted ? new Error(
|
|
3419
|
-
`[Render] Sequential ${captureModeLabel(input.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
|
|
3421
|
+
`[Render] Sequential ${captureModeLabel(input.session.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
|
|
3420
3422
|
) : new SequentialCaptureStallError(
|
|
3421
|
-
input.
|
|
3423
|
+
input.session,
|
|
3422
3424
|
input.stallTimeoutMs,
|
|
3423
3425
|
input.frameIndex,
|
|
3424
3426
|
input.totalFrames
|
|
@@ -3546,7 +3548,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3546
3548
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
3547
3549
|
promise,
|
|
3548
3550
|
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3549
|
-
{
|
|
3551
|
+
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3550
3552
|
abortSignal
|
|
3551
3553
|
);
|
|
3552
3554
|
const drainPrev = async () => {
|
|
@@ -3900,7 +3902,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
3900
3902
|
const { buffer } = await raceAgainstStall(
|
|
3901
3903
|
captureFrameToBuffer(session, i, time),
|
|
3902
3904
|
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3903
|
-
{
|
|
3905
|
+
{ session, frameIndex: i, totalFrames, stallTimeoutMs },
|
|
3904
3906
|
abortSignal
|
|
3905
3907
|
);
|
|
3906
3908
|
await reorderBuffer.waitForFrame(i);
|
|
@@ -3989,7 +3991,7 @@ async function captureSegmentFrames(ctx, segment, encoder) {
|
|
|
3989
3991
|
ctx.captureFrame(ctx.session, i, time),
|
|
3990
3992
|
ctx.stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3991
3993
|
{
|
|
3992
|
-
|
|
3994
|
+
session: ctx.session,
|
|
3993
3995
|
frameIndex: i,
|
|
3994
3996
|
totalFrames: ctx.totalFrames,
|
|
3995
3997
|
stallTimeoutMs: ctx.stallTimeoutMs
|
|
@@ -4385,7 +4387,7 @@ import {
|
|
|
4385
4387
|
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
4386
4388
|
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
4387
4389
|
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
4388
|
-
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}"
|
|
4390
|
+
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
4389
4391
|
}
|
|
4390
4392
|
function findRenderHdrAutoPromotionTrigger(input) {
|
|
4391
4393
|
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
@@ -4398,12 +4400,12 @@ function findRenderHdrAutoPromotionTrigger(input) {
|
|
|
4398
4400
|
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources) {
|
|
4399
4401
|
if (forcedHdrWithoutSources) {
|
|
4400
4402
|
input.log.warn(
|
|
4401
|
-
"[Render] HDR forced by --hdr flag, but no HDR sources were detected
|
|
4403
|
+
"[Render] HDR forced by --hdr flag, but no HDR sources were detected, defaulting to HLG. SDR-only compositions may look perceptually wrong on HDR displays."
|
|
4402
4404
|
);
|
|
4403
4405
|
}
|
|
4404
4406
|
if (!effectiveHdr) {
|
|
4405
4407
|
input.log.info(
|
|
4406
|
-
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected
|
|
4408
|
+
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
4407
4409
|
);
|
|
4408
4410
|
return;
|
|
4409
4411
|
}
|
|
@@ -4418,9 +4420,9 @@ function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources)
|
|
|
4418
4420
|
);
|
|
4419
4421
|
return;
|
|
4420
4422
|
}
|
|
4421
|
-
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected
|
|
4423
|
+
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
4422
4424
|
input.log.info(
|
|
4423
|
-
`[Render] HDR ${reason}
|
|
4425
|
+
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
4424
4426
|
);
|
|
4425
4427
|
}
|
|
4426
4428
|
function resolveEffectiveHdrMode(input) {
|
|
@@ -4516,14 +4518,14 @@ function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled,
|
|
|
4516
4518
|
const reasonCodes = new Set(compiled.renderModeHints.reasons.map((r) => r.code));
|
|
4517
4519
|
if (reasonCodes.has("htmlInCanvas")) {
|
|
4518
4520
|
log.warn(
|
|
4519
|
-
"[Render] html-in-canvas (drawElementImage) detected
|
|
4521
|
+
"[Render] html-in-canvas (drawElementImage) detected, pinning to 1 worker (Chrome concurrency limitation).",
|
|
4520
4522
|
{ requestedWorkers }
|
|
4521
4523
|
);
|
|
4522
4524
|
return 1;
|
|
4523
4525
|
}
|
|
4524
4526
|
if (cfg.lowMemoryMode && requestedWorkers === void 0) {
|
|
4525
4527
|
log.info(
|
|
4526
|
-
"[Render] Low-memory profile
|
|
4528
|
+
"[Render] Low-memory profile, pinning to 1 capture worker (auto-worker calibration skipped)."
|
|
4527
4529
|
);
|
|
4528
4530
|
return 1;
|
|
4529
4531
|
}
|
|
@@ -5158,7 +5160,7 @@ function assertVideoFrameCoverage(reports, threshold) {
|
|
|
5158
5160
|
const thresholdPct = (threshold * 100).toFixed(1);
|
|
5159
5161
|
const suffix = sorted.length > 1 ? ` (+${sorted.length - 1} more clip(s) below threshold)` : "";
|
|
5160
5162
|
throw new VideoFrameCoverageError(
|
|
5161
|
-
`Video "${worst.videoId}" captured ${worst.capturedFrames} of expected ${worst.expectedFrames} frames (coverage ${pct}%, threshold ${thresholdPct}%). check/snapshot may pass while the encoded MP4 renders this clip blank
|
|
5163
|
+
`Video "${worst.videoId}" captured ${worst.capturedFrames} of expected ${worst.expectedFrames} frames (coverage ${pct}%, threshold ${thresholdPct}%). check/snapshot may pass while the encoded MP4 renders this clip blank, so the render is aborted to prevent shipping a wrong MP4.${suffix} Set HF_VIDEO_COVERAGE_THRESHOLD=0 to disable this gate.`,
|
|
5162
5164
|
{ threshold, worst, failedReports: sorted }
|
|
5163
5165
|
);
|
|
5164
5166
|
}
|
|
@@ -6054,7 +6056,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6054
6056
|
if (tagName !== "video") {
|
|
6055
6057
|
if (error instanceof NotMediaPayloadError2) {
|
|
6056
6058
|
log?.warn(
|
|
6057
|
-
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file
|
|
6059
|
+
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
|
|
6058
6060
|
);
|
|
6059
6061
|
}
|
|
6060
6062
|
return { duration: null, resolvedPath: filePath };
|
|
@@ -6143,7 +6145,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log) {
|
|
|
6143
6145
|
if (r.maxDuration != null && shouldClampResolvedMediaDuration(r.tagName, r.duration, r.maxDuration)) {
|
|
6144
6146
|
clampList.push({ id: r.id, duration: r.maxDuration });
|
|
6145
6147
|
log?.warn(
|
|
6146
|
-
`[compile] Audio "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its data-duration is ${r.duration.toFixed(2)}s
|
|
6148
|
+
`[compile] Audio "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its data-duration is ${r.duration.toFixed(2)}s, so the slot is shortened to the media length. Set data-duration to ~${r.maxDuration.toFixed(2)}s, trim data-media-start, or use a longer/looping source if that isn't intended.`
|
|
6147
6149
|
);
|
|
6148
6150
|
}
|
|
6149
6151
|
}
|
|
@@ -6532,7 +6534,7 @@ ${safeText}
|
|
|
6532
6534
|
} else {
|
|
6533
6535
|
if (download.reason instanceof ScriptIntegrityError) throw download.reason;
|
|
6534
6536
|
defaultLogger.warn(
|
|
6535
|
-
`[Compiler] WARNING: Failed to download CDN script: ${src}
|
|
6537
|
+
`[Compiler] WARNING: Failed to download CDN script: ${src} (${download.reason}). The render may fail if this script is required (e.g. GSAP). Consider bundling it locally in your project.`
|
|
6536
6538
|
);
|
|
6537
6539
|
}
|
|
6538
6540
|
}
|
|
@@ -6584,7 +6586,7 @@ function collectExternalAssets(html, projectDir) {
|
|
|
6584
6586
|
}
|
|
6585
6587
|
if (externalAssets.size > 0) {
|
|
6586
6588
|
defaultLogger.info(
|
|
6587
|
-
`[Compiler] Found ${externalAssets.size} asset(s) outside project directory
|
|
6589
|
+
`[Compiler] Found ${externalAssets.size} asset(s) outside project directory, will copy to render output`
|
|
6588
6590
|
);
|
|
6589
6591
|
}
|
|
6590
6592
|
return {
|
|
@@ -6609,7 +6611,7 @@ async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLab
|
|
|
6609
6611
|
urlToLocal.set(url, localPath);
|
|
6610
6612
|
} catch (err) {
|
|
6611
6613
|
const identity = safeDownloadUrlIdentity(url);
|
|
6612
|
-
defaultLogger.warn(`[Compiler] ${warnLabel}
|
|
6614
|
+
defaultLogger.warn(`[Compiler] ${warnLabel}, using original URL as fallback.`, {
|
|
6613
6615
|
urlFingerprint: identity.urlFingerprint,
|
|
6614
6616
|
host: identity.host,
|
|
6615
6617
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -6702,7 +6704,7 @@ async function fetchExternalStylesheetCss(href) {
|
|
|
6702
6704
|
timeoutMs: 15e3
|
|
6703
6705
|
});
|
|
6704
6706
|
} catch (err) {
|
|
6705
|
-
defaultLogger.warn("[Compiler] External stylesheet fetch failed
|
|
6707
|
+
defaultLogger.warn("[Compiler] External stylesheet fetch failed, preserving link tag.", {
|
|
6706
6708
|
urlFingerprint: identity.urlFingerprint,
|
|
6707
6709
|
host: identity.host,
|
|
6708
6710
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -7346,12 +7348,15 @@ async function resolveCompositionDurations(page, unresolved) {
|
|
|
7346
7348
|
const resolved = [];
|
|
7347
7349
|
for (const id of compIds) {
|
|
7348
7350
|
const tl = timelines[id];
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7351
|
+
let dur = 0;
|
|
7352
|
+
try {
|
|
7353
|
+
dur = typeof tl?.duration === "function" ? Number(tl.duration()) : 0;
|
|
7354
|
+
} catch {
|
|
7355
|
+
dur = 0;
|
|
7356
|
+
}
|
|
7357
|
+
if (dur > 0) {
|
|
7358
|
+
resolved.push({ id, duration: dur, source: "__timelines" });
|
|
7359
|
+
continue;
|
|
7355
7360
|
}
|
|
7356
7361
|
const el = document.getElementById(id);
|
|
7357
7362
|
if (el) {
|
|
@@ -7447,28 +7452,28 @@ async function runCompileStage(input) {
|
|
|
7447
7452
|
cfg.useDrawElement = false;
|
|
7448
7453
|
deCompileGate = "3d";
|
|
7449
7454
|
log.info(
|
|
7450
|
-
"[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility)
|
|
7455
|
+
"[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7451
7456
|
);
|
|
7452
7457
|
}
|
|
7453
7458
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
|
|
7454
7459
|
cfg.useDrawElement = false;
|
|
7455
7460
|
deCompileGate = "mix_blend_mode";
|
|
7456
7461
|
log.info(
|
|
7457
|
-
"[Render] Fast capture: composition uses mix-blend-mode
|
|
7462
|
+
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7458
7463
|
);
|
|
7459
7464
|
}
|
|
7460
7465
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
|
|
7461
7466
|
cfg.useDrawElement = false;
|
|
7462
7467
|
deCompileGate = "ancestor_background_image";
|
|
7463
7468
|
log.info(
|
|
7464
|
-
"[Render] Fast capture: composition root's ancestors (body/html/wrapper) carry a background-image
|
|
7469
|
+
"[Render] Fast capture: composition root's ancestors (body/html/wrapper) carry a background-image, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7465
7470
|
);
|
|
7466
7471
|
}
|
|
7467
7472
|
if (cfg.useDrawElement && process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE !== "true" && compiled.hasShaderTransitions) {
|
|
7468
7473
|
cfg.useDrawElement = false;
|
|
7469
7474
|
deCompileGate = "shader_transitions";
|
|
7470
7475
|
log.info(
|
|
7471
|
-
"[Render] Fast capture: composition uses shader transitions
|
|
7476
|
+
"[Render] Fast capture: composition uses shader transitions, so disabling drawElementImage so page-side compositing stays available."
|
|
7472
7477
|
);
|
|
7473
7478
|
}
|
|
7474
7479
|
if (!cfg.useDrawElement && cfg.pageSideCompositingAutoDisabled) {
|
|
@@ -7796,7 +7801,7 @@ async function runProbeStage(input) {
|
|
|
7796
7801
|
} else {
|
|
7797
7802
|
beginFrameStalled = true;
|
|
7798
7803
|
log.warn(
|
|
7799
|
-
"[Render] BeginFrame liveness probe timed out
|
|
7804
|
+
"[Render] BeginFrame liveness probe timed out: this composition stalls BeginFrame on this host (SwiftShader heavy-layer pattern). Relaunching the probe browser in screenshot capture mode; the render will use screenshot capture throughout.",
|
|
7800
7805
|
{ probeTimeoutMs }
|
|
7801
7806
|
);
|
|
7802
7807
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
@@ -8010,7 +8015,7 @@ async function runProbeStage(input) {
|
|
|
8010
8015
|
video.start = win.visibleStart;
|
|
8011
8016
|
video.end = win.visibleEnd;
|
|
8012
8017
|
log.info(
|
|
8013
|
-
`[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s
|
|
8018
|
+
`[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s`
|
|
8014
8019
|
);
|
|
8015
8020
|
}
|
|
8016
8021
|
}
|
|
@@ -8058,7 +8063,7 @@ async function runProbeStage(input) {
|
|
|
8058
8063
|
});
|
|
8059
8064
|
if (!timelinesInfo.gsapLoaded) {
|
|
8060
8065
|
diagnostics.push(
|
|
8061
|
-
"GSAP is not loaded
|
|
8066
|
+
"GSAP is not loaded. The CDN script may have failed to download. Bundle GSAP locally in your project instead of using a CDN <script src>."
|
|
8062
8067
|
);
|
|
8063
8068
|
} else if (timelinesInfo.timelineKeys.length === 0) {
|
|
8064
8069
|
diagnostics.push(
|
|
@@ -8075,10 +8080,10 @@ async function runProbeStage(input) {
|
|
|
8075
8080
|
log.warn("Failed to gather browser diagnostics for zero-duration composition", {
|
|
8076
8081
|
error: err instanceof Error ? err.message : String(err)
|
|
8077
8082
|
});
|
|
8078
|
-
diagnostics.push("(Could not gather browser diagnostics
|
|
8083
|
+
diagnostics.push("(Could not gather browser diagnostics. The page may have crashed.)");
|
|
8079
8084
|
}
|
|
8080
8085
|
const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
|
|
8081
|
-
throw new Error("Composition duration is 0
|
|
8086
|
+
throw new Error("Composition duration is 0, so this would produce a black video." + hint);
|
|
8082
8087
|
}
|
|
8083
8088
|
if (probeSession) {
|
|
8084
8089
|
const failedRequests = probeSession.browserConsoleBuffer.filter(isActionableProbeFailure);
|
|
@@ -8159,6 +8164,7 @@ import {
|
|
|
8159
8164
|
detectTransfer,
|
|
8160
8165
|
extractAllVideoFrames,
|
|
8161
8166
|
extractMediaMetadata as extractMediaMetadata3,
|
|
8167
|
+
isVideoPastTimelineEnd,
|
|
8162
8168
|
isHdrColorSpace as isHdrColorSpace2,
|
|
8163
8169
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
8164
8170
|
runVideoExtractionWithRetry,
|
|
@@ -8373,6 +8379,9 @@ async function runExtractVideosStage(input) {
|
|
|
8373
8379
|
} = input;
|
|
8374
8380
|
const stage2Start = Date.now();
|
|
8375
8381
|
const extractionPolicy = resolveVideoExtractionPolicy();
|
|
8382
|
+
composition.videos = composition.videos.filter(
|
|
8383
|
+
(video) => !isVideoPastTimelineEnd(video, composition.duration)
|
|
8384
|
+
);
|
|
8376
8385
|
let frameLookup = null;
|
|
8377
8386
|
let extractionResult = null;
|
|
8378
8387
|
let failureToEnforce = null;
|
|
@@ -8459,7 +8468,8 @@ async function runExtractVideosStage(input) {
|
|
|
8459
8468
|
timelineEnd: composition.duration,
|
|
8460
8469
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
8461
8470
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
8462
|
-
deferRangeExtraction
|
|
8471
|
+
deferRangeExtraction,
|
|
8472
|
+
contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)]
|
|
8463
8473
|
},
|
|
8464
8474
|
abortSignal,
|
|
8465
8475
|
cfg,
|
|
@@ -11085,7 +11095,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11085
11095
|
const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
|
|
11086
11096
|
if (!madeProgress) {
|
|
11087
11097
|
options.log.warn(
|
|
11088
|
-
"[Render] Capture attempt made no forward progress; composition is likely structurally broken
|
|
11098
|
+
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
|
11089
11099
|
{ attempt, frameCount, remainingCount, workers: currentWorkers }
|
|
11090
11100
|
);
|
|
11091
11101
|
}
|
|
@@ -11154,7 +11164,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11154
11164
|
}
|
|
11155
11165
|
if (!madeProgress) {
|
|
11156
11166
|
options.log.warn(
|
|
11157
|
-
"[Render] Capture attempt made no forward progress; composition is likely structurally broken
|
|
11167
|
+
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
|
11158
11168
|
{ attempt, frameCount, remainingCount, workers: currentWorkers }
|
|
11159
11169
|
);
|
|
11160
11170
|
}
|
|
@@ -11617,7 +11627,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
11617
11627
|
execution.defer("remove workDir", () => {
|
|
11618
11628
|
if (job.config.debug) return;
|
|
11619
11629
|
if (job.status === "complete" && process.env.KEEP_TEMP === "1") {
|
|
11620
|
-
log.info("KEEP_TEMP=1
|
|
11630
|
+
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
11621
11631
|
return;
|
|
11622
11632
|
}
|
|
11623
11633
|
rmSync9(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
@@ -11854,7 +11864,7 @@ async function executeRenderPipeline(input) {
|
|
|
11854
11864
|
captureForceScreenshot = true;
|
|
11855
11865
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
11856
11866
|
log.info(
|
|
11857
|
-
"[Render] Low-memory render profile active
|
|
11867
|
+
"[Render] Low-memory render profile active: screenshot capture, auto-worker calibration skipped" + (job.config.workers === void 0 ? ", pinned to 1 worker" : "") + ". Override with --no-low-memory-mode or PRODUCER_LOW_MEMORY_MODE=false.",
|
|
11858
11868
|
{ totalMemMb: getSystemTotalMb(), thresholdMb: LOW_MEMORY_TOTAL_MB_THRESHOLD }
|
|
11859
11869
|
);
|
|
11860
11870
|
}
|
|
@@ -12422,7 +12432,7 @@ async function executeRenderPipeline(input) {
|
|
|
12422
12432
|
cfg.useDrawElement = false;
|
|
12423
12433
|
deClampReason = workerCount > 1 ? "parallel" : "disk_path";
|
|
12424
12434
|
log.info(
|
|
12425
|
-
"[Render] Fast capture: default-on drawElement disabled for this render
|
|
12435
|
+
"[Render] Fast capture: default-on drawElement disabled for this render: " + (workerCount > 1 ? "parallel capture" : "the disk capture path") + " has no runtime self-verification. Set PRODUCER_EXPERIMENTAL_FAST_CAPTURE=true to override."
|
|
12426
12436
|
);
|
|
12427
12437
|
if (probeSession && probeSession.captureMode === "drawelement") {
|
|
12428
12438
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
@@ -12518,7 +12528,7 @@ async function executeRenderPipeline(input) {
|
|
|
12518
12528
|
});
|
|
12519
12529
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
12520
12530
|
log.info(
|
|
12521
|
-
`[Render] Page-side compositing enabled
|
|
12531
|
+
`[Render] Page-side compositing enabled, bypassing Node-side layered shader-blend path. Engine will capture one ${needsAlpha ? "RGBA PNG" : "opaque RGB"} screenshot per output frame.`
|
|
12522
12532
|
);
|
|
12523
12533
|
}
|
|
12524
12534
|
const useLayeredComposite = !usePageSideCompositingForTransitions && shouldUseLayeredComposite({
|
|
@@ -13015,7 +13025,7 @@ async function executeRenderPipeline(input) {
|
|
|
13015
13025
|
cfg.useDrawElement = false;
|
|
13016
13026
|
deClampReason = deClampReason ?? "disk_path";
|
|
13017
13027
|
log.info(
|
|
13018
|
-
"[Render] Fast capture: drawElement disabled for the disk fallback
|
|
13028
|
+
"[Render] Fast capture: drawElement disabled for the disk fallback: streaming encoder spawn failed and the disk path has no runtime self-verification."
|
|
13019
13029
|
);
|
|
13020
13030
|
if (probeSession && probeSession.captureMode === "drawelement") {
|
|
13021
13031
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
@@ -13633,10 +13643,10 @@ function validateJsonSafeValue(value, field) {
|
|
|
13633
13643
|
walkVariables(value, field, /* @__PURE__ */ new WeakSet());
|
|
13634
13644
|
}
|
|
13635
13645
|
var LEAF_REJECTIONS = {
|
|
13636
|
-
undefined: "undefined leaves are silently dropped by JSON.stringify
|
|
13646
|
+
undefined: "undefined leaves are silently dropped by JSON.stringify. Use null if you mean an absent value",
|
|
13637
13647
|
function: "functions are not JSON-serializable",
|
|
13638
13648
|
symbol: "Symbols are not JSON-serializable",
|
|
13639
|
-
bigint: "BigInt values throw at JSON.stringify
|
|
13649
|
+
bigint: "BigInt values throw at JSON.stringify. Encode as a string if you need 64-bit integers"
|
|
13640
13650
|
};
|
|
13641
13651
|
function walkVariables(value, path, seen) {
|
|
13642
13652
|
const t = typeof value;
|
|
@@ -13657,7 +13667,7 @@ function walkVariables(value, path, seen) {
|
|
|
13657
13667
|
if (seen.has(value)) {
|
|
13658
13668
|
throw new InvalidConfigError(
|
|
13659
13669
|
path,
|
|
13660
|
-
"circular reference detected
|
|
13670
|
+
"circular reference detected: JSON.stringify cannot serialize cycles"
|
|
13661
13671
|
);
|
|
13662
13672
|
}
|
|
13663
13673
|
seen.add(value);
|
|
@@ -13997,7 +14007,7 @@ function validateOutputResolutionOverride(body) {
|
|
|
13997
14007
|
return `Invalid outputResolution "${body.outputResolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: 1080p, 4k, \u2026).`;
|
|
13998
14008
|
}
|
|
13999
14009
|
if (normalized !== void 0 && (body.format === "webm" || body.format === "mov")) {
|
|
14000
|
-
return `outputResolution is not supported with format "${body.format}"
|
|
14010
|
+
return `outputResolution is not supported with format "${body.format}": the alpha (webm/mov) capture path can't supersample. Use format "mp4", or omit outputResolution to render at the composition's native dimensions.`;
|
|
14001
14011
|
}
|
|
14002
14012
|
return void 0;
|
|
14003
14013
|
}
|
|
@@ -67,6 +67,7 @@ export declare function collectExternalAssets(html: string, projectDir: string):
|
|
|
67
67
|
html: string;
|
|
68
68
|
externalAssets: Map<string, string>;
|
|
69
69
|
};
|
|
70
|
+
export declare const REMOTE_MEDIA_SUBDIR = "_remote_media";
|
|
70
71
|
/**
|
|
71
72
|
* Download any remote `src` URLs on `<video>` / `<audio>` elements and their
|
|
72
73
|
* `<source>` children into a local subdirectory of `downloadDir`, rewrite the
|
|
@@ -44,8 +44,9 @@ import type { ProducerLogger } from "../../../logger.js";
|
|
|
44
44
|
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
|
|
45
45
|
import type { SdrStreamingCapturePlan } from "../capturePlan.js";
|
|
46
46
|
export declare function resolveCaptureStallTimeoutMs(): number;
|
|
47
|
+
type StalledSession = Pick<CaptureSession, "captureMode" | "captureStep">;
|
|
47
48
|
export declare function raceAgainstStall<T>(promise: Promise<T>, deadlineMs: number, input: {
|
|
48
|
-
|
|
49
|
+
session: StalledSession;
|
|
49
50
|
frameIndex: number;
|
|
50
51
|
totalFrames: number;
|
|
51
52
|
stallTimeoutMs: number;
|
|
@@ -123,3 +124,4 @@ export type CaptureStreamingStageResult = {
|
|
|
123
124
|
success: false;
|
|
124
125
|
};
|
|
125
126
|
export declare function runCaptureStreamingStage(input: CaptureStreamingStageInput): Promise<CaptureStreamingStageResult>;
|
|
127
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.147",
|
|
4
4
|
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"postcss": "^8.4.0",
|
|
36
36
|
"puppeteer": "^25.2.1",
|
|
37
37
|
"puppeteer-core": "^25.2.1",
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/lint": "0.8.
|
|
41
|
-
"@hyperframes/parsers": "0.8.
|
|
42
|
-
"@hyperframes/studio-server": "0.8.
|
|
38
|
+
"@hyperframes/engine": "0.8.147",
|
|
39
|
+
"@hyperframes/core": "0.8.147",
|
|
40
|
+
"@hyperframes/lint": "0.8.147",
|
|
41
|
+
"@hyperframes/parsers": "0.8.147",
|
|
42
|
+
"@hyperframes/studio-server": "0.8.147"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^25.0.10",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"tsx": "^4.21.0",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^4.1.11",
|
|
51
|
-
"@hyperframes/aws-lambda": "0.8.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.8.147"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|