@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/index.js
CHANGED
|
@@ -272,7 +272,7 @@ function buildVirtualTimeShim(options) {
|
|
|
272
272
|
const seedRandomFromFrame = options.seedRandomFromFrame === true;
|
|
273
273
|
const seededRandomBlock = seedRandomFromFrame ? String.raw`
|
|
274
274
|
// Seeded Math.random / crypto.getRandomValues, keyed by virtual time.
|
|
275
|
-
// Mulberry32
|
|
275
|
+
// Mulberry32: single uint32 state, deterministic, fast.
|
|
276
276
|
var rngState = 0;
|
|
277
277
|
function mulberry32() {
|
|
278
278
|
rngState |= 0; rngState = (rngState + 0x6D2B79F5) | 0;
|
|
@@ -283,7 +283,7 @@ function buildVirtualTimeShim(options) {
|
|
|
283
283
|
}
|
|
284
284
|
function reseedRngFromTime(ms) {
|
|
285
285
|
var ms32 = Math.max(0, Math.floor(Number(ms) || 0)) | 0;
|
|
286
|
-
// Knuth's multiplicative hash + golden-ratio offset
|
|
286
|
+
// Knuth's multiplicative hash + golden-ratio offset gives a well-
|
|
287
287
|
// distributed seed even for frame 0 (otherwise rngState=0 degenerates
|
|
288
288
|
// the PRNG's first few outputs).
|
|
289
289
|
rngState = (Math.imul(ms32, -1640531527) + 0x9E3779B9) | 0;
|
|
@@ -525,7 +525,7 @@ function buildRenderModeScript(fps) {
|
|
|
525
525
|
};
|
|
526
526
|
window.__playerReady = true;
|
|
527
527
|
// Media-fallback player has no timeline to bind, so render-ready is immediate.
|
|
528
|
-
// init.ts defers __renderReady until the timeline is bound
|
|
528
|
+
// init.ts defers __renderReady until the timeline is bound (a different runtime).
|
|
529
529
|
window.__renderReady = true;
|
|
530
530
|
return true;
|
|
531
531
|
}
|
|
@@ -2117,7 +2117,7 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
|
|
|
2117
2117
|
if (matrix && !isTranslationOnly) {
|
|
2118
2118
|
if (clipped && log) {
|
|
2119
2119
|
log.debug(
|
|
2120
|
-
`HDR clip rect on affine-transformed element ${el.id}
|
|
2120
|
+
`HDR clip rect on affine-transformed element ${el.id}: clip not applied (affine scissor not yet supported)`
|
|
2121
2121
|
);
|
|
2122
2122
|
}
|
|
2123
2123
|
blitRgb48leAffine(
|
|
@@ -2174,7 +2174,7 @@ function blitHdrImageLayer(canvas, el, hdrImageBuffers, hdrImageTransferCache, w
|
|
|
2174
2174
|
return;
|
|
2175
2175
|
}
|
|
2176
2176
|
if (el.clipRect && log) {
|
|
2177
|
-
log.debug(`HDR clip rect on image element ${el.id}
|
|
2177
|
+
log.debug(`HDR clip rect on image element ${el.id}: clip not yet supported for images`);
|
|
2178
2178
|
}
|
|
2179
2179
|
try {
|
|
2180
2180
|
if (hdrPerf) hdrPerf.hdrImageLayerBlits += 1;
|
|
@@ -3360,12 +3360,13 @@ function buildRenderPerfSummary(input) {
|
|
|
3360
3360
|
// src/services/render/stages/captureStreamingStage.ts
|
|
3361
3361
|
var DEFAULT_CAPTURE_STALL_MS = 6e4;
|
|
3362
3362
|
var DE_STALL_POLL_MS = 5e3;
|
|
3363
|
+
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.";
|
|
3363
3364
|
var ParallelCaptureStallError = class extends Error {
|
|
3364
3365
|
variant;
|
|
3365
3366
|
constructor(args) {
|
|
3366
3367
|
const phases = args.phaseSummary ? ` Last worker phases: ${args.phaseSummary}.` : "";
|
|
3367
3368
|
super(
|
|
3368
|
-
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}`
|
|
3369
|
+
(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}`
|
|
3369
3370
|
);
|
|
3370
3371
|
this.name = "ParallelCaptureStallError";
|
|
3371
3372
|
this.variant = args.variant;
|
|
@@ -3383,12 +3384,13 @@ function captureModeLabel(mode) {
|
|
|
3383
3384
|
}
|
|
3384
3385
|
var SequentialCaptureStallError = class extends Error {
|
|
3385
3386
|
captureMode;
|
|
3386
|
-
constructor(
|
|
3387
|
+
constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
|
|
3388
|
+
const step = session.captureStep ? ` while ${session.captureStep}` : "";
|
|
3387
3389
|
super(
|
|
3388
|
-
`[Render] Sequential ${captureModeLabel(captureMode)} capture stalled: no frame progress for ${stallTimeoutMs}ms (stuck at frame ${frameIndex}/${totalFrames})
|
|
3390
|
+
`[Render] Sequential ${captureModeLabel(session.captureMode)} capture stalled: no frame progress for ${stallTimeoutMs}ms (stuck at frame ${frameIndex}/${totalFrames}${step}). ` + STALL_ADVICE
|
|
3389
3391
|
);
|
|
3390
3392
|
this.name = "SequentialCaptureStallError";
|
|
3391
|
-
this.captureMode = captureMode;
|
|
3393
|
+
this.captureMode = session.captureMode;
|
|
3392
3394
|
}
|
|
3393
3395
|
};
|
|
3394
3396
|
function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
@@ -3397,9 +3399,9 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
|
3397
3399
|
() => {
|
|
3398
3400
|
reject(
|
|
3399
3401
|
signal?.aborted ? new Error(
|
|
3400
|
-
`[Render] Sequential ${captureModeLabel(input.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
|
|
3402
|
+
`[Render] Sequential ${captureModeLabel(input.session.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
|
|
3401
3403
|
) : new SequentialCaptureStallError(
|
|
3402
|
-
input.
|
|
3404
|
+
input.session,
|
|
3403
3405
|
input.stallTimeoutMs,
|
|
3404
3406
|
input.frameIndex,
|
|
3405
3407
|
input.totalFrames
|
|
@@ -3527,7 +3529,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3527
3529
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
3528
3530
|
promise,
|
|
3529
3531
|
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3530
|
-
{
|
|
3532
|
+
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3531
3533
|
abortSignal
|
|
3532
3534
|
);
|
|
3533
3535
|
const drainPrev = async () => {
|
|
@@ -3881,7 +3883,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
3881
3883
|
const { buffer } = await raceAgainstStall(
|
|
3882
3884
|
captureFrameToBuffer(session, i, time),
|
|
3883
3885
|
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3884
|
-
{
|
|
3886
|
+
{ session, frameIndex: i, totalFrames, stallTimeoutMs },
|
|
3885
3887
|
abortSignal
|
|
3886
3888
|
);
|
|
3887
3889
|
await reorderBuffer.waitForFrame(i);
|
|
@@ -3970,7 +3972,7 @@ async function captureSegmentFrames(ctx, segment, encoder) {
|
|
|
3970
3972
|
ctx.captureFrame(ctx.session, i, time),
|
|
3971
3973
|
ctx.stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3972
3974
|
{
|
|
3973
|
-
|
|
3975
|
+
session: ctx.session,
|
|
3974
3976
|
frameIndex: i,
|
|
3975
3977
|
totalFrames: ctx.totalFrames,
|
|
3976
3978
|
stallTimeoutMs: ctx.stallTimeoutMs
|
|
@@ -4366,7 +4368,7 @@ import {
|
|
|
4366
4368
|
function formatDowngradeWarning(outputFormat, forcedHdrWithoutSources) {
|
|
4367
4369
|
const hdrSourceReason = forcedHdrWithoutSources ? "HDR was forced without detected HDR sources" : "HDR source detected";
|
|
4368
4370
|
const formatReason = outputFormat === "hls" ? "HLS output is SDR-only (H.264 in MPEG-TS)" : "HDR + alpha is not supported";
|
|
4369
|
-
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}"
|
|
4371
|
+
return `[Render] ${hdrSourceReason}, but format is "${outputFormat}", so falling back to SDR. ${formatReason}. Use --format mp4 for HDR10 output.`;
|
|
4370
4372
|
}
|
|
4371
4373
|
function findRenderHdrAutoPromotionTrigger(input) {
|
|
4372
4374
|
for (const extracted of input.extractionResult?.extracted ?? []) {
|
|
@@ -4379,12 +4381,12 @@ function findRenderHdrAutoPromotionTrigger(input) {
|
|
|
4379
4381
|
function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources) {
|
|
4380
4382
|
if (forcedHdrWithoutSources) {
|
|
4381
4383
|
input.log.warn(
|
|
4382
|
-
"[Render] HDR forced by --hdr flag, but no HDR sources were detected
|
|
4384
|
+
"[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."
|
|
4383
4385
|
);
|
|
4384
4386
|
}
|
|
4385
4387
|
if (!effectiveHdr) {
|
|
4386
4388
|
input.log.info(
|
|
4387
|
-
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected
|
|
4389
|
+
hdrMode === "force-sdr" ? "[Render] SDR forced by --sdr flag" : "[Render] No HDR sources detected, rendering SDR"
|
|
4388
4390
|
);
|
|
4389
4391
|
return;
|
|
4390
4392
|
}
|
|
@@ -4399,9 +4401,9 @@ function logHdrResolution(input, hdrMode, effectiveHdr, forcedHdrWithoutSources)
|
|
|
4399
4401
|
);
|
|
4400
4402
|
return;
|
|
4401
4403
|
}
|
|
4402
|
-
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected
|
|
4404
|
+
const reason = forcedHdrWithoutSources ? "forced by --hdr flag (no HDR sources detected, defaulting to HLG)" : "forced by --hdr flag";
|
|
4403
4405
|
input.log.info(
|
|
4404
|
-
`[Render] HDR ${reason}
|
|
4406
|
+
`[Render] HDR ${reason}, output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
4405
4407
|
);
|
|
4406
4408
|
}
|
|
4407
4409
|
function resolveEffectiveHdrMode(input) {
|
|
@@ -4497,14 +4499,14 @@ function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled,
|
|
|
4497
4499
|
const reasonCodes = new Set(compiled.renderModeHints.reasons.map((r) => r.code));
|
|
4498
4500
|
if (reasonCodes.has("htmlInCanvas")) {
|
|
4499
4501
|
log.warn(
|
|
4500
|
-
"[Render] html-in-canvas (drawElementImage) detected
|
|
4502
|
+
"[Render] html-in-canvas (drawElementImage) detected, pinning to 1 worker (Chrome concurrency limitation).",
|
|
4501
4503
|
{ requestedWorkers }
|
|
4502
4504
|
);
|
|
4503
4505
|
return 1;
|
|
4504
4506
|
}
|
|
4505
4507
|
if (cfg.lowMemoryMode && requestedWorkers === void 0) {
|
|
4506
4508
|
log.info(
|
|
4507
|
-
"[Render] Low-memory profile
|
|
4509
|
+
"[Render] Low-memory profile, pinning to 1 capture worker (auto-worker calibration skipped)."
|
|
4508
4510
|
);
|
|
4509
4511
|
return 1;
|
|
4510
4512
|
}
|
|
@@ -5139,7 +5141,7 @@ function assertVideoFrameCoverage(reports, threshold) {
|
|
|
5139
5141
|
const thresholdPct = (threshold * 100).toFixed(1);
|
|
5140
5142
|
const suffix = sorted.length > 1 ? ` (+${sorted.length - 1} more clip(s) below threshold)` : "";
|
|
5141
5143
|
throw new VideoFrameCoverageError(
|
|
5142
|
-
`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
|
|
5144
|
+
`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.`,
|
|
5143
5145
|
{ threshold, worst, failedReports: sorted }
|
|
5144
5146
|
);
|
|
5145
5147
|
}
|
|
@@ -6035,7 +6037,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6035
6037
|
if (tagName !== "video") {
|
|
6036
6038
|
if (error instanceof NotMediaPayloadError2) {
|
|
6037
6039
|
log?.warn(
|
|
6038
|
-
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file
|
|
6040
|
+
`[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.`
|
|
6039
6041
|
);
|
|
6040
6042
|
}
|
|
6041
6043
|
return { duration: null, resolvedPath: filePath };
|
|
@@ -6124,7 +6126,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log) {
|
|
|
6124
6126
|
if (r.maxDuration != null && shouldClampResolvedMediaDuration(r.tagName, r.duration, r.maxDuration)) {
|
|
6125
6127
|
clampList.push({ id: r.id, duration: r.maxDuration });
|
|
6126
6128
|
log?.warn(
|
|
6127
|
-
`[compile] Audio "${r.id}" (${r.src}) is ${r.maxDuration.toFixed(2)}s but its data-duration is ${r.duration.toFixed(2)}s
|
|
6129
|
+
`[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.`
|
|
6128
6130
|
);
|
|
6129
6131
|
}
|
|
6130
6132
|
}
|
|
@@ -6513,7 +6515,7 @@ ${safeText}
|
|
|
6513
6515
|
} else {
|
|
6514
6516
|
if (download.reason instanceof ScriptIntegrityError) throw download.reason;
|
|
6515
6517
|
defaultLogger.warn(
|
|
6516
|
-
`[Compiler] WARNING: Failed to download CDN script: ${src}
|
|
6518
|
+
`[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.`
|
|
6517
6519
|
);
|
|
6518
6520
|
}
|
|
6519
6521
|
}
|
|
@@ -6565,7 +6567,7 @@ function collectExternalAssets(html, projectDir) {
|
|
|
6565
6567
|
}
|
|
6566
6568
|
if (externalAssets.size > 0) {
|
|
6567
6569
|
defaultLogger.info(
|
|
6568
|
-
`[Compiler] Found ${externalAssets.size} asset(s) outside project directory
|
|
6570
|
+
`[Compiler] Found ${externalAssets.size} asset(s) outside project directory, will copy to render output`
|
|
6569
6571
|
);
|
|
6570
6572
|
}
|
|
6571
6573
|
return {
|
|
@@ -6590,7 +6592,7 @@ async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLab
|
|
|
6590
6592
|
urlToLocal.set(url, localPath);
|
|
6591
6593
|
} catch (err) {
|
|
6592
6594
|
const identity = safeDownloadUrlIdentity(url);
|
|
6593
|
-
defaultLogger.warn(`[Compiler] ${warnLabel}
|
|
6595
|
+
defaultLogger.warn(`[Compiler] ${warnLabel}, using original URL as fallback.`, {
|
|
6594
6596
|
urlFingerprint: identity.urlFingerprint,
|
|
6595
6597
|
host: identity.host,
|
|
6596
6598
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -6683,7 +6685,7 @@ async function fetchExternalStylesheetCss(href) {
|
|
|
6683
6685
|
timeoutMs: 15e3
|
|
6684
6686
|
});
|
|
6685
6687
|
} catch (err) {
|
|
6686
|
-
defaultLogger.warn("[Compiler] External stylesheet fetch failed
|
|
6688
|
+
defaultLogger.warn("[Compiler] External stylesheet fetch failed, preserving link tag.", {
|
|
6687
6689
|
urlFingerprint: identity.urlFingerprint,
|
|
6688
6690
|
host: identity.host,
|
|
6689
6691
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -7327,12 +7329,15 @@ async function resolveCompositionDurations(page, unresolved) {
|
|
|
7327
7329
|
const resolved = [];
|
|
7328
7330
|
for (const id of compIds) {
|
|
7329
7331
|
const tl = timelines[id];
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7332
|
+
let dur = 0;
|
|
7333
|
+
try {
|
|
7334
|
+
dur = typeof tl?.duration === "function" ? Number(tl.duration()) : 0;
|
|
7335
|
+
} catch {
|
|
7336
|
+
dur = 0;
|
|
7337
|
+
}
|
|
7338
|
+
if (dur > 0) {
|
|
7339
|
+
resolved.push({ id, duration: dur, source: "__timelines" });
|
|
7340
|
+
continue;
|
|
7336
7341
|
}
|
|
7337
7342
|
const el = document.getElementById(id);
|
|
7338
7343
|
if (el) {
|
|
@@ -7428,28 +7433,28 @@ async function runCompileStage(input) {
|
|
|
7428
7433
|
cfg.useDrawElement = false;
|
|
7429
7434
|
deCompileGate = "3d";
|
|
7430
7435
|
log.info(
|
|
7431
|
-
"[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility)
|
|
7436
|
+
"[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."
|
|
7432
7437
|
);
|
|
7433
7438
|
}
|
|
7434
7439
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
|
|
7435
7440
|
cfg.useDrawElement = false;
|
|
7436
7441
|
deCompileGate = "mix_blend_mode";
|
|
7437
7442
|
log.info(
|
|
7438
|
-
"[Render] Fast capture: composition uses mix-blend-mode
|
|
7443
|
+
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7439
7444
|
);
|
|
7440
7445
|
}
|
|
7441
7446
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
|
|
7442
7447
|
cfg.useDrawElement = false;
|
|
7443
7448
|
deCompileGate = "ancestor_background_image";
|
|
7444
7449
|
log.info(
|
|
7445
|
-
"[Render] Fast capture: composition root's ancestors (body/html/wrapper) carry a background-image
|
|
7450
|
+
"[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."
|
|
7446
7451
|
);
|
|
7447
7452
|
}
|
|
7448
7453
|
if (cfg.useDrawElement && process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE !== "true" && compiled.hasShaderTransitions) {
|
|
7449
7454
|
cfg.useDrawElement = false;
|
|
7450
7455
|
deCompileGate = "shader_transitions";
|
|
7451
7456
|
log.info(
|
|
7452
|
-
"[Render] Fast capture: composition uses shader transitions
|
|
7457
|
+
"[Render] Fast capture: composition uses shader transitions, so disabling drawElementImage so page-side compositing stays available."
|
|
7453
7458
|
);
|
|
7454
7459
|
}
|
|
7455
7460
|
if (!cfg.useDrawElement && cfg.pageSideCompositingAutoDisabled) {
|
|
@@ -7777,7 +7782,7 @@ async function runProbeStage(input) {
|
|
|
7777
7782
|
} else {
|
|
7778
7783
|
beginFrameStalled = true;
|
|
7779
7784
|
log.warn(
|
|
7780
|
-
"[Render] BeginFrame liveness probe timed out
|
|
7785
|
+
"[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.",
|
|
7781
7786
|
{ probeTimeoutMs }
|
|
7782
7787
|
);
|
|
7783
7788
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
@@ -7991,7 +7996,7 @@ async function runProbeStage(input) {
|
|
|
7991
7996
|
video.start = win.visibleStart;
|
|
7992
7997
|
video.end = win.visibleEnd;
|
|
7993
7998
|
log.info(
|
|
7994
|
-
`[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s
|
|
7999
|
+
`[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s`
|
|
7995
8000
|
);
|
|
7996
8001
|
}
|
|
7997
8002
|
}
|
|
@@ -8039,7 +8044,7 @@ async function runProbeStage(input) {
|
|
|
8039
8044
|
});
|
|
8040
8045
|
if (!timelinesInfo.gsapLoaded) {
|
|
8041
8046
|
diagnostics.push(
|
|
8042
|
-
"GSAP is not loaded
|
|
8047
|
+
"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>."
|
|
8043
8048
|
);
|
|
8044
8049
|
} else if (timelinesInfo.timelineKeys.length === 0) {
|
|
8045
8050
|
diagnostics.push(
|
|
@@ -8056,10 +8061,10 @@ async function runProbeStage(input) {
|
|
|
8056
8061
|
log.warn("Failed to gather browser diagnostics for zero-duration composition", {
|
|
8057
8062
|
error: err instanceof Error ? err.message : String(err)
|
|
8058
8063
|
});
|
|
8059
|
-
diagnostics.push("(Could not gather browser diagnostics
|
|
8064
|
+
diagnostics.push("(Could not gather browser diagnostics. The page may have crashed.)");
|
|
8060
8065
|
}
|
|
8061
8066
|
const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
|
|
8062
|
-
throw new Error("Composition duration is 0
|
|
8067
|
+
throw new Error("Composition duration is 0, so this would produce a black video." + hint);
|
|
8063
8068
|
}
|
|
8064
8069
|
if (probeSession) {
|
|
8065
8070
|
const failedRequests = probeSession.browserConsoleBuffer.filter(isActionableProbeFailure);
|
|
@@ -8124,7 +8129,7 @@ function validateNoGpuEncode(config) {
|
|
|
8124
8129
|
if (config.useGpu === true) {
|
|
8125
8130
|
throw new PlanValidationError(
|
|
8126
8131
|
BROWSER_GPU_NOT_SOFTWARE,
|
|
8127
|
-
"[planValidation] GPU encode is banned in distributed mode: config.useGpu === true. Distributed retries must be byte-identical, but NVENC/QSV/VAAPI produce different output across machines. Set useGpu=false (the default)
|
|
8132
|
+
"[planValidation] GPU encode is banned in distributed mode: config.useGpu === true. Distributed retries must be byte-identical, but NVENC/QSV/VAAPI produce different output across machines. Set useGpu=false (the default): software libx264/libx265 is the only supported encoder in distributed mode."
|
|
8128
8133
|
);
|
|
8129
8134
|
}
|
|
8130
8135
|
if (config.browserGpuMode !== void 0 && config.browserGpuMode !== "software") {
|
|
@@ -8171,6 +8176,7 @@ import {
|
|
|
8171
8176
|
detectTransfer,
|
|
8172
8177
|
extractAllVideoFrames,
|
|
8173
8178
|
extractMediaMetadata as extractMediaMetadata3,
|
|
8179
|
+
isVideoPastTimelineEnd,
|
|
8174
8180
|
isHdrColorSpace as isHdrColorSpace2,
|
|
8175
8181
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
8176
8182
|
runVideoExtractionWithRetry,
|
|
@@ -8390,6 +8396,9 @@ async function runExtractVideosStage(input) {
|
|
|
8390
8396
|
} = input;
|
|
8391
8397
|
const stage2Start = Date.now();
|
|
8392
8398
|
const extractionPolicy = resolveVideoExtractionPolicy();
|
|
8399
|
+
composition.videos = composition.videos.filter(
|
|
8400
|
+
(video) => !isVideoPastTimelineEnd(video, composition.duration)
|
|
8401
|
+
);
|
|
8393
8402
|
let frameLookup = null;
|
|
8394
8403
|
let extractionResult = null;
|
|
8395
8404
|
let failureToEnforce = null;
|
|
@@ -8476,7 +8485,8 @@ async function runExtractVideosStage(input) {
|
|
|
8476
8485
|
timelineEnd: composition.duration,
|
|
8477
8486
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
8478
8487
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
8479
|
-
deferRangeExtraction
|
|
8488
|
+
deferRangeExtraction,
|
|
8489
|
+
contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)]
|
|
8480
8490
|
},
|
|
8481
8491
|
abortSignal,
|
|
8482
8492
|
cfg,
|
|
@@ -11102,7 +11112,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11102
11112
|
const madeProgress = captureAttemptMadeProgress(frameCount, remainingCount);
|
|
11103
11113
|
if (!madeProgress) {
|
|
11104
11114
|
options.log.warn(
|
|
11105
|
-
"[Render] Capture attempt made no forward progress; composition is likely structurally broken
|
|
11115
|
+
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
|
11106
11116
|
{ attempt, frameCount, remainingCount, workers: currentWorkers }
|
|
11107
11117
|
);
|
|
11108
11118
|
}
|
|
@@ -11171,7 +11181,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11171
11181
|
}
|
|
11172
11182
|
if (!madeProgress) {
|
|
11173
11183
|
options.log.warn(
|
|
11174
|
-
"[Render] Capture attempt made no forward progress; composition is likely structurally broken
|
|
11184
|
+
"[Render] Capture attempt made no forward progress; composition is likely structurally broken, not retrying.",
|
|
11175
11185
|
{ attempt, frameCount, remainingCount, workers: currentWorkers }
|
|
11176
11186
|
);
|
|
11177
11187
|
}
|
|
@@ -11634,7 +11644,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
11634
11644
|
execution.defer("remove workDir", () => {
|
|
11635
11645
|
if (job.config.debug) return;
|
|
11636
11646
|
if (job.status === "complete" && process.env.KEEP_TEMP === "1") {
|
|
11637
|
-
log.info("KEEP_TEMP=1
|
|
11647
|
+
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
11638
11648
|
return;
|
|
11639
11649
|
}
|
|
11640
11650
|
rmSync9(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
@@ -11871,7 +11881,7 @@ async function executeRenderPipeline(input) {
|
|
|
11871
11881
|
captureForceScreenshot = true;
|
|
11872
11882
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
11873
11883
|
log.info(
|
|
11874
|
-
"[Render] Low-memory render profile active
|
|
11884
|
+
"[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.",
|
|
11875
11885
|
{ totalMemMb: getSystemTotalMb(), thresholdMb: LOW_MEMORY_TOTAL_MB_THRESHOLD }
|
|
11876
11886
|
);
|
|
11877
11887
|
}
|
|
@@ -12439,7 +12449,7 @@ async function executeRenderPipeline(input) {
|
|
|
12439
12449
|
cfg.useDrawElement = false;
|
|
12440
12450
|
deClampReason = workerCount > 1 ? "parallel" : "disk_path";
|
|
12441
12451
|
log.info(
|
|
12442
|
-
"[Render] Fast capture: default-on drawElement disabled for this render
|
|
12452
|
+
"[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."
|
|
12443
12453
|
);
|
|
12444
12454
|
if (probeSession && probeSession.captureMode === "drawelement") {
|
|
12445
12455
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
@@ -12535,7 +12545,7 @@ async function executeRenderPipeline(input) {
|
|
|
12535
12545
|
});
|
|
12536
12546
|
updateCaptureObservability({ forceScreenshot: captureForceScreenshot });
|
|
12537
12547
|
log.info(
|
|
12538
|
-
`[Render] Page-side compositing enabled
|
|
12548
|
+
`[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.`
|
|
12539
12549
|
);
|
|
12540
12550
|
}
|
|
12541
12551
|
const useLayeredComposite = !usePageSideCompositingForTransitions && shouldUseLayeredComposite({
|
|
@@ -13032,7 +13042,7 @@ async function executeRenderPipeline(input) {
|
|
|
13032
13042
|
cfg.useDrawElement = false;
|
|
13033
13043
|
deClampReason = deClampReason ?? "disk_path";
|
|
13034
13044
|
log.info(
|
|
13035
|
-
"[Render] Fast capture: drawElement disabled for the disk fallback
|
|
13045
|
+
"[Render] Fast capture: drawElement disabled for the disk fallback: streaming encoder spawn failed and the disk path has no runtime self-verification."
|
|
13036
13046
|
);
|
|
13037
13047
|
if (probeSession && probeSession.captureMode === "drawelement") {
|
|
13038
13048
|
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
@@ -13584,10 +13594,10 @@ function validateJsonSafeValue(value, field) {
|
|
|
13584
13594
|
walkVariables(value, field, /* @__PURE__ */ new WeakSet());
|
|
13585
13595
|
}
|
|
13586
13596
|
var LEAF_REJECTIONS = {
|
|
13587
|
-
undefined: "undefined leaves are silently dropped by JSON.stringify
|
|
13597
|
+
undefined: "undefined leaves are silently dropped by JSON.stringify. Use null if you mean an absent value",
|
|
13588
13598
|
function: "functions are not JSON-serializable",
|
|
13589
13599
|
symbol: "Symbols are not JSON-serializable",
|
|
13590
|
-
bigint: "BigInt values throw at JSON.stringify
|
|
13600
|
+
bigint: "BigInt values throw at JSON.stringify. Encode as a string if you need 64-bit integers"
|
|
13591
13601
|
};
|
|
13592
13602
|
function walkVariables(value, path, seen) {
|
|
13593
13603
|
const t = typeof value;
|
|
@@ -13608,7 +13618,7 @@ function walkVariables(value, path, seen) {
|
|
|
13608
13618
|
if (seen.has(value)) {
|
|
13609
13619
|
throw new InvalidConfigError(
|
|
13610
13620
|
path,
|
|
13611
|
-
"circular reference detected
|
|
13621
|
+
"circular reference detected: JSON.stringify cannot serialize cycles"
|
|
13612
13622
|
);
|
|
13613
13623
|
}
|
|
13614
13624
|
seen.add(value);
|
|
@@ -14299,7 +14309,7 @@ function validateOutputResolutionOverride(body) {
|
|
|
14299
14309
|
return `Invalid outputResolution "${body.outputResolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: 1080p, 4k, \u2026).`;
|
|
14300
14310
|
}
|
|
14301
14311
|
if (normalized !== void 0 && (body.format === "webm" || body.format === "mov")) {
|
|
14302
|
-
return `outputResolution is not supported with format "${body.format}"
|
|
14312
|
+
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.`;
|
|
14303
14313
|
}
|
|
14304
14314
|
return void 0;
|
|
14305
14315
|
}
|
|
@@ -15024,7 +15034,7 @@ function collectPlanAssetShas(planDir) {
|
|
|
15024
15034
|
}
|
|
15025
15035
|
if (compositionHtml === null) {
|
|
15026
15036
|
throw new Error(
|
|
15027
|
-
`[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH}
|
|
15037
|
+
`[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH} (upstream compile stage did not materialize the expected file).`
|
|
15028
15038
|
);
|
|
15029
15039
|
}
|
|
15030
15040
|
return { compositionHtml, assets };
|
|
@@ -15607,7 +15617,7 @@ var PlanTooLargeError = class extends Error {
|
|
|
15607
15617
|
const breakdownSuffix = breakdown ? ` Breakdown: video-frames=${formatBytes(breakdown.videoFramesBytes)}, compiled=${formatBytes(breakdown.compiledBytes)} (source-media=${formatBytes(breakdown.sourceMediaBytes)}), audio=${formatBytes(breakdown.audioBytes)}, metadata=${formatBytes(breakdown.metadataBytes)}, other=${formatBytes(breakdown.otherBytes)}, files=${breakdown.fileCount}.` : "";
|
|
15608
15618
|
const observationSuffix = observedAt ? ` Observed at ${observedAt}.` : "";
|
|
15609
15619
|
super(
|
|
15610
|
-
`[plan] planDir size ${formatBytes(sizeBytes)} exceeds the configured ceiling ${formatBytes(limitBytes)} (PLAN_TOO_LARGE). The default 2 GB cap fits inside AWS Lambda's 10 GB /tmp budget alongside the chunk worker's frame buffer and ffmpeg's working set. To unblock: use the content-addressed \`planV2()\` transport, shorten the composition, lower the framerate, or use the in-process renderer (\`executeRenderJob\`)
|
|
15620
|
+
`[plan] planDir size ${formatBytes(sizeBytes)} exceeds the configured ceiling ${formatBytes(limitBytes)} (PLAN_TOO_LARGE). The default 2 GB cap fits inside AWS Lambda's 10 GB /tmp budget alongside the chunk worker's frame buffer and ffmpeg's working set. To unblock: use the content-addressed \`planV2()\` transport, shorten the composition, lower the framerate, or use the in-process renderer (\`executeRenderJob\`), which has no planDir size cap.` + observationSuffix + breakdownSuffix
|
|
15611
15621
|
);
|
|
15612
15622
|
this.name = "PlanTooLargeError";
|
|
15613
15623
|
this.sizeBytes = sizeBytes;
|
|
@@ -15624,7 +15634,7 @@ var FormatNotSupportedInDistributedError = class extends Error {
|
|
|
15624
15634
|
reason;
|
|
15625
15635
|
constructor(format, reason) {
|
|
15626
15636
|
super(
|
|
15627
|
-
`[plan] format ${JSON.stringify(format)} is not supported in distributed mode: ${reason}. Render with the in-process renderer (\`executeRenderJob\`)
|
|
15637
|
+
`[plan] format ${JSON.stringify(format)} is not supported in distributed mode: ${reason}. Render with the in-process renderer (\`executeRenderJob\`), which has full format support, or pick a distributed-supported format: mp4 SDR, mov ProRes 4444, png-sequence, or webm VP9.`
|
|
15628
15638
|
);
|
|
15629
15639
|
this.name = "FormatNotSupportedInDistributedError";
|
|
15630
15640
|
this.format = format;
|
|
@@ -15713,7 +15723,7 @@ async function readFontSnapshotSha() {
|
|
|
15713
15723
|
const data = module.EMBEDDED_FONT_DATA;
|
|
15714
15724
|
if (!data || typeof data !== "object") {
|
|
15715
15725
|
throw new Error(
|
|
15716
|
-
"[plan] EMBEDDED_FONT_DATA missing from @hyperframes/core/fonts/embedded-data
|
|
15726
|
+
"[plan] EMBEDDED_FONT_DATA missing from @hyperframes/core/fonts/embedded-data. Was `bun run build:fonts` run?"
|
|
15717
15727
|
);
|
|
15718
15728
|
}
|
|
15719
15729
|
const dataObj = data;
|
|
@@ -15763,7 +15773,7 @@ function resolveEncoderTriple(config) {
|
|
|
15763
15773
|
}
|
|
15764
15774
|
if (config.codec !== void 0) {
|
|
15765
15775
|
throw new Error(
|
|
15766
|
-
`[plan] DistributedRenderConfig.codec is only valid for format="mp4"; received codec=${JSON.stringify(config.codec)} with format=${JSON.stringify(config.format)}. Omit codec for non-mp4 formats
|
|
15776
|
+
`[plan] DistributedRenderConfig.codec is only valid for format="mp4"; received codec=${JSON.stringify(config.codec)} with format=${JSON.stringify(config.format)}. Omit codec for non-mp4 formats: mov is always ProRes 4444, webm is always libvpx-vp9, and png-sequence has no encoder.`
|
|
15767
15777
|
);
|
|
15768
15778
|
}
|
|
15769
15779
|
return resolveNonMp4EncoderTriple(config.format, config.quality ?? "standard");
|