@hyperframes/producer 0.8.213 → 0.8.216
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 +58 -19
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +71 -71
- package/dist/index.js +86 -25
- package/dist/public-server.js +78 -17
- package/dist/services/render/observability.d.ts +4 -0
- package/dist/services/render/stages/probeStage.d.ts +13 -1
- package/dist/services/renderOrchestrator.d.ts +3 -2
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -3690,6 +3690,10 @@ function resolveCaptureStallTimeoutMs() {
|
|
|
3690
3690
|
const parsed = raw ? Number(raw) : Number.NaN;
|
|
3691
3691
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
|
|
3692
3692
|
}
|
|
3693
|
+
function stallBudgetMs(stallTimeoutMs, framesDone, beginFrame, cfg) {
|
|
3694
|
+
const deadline = cfg.beginFrameFirstFrameDeadlineMs;
|
|
3695
|
+
return beginFrame && framesDone === 0 && deadline ? Math.min(stallTimeoutMs, deadline) : stallTimeoutMs;
|
|
3696
|
+
}
|
|
3693
3697
|
function captureModeLabel(mode) {
|
|
3694
3698
|
if (mode === "drawelement") return "drawElement";
|
|
3695
3699
|
if (mode === "beginframe") return "BeginFrame";
|
|
@@ -4062,14 +4066,20 @@ async function runCaptureStreamingStage(input) {
|
|
|
4062
4066
|
const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
|
|
4063
4067
|
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
4064
4068
|
let stalled = false;
|
|
4069
|
+
const budget = () => stallBudgetMs(
|
|
4070
|
+
stallTimeoutMs,
|
|
4071
|
+
resumeFrom + lastCapturedFrames,
|
|
4072
|
+
captureLabel === "BeginFrame",
|
|
4073
|
+
cfg
|
|
4074
|
+
);
|
|
4065
4075
|
const stallTimer = setInterval(
|
|
4066
4076
|
() => {
|
|
4067
|
-
if (!watchdogArmed || Date.now() - lastProgressAt <=
|
|
4077
|
+
if (!watchdogArmed || Date.now() - lastProgressAt <= budget()) return;
|
|
4068
4078
|
stalled = true;
|
|
4069
4079
|
const stallErr = new ParallelCaptureStallError({
|
|
4070
4080
|
variant: "drain",
|
|
4071
4081
|
captureLabel,
|
|
4072
|
-
stallTimeoutMs,
|
|
4082
|
+
stallTimeoutMs: budget(),
|
|
4073
4083
|
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4074
4084
|
totalFrames,
|
|
4075
4085
|
phaseSummary: phaseSummary()
|
|
@@ -4077,7 +4087,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4077
4087
|
parallelReorder.abort(stallErr);
|
|
4078
4088
|
stallController.abort();
|
|
4079
4089
|
},
|
|
4080
|
-
Math.min(
|
|
4090
|
+
Math.min(budget(), DE_STALL_POLL_MS)
|
|
4081
4091
|
);
|
|
4082
4092
|
try {
|
|
4083
4093
|
workerResults = await executeParallelCapture(
|
|
@@ -4158,7 +4168,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4158
4168
|
throw new ParallelCaptureStallError({
|
|
4159
4169
|
variant: "pool",
|
|
4160
4170
|
captureLabel,
|
|
4161
|
-
stallTimeoutMs,
|
|
4171
|
+
stallTimeoutMs: budget(),
|
|
4162
4172
|
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4163
4173
|
totalFrames,
|
|
4164
4174
|
phaseSummary: phaseSummary()
|
|
@@ -4259,10 +4269,16 @@ async function runCaptureStreamingStage(input) {
|
|
|
4259
4269
|
for (let i = 0; i < totalFrames; i++) {
|
|
4260
4270
|
assertNotAborted();
|
|
4261
4271
|
const time = i * job.config.fps.den / job.config.fps.num;
|
|
4272
|
+
const budget = stallBudgetMs(
|
|
4273
|
+
stallTimeoutMs,
|
|
4274
|
+
i,
|
|
4275
|
+
session.captureMode === "beginframe",
|
|
4276
|
+
cfg
|
|
4277
|
+
);
|
|
4262
4278
|
const { buffer } = await raceAgainstStall(
|
|
4263
4279
|
captureFrameToBuffer(session, i, time),
|
|
4264
|
-
|
|
4265
|
-
{ session, frameIndex: i, totalFrames, stallTimeoutMs },
|
|
4280
|
+
budget - (Date.now() - lastProgressAt),
|
|
4281
|
+
{ session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
|
|
4266
4282
|
abortSignal
|
|
4267
4283
|
);
|
|
4268
4284
|
await reorderBuffer.waitForFrame(i);
|
|
@@ -5480,6 +5496,7 @@ function watchStall(phase, rule) {
|
|
|
5480
5496
|
let lastChangeAt = Date.now();
|
|
5481
5497
|
let attempt;
|
|
5482
5498
|
let timer;
|
|
5499
|
+
let inTail = false;
|
|
5483
5500
|
const restartWindow = (ms) => {
|
|
5484
5501
|
last = rule.progress();
|
|
5485
5502
|
budgetMs = ms;
|
|
@@ -5489,6 +5506,10 @@ function watchStall(phase, rule) {
|
|
|
5489
5506
|
timer = setInterval(
|
|
5490
5507
|
() => {
|
|
5491
5508
|
const current = rule.progress();
|
|
5509
|
+
if (rule.complete?.() && !inTail) {
|
|
5510
|
+
inTail = true;
|
|
5511
|
+
return restartWindow(rule.tailMs ?? rule.noProgressMs);
|
|
5512
|
+
}
|
|
5492
5513
|
if (current !== last) return restartWindow(rule.noProgressMs);
|
|
5493
5514
|
if (Date.now() - lastChangeAt < budgetMs) return;
|
|
5494
5515
|
const error = new RenderStageStalledError(phase, budgetMs, current);
|
|
@@ -5504,6 +5525,7 @@ function watchStall(phase, rule) {
|
|
|
5504
5525
|
});
|
|
5505
5526
|
const openAttempt = (parent) => {
|
|
5506
5527
|
attempt = new AbortController();
|
|
5528
|
+
inTail = false;
|
|
5507
5529
|
restartWindow(rule.firstProgressMs);
|
|
5508
5530
|
return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
|
|
5509
5531
|
};
|
|
@@ -8495,7 +8517,8 @@ async function runCompileStage(input) {
|
|
|
8495
8517
|
}
|
|
8496
8518
|
|
|
8497
8519
|
// src/services/render/stages/probeStage.ts
|
|
8498
|
-
import {
|
|
8520
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8521
|
+
import { basename as basename5, join as join17 } from "node:path";
|
|
8499
8522
|
import { parseHTML as parseHTML6 } from "linkedom";
|
|
8500
8523
|
import {
|
|
8501
8524
|
closeCaptureSession as closeCaptureSession4,
|
|
@@ -8503,6 +8526,7 @@ import {
|
|
|
8503
8526
|
createCaptureSession as createCaptureSession4,
|
|
8504
8527
|
deriveBeginFrameProbeTimeTicks,
|
|
8505
8528
|
getCompositionDuration,
|
|
8529
|
+
getCompositionLengthSource,
|
|
8506
8530
|
initializeSession as initializeSession4,
|
|
8507
8531
|
isTransientBrowserError,
|
|
8508
8532
|
probeBeginFrameLiveness,
|
|
@@ -8641,6 +8665,13 @@ function hasRuntimeMediaChanges(html) {
|
|
|
8641
8665
|
function probeRequiresBrowser(args) {
|
|
8642
8666
|
return args.durationSeconds <= 0 || args.unresolvedCompositionCount > 0 || args.hasAutoStart || args.hasScriptedAudio || args.hasVariableMedia || args.hasInsertedMedia;
|
|
8643
8667
|
}
|
|
8668
|
+
function reportedLengthSource(source) {
|
|
8669
|
+
if (!source) return null;
|
|
8670
|
+
if (source.from !== "media") return { from: source.from };
|
|
8671
|
+
const name = basename5(source.src.split(/[?#]/, 1)[0] ?? "");
|
|
8672
|
+
const srcHash = createHash7("sha256").update(name).digest("hex").slice(0, 12);
|
|
8673
|
+
return { from: "media", tag: source.tag, trimmed: source.trimmed, srcHash };
|
|
8674
|
+
}
|
|
8644
8675
|
async function runProbeStage(input) {
|
|
8645
8676
|
const {
|
|
8646
8677
|
projectDir,
|
|
@@ -8662,6 +8693,7 @@ async function runProbeStage(input) {
|
|
|
8662
8693
|
const probeCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
8663
8694
|
let fileServer = null;
|
|
8664
8695
|
let probeSession = null;
|
|
8696
|
+
let lengthSource = input.composition.duration > 0 ? { from: "declared" } : null;
|
|
8665
8697
|
let beginFrameStalled = false;
|
|
8666
8698
|
let lastBrowserConsole = [];
|
|
8667
8699
|
const probeStart = Date.now();
|
|
@@ -8823,10 +8855,12 @@ async function runProbeStage(input) {
|
|
|
8823
8855
|
if (composition.duration <= 0) {
|
|
8824
8856
|
log.info("Discovering composition duration...");
|
|
8825
8857
|
const discoveredDuration = await getCompositionDuration(session);
|
|
8858
|
+
lengthSource = reportedLengthSource(await getCompositionLengthSource(session));
|
|
8826
8859
|
assertNotAborted();
|
|
8827
8860
|
log.info("Probed composition duration from browser", {
|
|
8828
8861
|
discoveredDuration,
|
|
8829
|
-
staticDuration: compiled.staticDuration
|
|
8862
|
+
staticDuration: compiled.staticDuration,
|
|
8863
|
+
lengthSource
|
|
8830
8864
|
});
|
|
8831
8865
|
composition.duration = discoveredDuration;
|
|
8832
8866
|
} else {
|
|
@@ -9115,6 +9149,7 @@ async function runProbeStage(input) {
|
|
|
9115
9149
|
probeSession,
|
|
9116
9150
|
lastBrowserConsole,
|
|
9117
9151
|
duration,
|
|
9152
|
+
lengthSource,
|
|
9118
9153
|
totalFrames,
|
|
9119
9154
|
browserProbeMs,
|
|
9120
9155
|
beginFrameStalled
|
|
@@ -9639,7 +9674,7 @@ function checkedDbfs(value) {
|
|
|
9639
9674
|
}
|
|
9640
9675
|
return value;
|
|
9641
9676
|
}
|
|
9642
|
-
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
9677
|
+
async function scanTruePeak(audioPath, trim, timeoutMs, signal, seekSeconds = 0) {
|
|
9643
9678
|
let firstBlock;
|
|
9644
9679
|
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
9645
9680
|
let partialLine = "";
|
|
@@ -9655,7 +9690,21 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
9655
9690
|
}
|
|
9656
9691
|
};
|
|
9657
9692
|
const result = await runFfmpeg4(
|
|
9658
|
-
[
|
|
9693
|
+
[
|
|
9694
|
+
...seekSeconds > 0 ? ["-ss", String(seekSeconds)] : [],
|
|
9695
|
+
...[
|
|
9696
|
+
"-i",
|
|
9697
|
+
audioPath,
|
|
9698
|
+
"-map",
|
|
9699
|
+
"0:a:0",
|
|
9700
|
+
"-vn",
|
|
9701
|
+
"-af",
|
|
9702
|
+
`${trim}ebur128=peak=true`,
|
|
9703
|
+
"-f",
|
|
9704
|
+
"null",
|
|
9705
|
+
"-"
|
|
9706
|
+
]
|
|
9707
|
+
],
|
|
9659
9708
|
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
9660
9709
|
);
|
|
9661
9710
|
if (!result.success) {
|
|
@@ -9681,12 +9730,15 @@ async function measureTruePeakDbfs(audioPath, options) {
|
|
|
9681
9730
|
const scans = await Promise.all(
|
|
9682
9731
|
Array.from({ length: count }, (_, k) => {
|
|
9683
9732
|
const start = k === 0 ? 0 : cut(k) - block;
|
|
9684
|
-
const
|
|
9733
|
+
const seekSeconds = Math.max(0, Math.floor(start / sampleRate) - 1);
|
|
9734
|
+
const skipped = seekSeconds * sampleRate;
|
|
9735
|
+
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1) - skipped}`;
|
|
9685
9736
|
return scanTruePeak(
|
|
9686
9737
|
audioPath,
|
|
9687
|
-
`atrim=start_sample=${start}${end},`,
|
|
9738
|
+
`atrim=start_sample=${start - skipped}${end},`,
|
|
9688
9739
|
timeoutMs,
|
|
9689
|
-
segmentSignal
|
|
9740
|
+
segmentSignal,
|
|
9741
|
+
seekSeconds
|
|
9690
9742
|
);
|
|
9691
9743
|
})
|
|
9692
9744
|
).catch((error) => {
|
|
@@ -12315,12 +12367,15 @@ function resetCaptureAttemptProgress(job) {
|
|
|
12315
12367
|
job.framesRendered = 0;
|
|
12316
12368
|
}
|
|
12317
12369
|
var FIRST_FRAME_STALL_MS = 3e5;
|
|
12318
|
-
|
|
12370
|
+
var TAIL_STALL_MS = FIRST_FRAME_STALL_MS;
|
|
12371
|
+
function frameStallRule(job, totalFrames) {
|
|
12319
12372
|
const noProgressMs = resolveCaptureStallTimeoutMs();
|
|
12320
12373
|
return {
|
|
12321
12374
|
progress: () => job.framesRendered ?? 0,
|
|
12322
12375
|
noProgressMs,
|
|
12323
|
-
firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
|
|
12376
|
+
firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs),
|
|
12377
|
+
complete: () => (job.framesRendered ?? 0) >= totalFrames,
|
|
12378
|
+
tailMs: Math.max(TAIL_STALL_MS, noProgressMs)
|
|
12324
12379
|
};
|
|
12325
12380
|
}
|
|
12326
12381
|
function isRecoverableParallelCaptureError(error) {
|
|
@@ -13337,6 +13392,12 @@ async function executeRenderPipeline(input) {
|
|
|
13337
13392
|
}
|
|
13338
13393
|
observability.checkpoint("browser_probe", "duration resolved", {
|
|
13339
13394
|
durationSeconds: probeResult.duration,
|
|
13395
|
+
lengthFrom: probeResult.lengthSource?.from ?? null,
|
|
13396
|
+
...probeResult.lengthSource?.from === "media" ? {
|
|
13397
|
+
lengthTag: probeResult.lengthSource.tag,
|
|
13398
|
+
lengthTrimmed: probeResult.lengthSource.trimmed,
|
|
13399
|
+
lengthSrcHash: probeResult.lengthSource.srcHash
|
|
13400
|
+
} : {},
|
|
13340
13401
|
totalFrames,
|
|
13341
13402
|
compositionHash,
|
|
13342
13403
|
beginFrameStalled: probeResult.beginFrameStalled
|
|
@@ -14206,7 +14267,7 @@ async function executeRenderPipeline(input) {
|
|
|
14206
14267
|
onProgress
|
|
14207
14268
|
});
|
|
14208
14269
|
},
|
|
14209
|
-
{ stall: frameStallRule(job) }
|
|
14270
|
+
{ stall: frameStallRule(job, totalFrames) }
|
|
14210
14271
|
);
|
|
14211
14272
|
lastBrowserConsole = hdrRes.lastBrowserConsole;
|
|
14212
14273
|
layeredCaptureWarnings.push(...hdrRes.warnings);
|
|
@@ -14651,7 +14712,7 @@ async function executeRenderPipeline(input) {
|
|
|
14651
14712
|
interleaveFrames: overlap !== null,
|
|
14652
14713
|
openAttempt
|
|
14653
14714
|
}),
|
|
14654
|
-
{ stall: frameStallRule(job) }
|
|
14715
|
+
{ stall: frameStallRule(job, totalFrames) }
|
|
14655
14716
|
);
|
|
14656
14717
|
let captureRes;
|
|
14657
14718
|
try {
|
|
@@ -15742,12 +15803,12 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
|
|
|
15742
15803
|
}
|
|
15743
15804
|
|
|
15744
15805
|
// src/services/render/stages/planHash.ts
|
|
15745
|
-
import { createHash as
|
|
15806
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
15746
15807
|
import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync5, readSync as readSync4 } from "node:fs";
|
|
15747
15808
|
var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
|
|
15748
15809
|
var FIELD_DELIMITER = Buffer.from([0]);
|
|
15749
15810
|
function computePlanHash(input) {
|
|
15750
|
-
const hash =
|
|
15811
|
+
const hash = createHash8("sha256");
|
|
15751
15812
|
hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
|
|
15752
15813
|
hash.update(input.compositionHtml);
|
|
15753
15814
|
hash.update(FIELD_DELIMITER);
|
|
@@ -15794,12 +15855,12 @@ function canonicalJsonStringify(value) {
|
|
|
15794
15855
|
throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
|
|
15795
15856
|
}
|
|
15796
15857
|
function sha256Hex(bytes) {
|
|
15797
|
-
const h =
|
|
15858
|
+
const h = createHash8("sha256");
|
|
15798
15859
|
h.update(bytes);
|
|
15799
15860
|
return h.digest("hex");
|
|
15800
15861
|
}
|
|
15801
15862
|
function sha256File(path) {
|
|
15802
|
-
const hash =
|
|
15863
|
+
const hash = createHash8("sha256");
|
|
15803
15864
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
15804
15865
|
const fd = openSync5(path, "r");
|
|
15805
15866
|
try {
|
|
@@ -15814,7 +15875,7 @@ function sha256File(path) {
|
|
|
15814
15875
|
return hash.digest("hex");
|
|
15815
15876
|
}
|
|
15816
15877
|
async function sha256FileAsync(path) {
|
|
15817
|
-
const hash =
|
|
15878
|
+
const hash = createHash8("sha256");
|
|
15818
15879
|
for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
|
|
15819
15880
|
hash.update(chunk);
|
|
15820
15881
|
}
|
|
@@ -16585,7 +16646,7 @@ async function muxChunkWithAudio(input) {
|
|
|
16585
16646
|
}
|
|
16586
16647
|
|
|
16587
16648
|
// src/services/distributed/planSize.ts
|
|
16588
|
-
import { createHash as
|
|
16649
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
16589
16650
|
import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
|
|
16590
16651
|
import { extname as extname5, join as join34, relative as relative4 } from "node:path";
|
|
16591
16652
|
var MAX_TOP_COMPONENTS = 10;
|
|
@@ -16615,7 +16676,7 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
16615
16676
|
"plan.json": "metadata"
|
|
16616
16677
|
};
|
|
16617
16678
|
function hashComponent(value) {
|
|
16618
|
-
return
|
|
16679
|
+
return createHash9("sha256").update(value).digest("hex").slice(0, 12);
|
|
16619
16680
|
}
|
|
16620
16681
|
function classifyFile(segments, rootKind) {
|
|
16621
16682
|
const rootName = segments[0] ?? "";
|
|
@@ -20834,7 +20895,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20834
20895
|
openAttempt,
|
|
20835
20896
|
verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
|
|
20836
20897
|
}),
|
|
20837
|
-
{ stall: frameStallRule(job) }
|
|
20898
|
+
{ stall: frameStallRule(job, framesInChunk) }
|
|
20838
20899
|
).catch(async (captureError) => {
|
|
20839
20900
|
await overlap.current?.abort();
|
|
20840
20901
|
overlap.current = null;
|
package/dist/public-server.js
CHANGED
|
@@ -3707,6 +3707,10 @@ function resolveCaptureStallTimeoutMs() {
|
|
|
3707
3707
|
const parsed = raw ? Number(raw) : Number.NaN;
|
|
3708
3708
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
|
|
3709
3709
|
}
|
|
3710
|
+
function stallBudgetMs(stallTimeoutMs, framesDone, beginFrame, cfg) {
|
|
3711
|
+
const deadline = cfg.beginFrameFirstFrameDeadlineMs;
|
|
3712
|
+
return beginFrame && framesDone === 0 && deadline ? Math.min(stallTimeoutMs, deadline) : stallTimeoutMs;
|
|
3713
|
+
}
|
|
3710
3714
|
function captureModeLabel(mode) {
|
|
3711
3715
|
if (mode === "drawelement") return "drawElement";
|
|
3712
3716
|
if (mode === "beginframe") return "BeginFrame";
|
|
@@ -4079,14 +4083,20 @@ async function runCaptureStreamingStage(input) {
|
|
|
4079
4083
|
const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
|
|
4080
4084
|
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
4081
4085
|
let stalled = false;
|
|
4086
|
+
const budget = () => stallBudgetMs(
|
|
4087
|
+
stallTimeoutMs,
|
|
4088
|
+
resumeFrom + lastCapturedFrames,
|
|
4089
|
+
captureLabel === "BeginFrame",
|
|
4090
|
+
cfg
|
|
4091
|
+
);
|
|
4082
4092
|
const stallTimer = setInterval(
|
|
4083
4093
|
() => {
|
|
4084
|
-
if (!watchdogArmed || Date.now() - lastProgressAt <=
|
|
4094
|
+
if (!watchdogArmed || Date.now() - lastProgressAt <= budget()) return;
|
|
4085
4095
|
stalled = true;
|
|
4086
4096
|
const stallErr = new ParallelCaptureStallError({
|
|
4087
4097
|
variant: "drain",
|
|
4088
4098
|
captureLabel,
|
|
4089
|
-
stallTimeoutMs,
|
|
4099
|
+
stallTimeoutMs: budget(),
|
|
4090
4100
|
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4091
4101
|
totalFrames,
|
|
4092
4102
|
phaseSummary: phaseSummary()
|
|
@@ -4094,7 +4104,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4094
4104
|
parallelReorder.abort(stallErr);
|
|
4095
4105
|
stallController.abort();
|
|
4096
4106
|
},
|
|
4097
|
-
Math.min(
|
|
4107
|
+
Math.min(budget(), DE_STALL_POLL_MS)
|
|
4098
4108
|
);
|
|
4099
4109
|
try {
|
|
4100
4110
|
workerResults = await executeParallelCapture(
|
|
@@ -4175,7 +4185,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4175
4185
|
throw new ParallelCaptureStallError({
|
|
4176
4186
|
variant: "pool",
|
|
4177
4187
|
captureLabel,
|
|
4178
|
-
stallTimeoutMs,
|
|
4188
|
+
stallTimeoutMs: budget(),
|
|
4179
4189
|
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4180
4190
|
totalFrames,
|
|
4181
4191
|
phaseSummary: phaseSummary()
|
|
@@ -4276,10 +4286,16 @@ async function runCaptureStreamingStage(input) {
|
|
|
4276
4286
|
for (let i = 0; i < totalFrames; i++) {
|
|
4277
4287
|
assertNotAborted();
|
|
4278
4288
|
const time = i * job.config.fps.den / job.config.fps.num;
|
|
4289
|
+
const budget = stallBudgetMs(
|
|
4290
|
+
stallTimeoutMs,
|
|
4291
|
+
i,
|
|
4292
|
+
session.captureMode === "beginframe",
|
|
4293
|
+
cfg
|
|
4294
|
+
);
|
|
4279
4295
|
const { buffer } = await raceAgainstStall(
|
|
4280
4296
|
captureFrameToBuffer(session, i, time),
|
|
4281
|
-
|
|
4282
|
-
{ session, frameIndex: i, totalFrames, stallTimeoutMs },
|
|
4297
|
+
budget - (Date.now() - lastProgressAt),
|
|
4298
|
+
{ session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
|
|
4283
4299
|
abortSignal
|
|
4284
4300
|
);
|
|
4285
4301
|
await reorderBuffer.waitForFrame(i);
|
|
@@ -5497,6 +5513,7 @@ function watchStall(phase, rule) {
|
|
|
5497
5513
|
let lastChangeAt = Date.now();
|
|
5498
5514
|
let attempt;
|
|
5499
5515
|
let timer;
|
|
5516
|
+
let inTail = false;
|
|
5500
5517
|
const restartWindow = (ms) => {
|
|
5501
5518
|
last = rule.progress();
|
|
5502
5519
|
budgetMs = ms;
|
|
@@ -5506,6 +5523,10 @@ function watchStall(phase, rule) {
|
|
|
5506
5523
|
timer = setInterval(
|
|
5507
5524
|
() => {
|
|
5508
5525
|
const current = rule.progress();
|
|
5526
|
+
if (rule.complete?.() && !inTail) {
|
|
5527
|
+
inTail = true;
|
|
5528
|
+
return restartWindow(rule.tailMs ?? rule.noProgressMs);
|
|
5529
|
+
}
|
|
5509
5530
|
if (current !== last) return restartWindow(rule.noProgressMs);
|
|
5510
5531
|
if (Date.now() - lastChangeAt < budgetMs) return;
|
|
5511
5532
|
const error = new RenderStageStalledError(phase, budgetMs, current);
|
|
@@ -5521,6 +5542,7 @@ function watchStall(phase, rule) {
|
|
|
5521
5542
|
});
|
|
5522
5543
|
const openAttempt = (parent) => {
|
|
5523
5544
|
attempt = new AbortController();
|
|
5545
|
+
inTail = false;
|
|
5524
5546
|
restartWindow(rule.firstProgressMs);
|
|
5525
5547
|
return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
|
|
5526
5548
|
};
|
|
@@ -8512,7 +8534,8 @@ async function runCompileStage(input) {
|
|
|
8512
8534
|
}
|
|
8513
8535
|
|
|
8514
8536
|
// src/services/render/stages/probeStage.ts
|
|
8515
|
-
import {
|
|
8537
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
8538
|
+
import { basename as basename5, join as join17 } from "node:path";
|
|
8516
8539
|
import { parseHTML as parseHTML6 } from "linkedom";
|
|
8517
8540
|
import {
|
|
8518
8541
|
closeCaptureSession as closeCaptureSession4,
|
|
@@ -8520,6 +8543,7 @@ import {
|
|
|
8520
8543
|
createCaptureSession as createCaptureSession4,
|
|
8521
8544
|
deriveBeginFrameProbeTimeTicks,
|
|
8522
8545
|
getCompositionDuration,
|
|
8546
|
+
getCompositionLengthSource,
|
|
8523
8547
|
initializeSession as initializeSession4,
|
|
8524
8548
|
isTransientBrowserError,
|
|
8525
8549
|
probeBeginFrameLiveness,
|
|
@@ -8658,6 +8682,13 @@ function hasRuntimeMediaChanges(html) {
|
|
|
8658
8682
|
function probeRequiresBrowser(args) {
|
|
8659
8683
|
return args.durationSeconds <= 0 || args.unresolvedCompositionCount > 0 || args.hasAutoStart || args.hasScriptedAudio || args.hasVariableMedia || args.hasInsertedMedia;
|
|
8660
8684
|
}
|
|
8685
|
+
function reportedLengthSource(source) {
|
|
8686
|
+
if (!source) return null;
|
|
8687
|
+
if (source.from !== "media") return { from: source.from };
|
|
8688
|
+
const name = basename5(source.src.split(/[?#]/, 1)[0] ?? "");
|
|
8689
|
+
const srcHash = createHash7("sha256").update(name).digest("hex").slice(0, 12);
|
|
8690
|
+
return { from: "media", tag: source.tag, trimmed: source.trimmed, srcHash };
|
|
8691
|
+
}
|
|
8661
8692
|
async function runProbeStage(input) {
|
|
8662
8693
|
const {
|
|
8663
8694
|
projectDir,
|
|
@@ -8679,6 +8710,7 @@ async function runProbeStage(input) {
|
|
|
8679
8710
|
const probeCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
8680
8711
|
let fileServer = null;
|
|
8681
8712
|
let probeSession = null;
|
|
8713
|
+
let lengthSource = input.composition.duration > 0 ? { from: "declared" } : null;
|
|
8682
8714
|
let beginFrameStalled = false;
|
|
8683
8715
|
let lastBrowserConsole = [];
|
|
8684
8716
|
const probeStart = Date.now();
|
|
@@ -8840,10 +8872,12 @@ async function runProbeStage(input) {
|
|
|
8840
8872
|
if (composition.duration <= 0) {
|
|
8841
8873
|
log.info("Discovering composition duration...");
|
|
8842
8874
|
const discoveredDuration = await getCompositionDuration(session);
|
|
8875
|
+
lengthSource = reportedLengthSource(await getCompositionLengthSource(session));
|
|
8843
8876
|
assertNotAborted();
|
|
8844
8877
|
log.info("Probed composition duration from browser", {
|
|
8845
8878
|
discoveredDuration,
|
|
8846
|
-
staticDuration: compiled.staticDuration
|
|
8879
|
+
staticDuration: compiled.staticDuration,
|
|
8880
|
+
lengthSource
|
|
8847
8881
|
});
|
|
8848
8882
|
composition.duration = discoveredDuration;
|
|
8849
8883
|
} else {
|
|
@@ -9132,6 +9166,7 @@ async function runProbeStage(input) {
|
|
|
9132
9166
|
probeSession,
|
|
9133
9167
|
lastBrowserConsole,
|
|
9134
9168
|
duration,
|
|
9169
|
+
lengthSource,
|
|
9135
9170
|
totalFrames,
|
|
9136
9171
|
browserProbeMs,
|
|
9137
9172
|
beginFrameStalled
|
|
@@ -9619,7 +9654,7 @@ function checkedDbfs(value) {
|
|
|
9619
9654
|
}
|
|
9620
9655
|
return value;
|
|
9621
9656
|
}
|
|
9622
|
-
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
9657
|
+
async function scanTruePeak(audioPath, trim, timeoutMs, signal, seekSeconds = 0) {
|
|
9623
9658
|
let firstBlock;
|
|
9624
9659
|
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
9625
9660
|
let partialLine = "";
|
|
@@ -9635,7 +9670,21 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
9635
9670
|
}
|
|
9636
9671
|
};
|
|
9637
9672
|
const result = await runFfmpeg4(
|
|
9638
|
-
[
|
|
9673
|
+
[
|
|
9674
|
+
...seekSeconds > 0 ? ["-ss", String(seekSeconds)] : [],
|
|
9675
|
+
...[
|
|
9676
|
+
"-i",
|
|
9677
|
+
audioPath,
|
|
9678
|
+
"-map",
|
|
9679
|
+
"0:a:0",
|
|
9680
|
+
"-vn",
|
|
9681
|
+
"-af",
|
|
9682
|
+
`${trim}ebur128=peak=true`,
|
|
9683
|
+
"-f",
|
|
9684
|
+
"null",
|
|
9685
|
+
"-"
|
|
9686
|
+
]
|
|
9687
|
+
],
|
|
9639
9688
|
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
9640
9689
|
);
|
|
9641
9690
|
if (!result.success) {
|
|
@@ -9661,12 +9710,15 @@ async function measureTruePeakDbfs(audioPath, options) {
|
|
|
9661
9710
|
const scans = await Promise.all(
|
|
9662
9711
|
Array.from({ length: count }, (_, k) => {
|
|
9663
9712
|
const start = k === 0 ? 0 : cut(k) - block;
|
|
9664
|
-
const
|
|
9713
|
+
const seekSeconds = Math.max(0, Math.floor(start / sampleRate) - 1);
|
|
9714
|
+
const skipped = seekSeconds * sampleRate;
|
|
9715
|
+
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1) - skipped}`;
|
|
9665
9716
|
return scanTruePeak(
|
|
9666
9717
|
audioPath,
|
|
9667
|
-
`atrim=start_sample=${start}${end},`,
|
|
9718
|
+
`atrim=start_sample=${start - skipped}${end},`,
|
|
9668
9719
|
timeoutMs,
|
|
9669
|
-
segmentSignal
|
|
9720
|
+
segmentSignal,
|
|
9721
|
+
seekSeconds
|
|
9670
9722
|
);
|
|
9671
9723
|
})
|
|
9672
9724
|
).catch((error) => {
|
|
@@ -12295,12 +12347,15 @@ function resetCaptureAttemptProgress(job) {
|
|
|
12295
12347
|
job.framesRendered = 0;
|
|
12296
12348
|
}
|
|
12297
12349
|
var FIRST_FRAME_STALL_MS = 3e5;
|
|
12298
|
-
|
|
12350
|
+
var TAIL_STALL_MS = FIRST_FRAME_STALL_MS;
|
|
12351
|
+
function frameStallRule(job, totalFrames) {
|
|
12299
12352
|
const noProgressMs = resolveCaptureStallTimeoutMs();
|
|
12300
12353
|
return {
|
|
12301
12354
|
progress: () => job.framesRendered ?? 0,
|
|
12302
12355
|
noProgressMs,
|
|
12303
|
-
firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
|
|
12356
|
+
firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs),
|
|
12357
|
+
complete: () => (job.framesRendered ?? 0) >= totalFrames,
|
|
12358
|
+
tailMs: Math.max(TAIL_STALL_MS, noProgressMs)
|
|
12304
12359
|
};
|
|
12305
12360
|
}
|
|
12306
12361
|
function isRecoverableParallelCaptureError(error) {
|
|
@@ -13317,6 +13372,12 @@ async function executeRenderPipeline(input) {
|
|
|
13317
13372
|
}
|
|
13318
13373
|
observability.checkpoint("browser_probe", "duration resolved", {
|
|
13319
13374
|
durationSeconds: probeResult.duration,
|
|
13375
|
+
lengthFrom: probeResult.lengthSource?.from ?? null,
|
|
13376
|
+
...probeResult.lengthSource?.from === "media" ? {
|
|
13377
|
+
lengthTag: probeResult.lengthSource.tag,
|
|
13378
|
+
lengthTrimmed: probeResult.lengthSource.trimmed,
|
|
13379
|
+
lengthSrcHash: probeResult.lengthSource.srcHash
|
|
13380
|
+
} : {},
|
|
13320
13381
|
totalFrames,
|
|
13321
13382
|
compositionHash,
|
|
13322
13383
|
beginFrameStalled: probeResult.beginFrameStalled
|
|
@@ -14186,7 +14247,7 @@ async function executeRenderPipeline(input) {
|
|
|
14186
14247
|
onProgress
|
|
14187
14248
|
});
|
|
14188
14249
|
},
|
|
14189
|
-
{ stall: frameStallRule(job) }
|
|
14250
|
+
{ stall: frameStallRule(job, totalFrames) }
|
|
14190
14251
|
);
|
|
14191
14252
|
lastBrowserConsole = hdrRes.lastBrowserConsole;
|
|
14192
14253
|
layeredCaptureWarnings.push(...hdrRes.warnings);
|
|
@@ -14631,7 +14692,7 @@ async function executeRenderPipeline(input) {
|
|
|
14631
14692
|
interleaveFrames: overlap !== null,
|
|
14632
14693
|
openAttempt
|
|
14633
14694
|
}),
|
|
14634
|
-
{ stall: frameStallRule(job) }
|
|
14695
|
+
{ stall: frameStallRule(job, totalFrames) }
|
|
14635
14696
|
);
|
|
14636
14697
|
let captureRes;
|
|
14637
14698
|
try {
|
|
@@ -333,6 +333,10 @@ export interface StageStallRule {
|
|
|
333
333
|
noProgressMs: number;
|
|
334
334
|
/** Allowed silence before the current attempt's first change. */
|
|
335
335
|
firstProgressMs: number;
|
|
336
|
+
/** True once every unit of work has landed: nothing more can change `progress()`, only the tail work runs. */
|
|
337
|
+
complete?: () => boolean;
|
|
338
|
+
/** Allowed silence once `complete()` holds; defaults to `noProgressMs`. */
|
|
339
|
+
tailMs?: number;
|
|
336
340
|
}
|
|
337
341
|
/**
|
|
338
342
|
* Classified like the per-call protocol timeout it replaces, so the capture retry ladder retries it.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* regex, the same first-10/first-5 slicing, and the same `console.warn`
|
|
27
27
|
* prefix.
|
|
28
28
|
*/
|
|
29
|
-
import { type CaptureSession, type EngineConfig, type SubTimelineWaitMemo } from "@hyperframes/engine";
|
|
29
|
+
import { type CaptureSession, type CompositionLengthSource, type EngineConfig, type SubTimelineWaitMemo } from "@hyperframes/engine";
|
|
30
30
|
import type { CompiledComposition } from "../../htmlCompiler.js";
|
|
31
31
|
import { type FileServerHandle } from "../../fileServer.js";
|
|
32
32
|
import type { ProducerLogger } from "../../../logger.js";
|
|
@@ -67,6 +67,8 @@ export interface ProbeStageResult {
|
|
|
67
67
|
lastBrowserConsole: string[];
|
|
68
68
|
/** Composition duration (post-probe). Guaranteed > 0 — the stage throws on <= 0. */
|
|
69
69
|
duration: number;
|
|
70
|
+
/** Which input set `duration`; null when the runtime did not say. */
|
|
71
|
+
lengthSource: ReportedLengthSource | null;
|
|
70
72
|
totalFrames: number;
|
|
71
73
|
/** Wall-clock ms for the entire probe phase (near-zero when `needsBrowser` was false). */
|
|
72
74
|
browserProbeMs: number;
|
|
@@ -113,6 +115,16 @@ export declare function probeRequiresBrowser(args: {
|
|
|
113
115
|
hasVariableMedia: boolean;
|
|
114
116
|
hasInsertedMedia: boolean;
|
|
115
117
|
}): boolean;
|
|
118
|
+
export type ReportedLengthSource = {
|
|
119
|
+
from: Exclude<CompositionLengthSource["from"], "media">;
|
|
120
|
+
} | {
|
|
121
|
+
from: "media";
|
|
122
|
+
tag: string;
|
|
123
|
+
trimmed: boolean;
|
|
124
|
+
srcHash: string;
|
|
125
|
+
};
|
|
126
|
+
/** The length source with a media clip's src reduced to a hash of its file name, so no path leaves the render. */
|
|
127
|
+
export declare function reportedLengthSource(source: CompositionLengthSource | null): ReportedLengthSource | null;
|
|
116
128
|
export declare function runProbeStage(input: ProbeStageInput): Promise<ProbeStageResult>;
|
|
117
129
|
/**
|
|
118
130
|
* Preview/render parity for `muted` media: the runtime keeps muted elements
|
|
@@ -567,10 +567,11 @@ export declare function captureAttemptMadeProgress(attemptTargetFrameCount: numb
|
|
|
567
567
|
export declare function resetCaptureAttemptProgress(job: {
|
|
568
568
|
framesRendered?: number;
|
|
569
569
|
}): void;
|
|
570
|
-
/** The no-progress rule for capture stages that have no watchdog of their own: disk, HDR, chunk.
|
|
570
|
+
/** The no-progress rule for capture stages that have no watchdog of their own: disk, HDR, chunk. Once
|
|
571
|
+
* `totalFrames` have landed the frame count can no longer change, so the stage is judged on the tail budget. */
|
|
571
572
|
export declare function frameStallRule(job: {
|
|
572
573
|
framesRendered?: number;
|
|
573
|
-
}): StageStallRule;
|
|
574
|
+
}, totalFrames: number): StageStallRule;
|
|
574
575
|
export declare function isRecoverableParallelCaptureError(error: unknown): boolean;
|
|
575
576
|
/**
|
|
576
577
|
* Turn a cryptic memory-exhaustion failure (V8 `Set maximum size exceeded`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.216",
|
|
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.11.0",
|
|
37
37
|
"puppeteer-core": "^25.11.0",
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/core": "0.8.216",
|
|
39
|
+
"@hyperframes/engine": "0.8.216",
|
|
40
|
+
"@hyperframes/lint": "0.8.216",
|
|
41
|
+
"@hyperframes/studio-server": "0.8.216",
|
|
42
|
+
"@hyperframes/parsers": "0.8.216"
|
|
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.216"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|