@hyperframes/producer 0.8.203 → 0.8.205
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 +396 -35
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +17 -17
- package/dist/index.js +196 -76
- package/dist/public-server.js +166 -53
- package/dist/services/render/observability.d.ts +23 -1
- package/dist/services/render/stages/captureStage.d.ts +3 -0
- package/dist/services/renderOrchestrator.d.ts +6 -1
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -1259,6 +1259,7 @@ function buildRenderErrorDetails(input) {
|
|
|
1259
1259
|
|
|
1260
1260
|
// src/services/render/renderEventPublisher.ts
|
|
1261
1261
|
import { cloneCaptureWarnings } from "@hyperframes/engine";
|
|
1262
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
1262
1263
|
function snapshotJob(job) {
|
|
1263
1264
|
return {
|
|
1264
1265
|
...job,
|
|
@@ -1288,7 +1289,15 @@ var OrderedRenderEventPublisher = class {
|
|
|
1288
1289
|
});
|
|
1289
1290
|
}
|
|
1290
1291
|
async flush() {
|
|
1291
|
-
|
|
1292
|
+
let timer;
|
|
1293
|
+
const drained = await Promise.race([
|
|
1294
|
+
this.tail.then(() => true),
|
|
1295
|
+
new Promise((resolve10) => {
|
|
1296
|
+
timer = setTimeout(() => resolve10(false), FLUSH_TIMEOUT_MS);
|
|
1297
|
+
})
|
|
1298
|
+
]);
|
|
1299
|
+
clearTimeout(timer);
|
|
1300
|
+
if (!drained) this.log.warn("Render event sink did not drain; continuing without it");
|
|
1292
1301
|
}
|
|
1293
1302
|
};
|
|
1294
1303
|
function publishRenderFailure(job, failure, onProgress) {
|
|
@@ -5120,6 +5129,7 @@ function shouldFallbackToScreenshotAfterCalibrationError(error) {
|
|
|
5120
5129
|
// src/services/render/observability.ts
|
|
5121
5130
|
import { createHash as createHash3 } from "node:crypto";
|
|
5122
5131
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
5132
|
+
import { CaptureFailure as CaptureFailure2 } from "@hyperframes/engine";
|
|
5123
5133
|
var MAX_EVENTS = 160;
|
|
5124
5134
|
var ALLOWED_STRING_DATA_KEYS = /* @__PURE__ */ new Set([
|
|
5125
5135
|
"browserGpuMode",
|
|
@@ -5352,6 +5362,56 @@ function heartbeatTargetMs(index) {
|
|
|
5352
5362
|
const overflow = index - HEARTBEAT_RAMP_MS.length + 1;
|
|
5353
5363
|
return HEARTBEAT_RAMP_END_MS + overflow * HEARTBEAT_REPEAT_MS;
|
|
5354
5364
|
}
|
|
5365
|
+
var RenderStageStalledError = class extends CaptureFailure2 {
|
|
5366
|
+
code = "RENDER_STAGE_STALLED";
|
|
5367
|
+
owner = "system";
|
|
5368
|
+
retryable = false;
|
|
5369
|
+
constructor(phase, silentMs, progress) {
|
|
5370
|
+
super({
|
|
5371
|
+
kind: "protocol_timeout",
|
|
5372
|
+
message: `[Render] ${phase} stalled: no progress for ${silentMs}ms (stuck at ${progress}).`
|
|
5373
|
+
});
|
|
5374
|
+
this.name = "RenderStageStalledError";
|
|
5375
|
+
}
|
|
5376
|
+
};
|
|
5377
|
+
var STALL_POLL_MS = 5e3;
|
|
5378
|
+
function watchStall(phase, rule) {
|
|
5379
|
+
let last = rule.progress();
|
|
5380
|
+
let budgetMs = rule.firstProgressMs;
|
|
5381
|
+
let lastChangeAt = Date.now();
|
|
5382
|
+
let attempt;
|
|
5383
|
+
let timer;
|
|
5384
|
+
const restartWindow = (ms) => {
|
|
5385
|
+
last = rule.progress();
|
|
5386
|
+
budgetMs = ms;
|
|
5387
|
+
lastChangeAt = Date.now();
|
|
5388
|
+
};
|
|
5389
|
+
const tripped = new Promise((_, reject) => {
|
|
5390
|
+
timer = setInterval(
|
|
5391
|
+
() => {
|
|
5392
|
+
const current = rule.progress();
|
|
5393
|
+
if (current !== last) return restartWindow(rule.noProgressMs);
|
|
5394
|
+
if (Date.now() - lastChangeAt < budgetMs) return;
|
|
5395
|
+
const error = new RenderStageStalledError(phase, budgetMs, current);
|
|
5396
|
+
if (attempt && !attempt.signal.aborted) {
|
|
5397
|
+
attempt.abort(error);
|
|
5398
|
+
return restartWindow(rule.noProgressMs);
|
|
5399
|
+
}
|
|
5400
|
+
clearInterval(timer);
|
|
5401
|
+
reject(error);
|
|
5402
|
+
},
|
|
5403
|
+
Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs)
|
|
5404
|
+
);
|
|
5405
|
+
});
|
|
5406
|
+
const openAttempt = (parent) => {
|
|
5407
|
+
attempt = new AbortController();
|
|
5408
|
+
restartWindow(rule.firstProgressMs);
|
|
5409
|
+
return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
|
|
5410
|
+
};
|
|
5411
|
+
const stalledBy = () => attempt?.signal.aborted ? attempt.signal.reason : void 0;
|
|
5412
|
+
return { tripped, openAttempt, stalledBy, stop: () => clearInterval(timer) };
|
|
5413
|
+
}
|
|
5414
|
+
var openUnwatchedAttempt = (parent) => parent ?? new AbortController().signal;
|
|
5355
5415
|
async function observeRenderStage(recorder, phase, data, fn, options = {}) {
|
|
5356
5416
|
const heartbeatMessage = options.heartbeatMessage ?? "stage still running";
|
|
5357
5417
|
const startedAt = recorder.stageStart(phase, data);
|
|
@@ -5373,15 +5433,19 @@ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
|
|
|
5373
5433
|
heartbeatTimer.unref?.();
|
|
5374
5434
|
};
|
|
5375
5435
|
scheduleNextHeartbeat();
|
|
5436
|
+
const stall = options.stall ? watchStall(phase, options.stall) : void 0;
|
|
5376
5437
|
const clearHeartbeats = () => {
|
|
5377
5438
|
clearTimeout(heartbeatTimer);
|
|
5439
|
+
stall?.stop();
|
|
5378
5440
|
};
|
|
5379
5441
|
try {
|
|
5380
|
-
const
|
|
5442
|
+
const run = fn(stall?.openAttempt ?? openUnwatchedAttempt);
|
|
5443
|
+
const result = await (stall ? Promise.race([run, stall.tripped]) : run);
|
|
5381
5444
|
clearHeartbeats();
|
|
5382
5445
|
recorder.stageEnd(phase, startedAt, data);
|
|
5383
5446
|
return result;
|
|
5384
|
-
} catch (
|
|
5447
|
+
} catch (caught) {
|
|
5448
|
+
const error = stall?.stalledBy() ?? caught;
|
|
5385
5449
|
clearHeartbeats();
|
|
5386
5450
|
recorder.stageError(phase, startedAt, error, data);
|
|
5387
5451
|
throw error;
|
|
@@ -5565,6 +5629,8 @@ import {
|
|
|
5565
5629
|
readDeclaredDefaults,
|
|
5566
5630
|
parseHostVariableValues,
|
|
5567
5631
|
headStyleRuns,
|
|
5632
|
+
isAlwaysAppliedStyle,
|
|
5633
|
+
dedupeFontFaceRules,
|
|
5568
5634
|
inlineScriptRuns,
|
|
5569
5635
|
parsesAsScript,
|
|
5570
5636
|
styleElementsFor,
|
|
@@ -5857,12 +5923,12 @@ function resolveClipWindowSeconds(img, compositionDuration) {
|
|
|
5857
5923
|
}
|
|
5858
5924
|
return null;
|
|
5859
5925
|
}
|
|
5860
|
-
function resolvePreparedPlayback(metadata,
|
|
5926
|
+
function resolvePreparedPlayback(metadata, loopOverride, windowSeconds) {
|
|
5861
5927
|
const gifDuration = metadata.durationSeconds > 0 ? metadata.durationSeconds : null;
|
|
5862
5928
|
let loopIterations = 1;
|
|
5863
|
-
if (
|
|
5929
|
+
if (resolveLoop(metadata, loopOverride)) {
|
|
5864
5930
|
loopIterations = windowSeconds != null && gifDuration != null ? Math.min(MAX_LOOP_ITERATIONS, Math.max(1, Math.ceil(windowSeconds / gifDuration))) : 1;
|
|
5865
|
-
} else if (metadata.loopCount != null && metadata.loopCount > 1) {
|
|
5931
|
+
} else if (loopOverride == null && metadata.loopCount != null && metadata.loopCount > 1) {
|
|
5866
5932
|
loopIterations = Math.min(MAX_LOOP_ITERATIONS, metadata.loopCount);
|
|
5867
5933
|
}
|
|
5868
5934
|
let padSeconds = 0;
|
|
@@ -6029,9 +6095,14 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6029
6095
|
const bytes = readFileSync5(sourcePath);
|
|
6030
6096
|
const metadata = parseAnimatedGifMetadata(bytes);
|
|
6031
6097
|
if (!metadata?.animated) continue;
|
|
6032
|
-
const
|
|
6098
|
+
const loopOverride = readLoopOverride(img);
|
|
6099
|
+
const loop = resolveLoop(metadata, loopOverride);
|
|
6033
6100
|
const windowSeconds = resolveClipWindowSeconds(img, compositionDuration);
|
|
6034
|
-
const { loopIterations, padSeconds } = resolvePreparedPlayback(
|
|
6101
|
+
const { loopIterations, padSeconds } = resolvePreparedPlayback(
|
|
6102
|
+
metadata,
|
|
6103
|
+
loopOverride,
|
|
6104
|
+
windowSeconds
|
|
6105
|
+
);
|
|
6035
6106
|
const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
|
|
6036
6107
|
const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
|
|
6037
6108
|
const cachePath = join13(cacheDir, filename);
|
|
@@ -6697,6 +6768,11 @@ function coalesceHeadStylesAndBodyScripts(html) {
|
|
|
6697
6768
|
const head = document2.querySelector("head");
|
|
6698
6769
|
const body = document2.querySelector("body");
|
|
6699
6770
|
if (!head && !body) return html;
|
|
6771
|
+
const liveStyles = [...document2.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
|
|
6772
|
+
const dedupedStyles = dedupeFontFaceRules(liveStyles.map((el) => el.textContent || ""));
|
|
6773
|
+
liveStyles.forEach((el, i) => {
|
|
6774
|
+
el.textContent = dedupedStyles[i] ?? "";
|
|
6775
|
+
});
|
|
6700
6776
|
const styleEls = head ? Array.from(head.querySelectorAll("style")) : [];
|
|
6701
6777
|
const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
|
|
6702
6778
|
for (const run of styleEls.length > 1 ? headStyleRuns(styleEls) : []) {
|
|
@@ -7536,9 +7612,10 @@ async function discoverMediaFromBrowser(page) {
|
|
|
7536
7612
|
const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
|
|
7537
7613
|
const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
|
|
7538
7614
|
if (!id) return;
|
|
7539
|
-
const
|
|
7540
|
-
const
|
|
7541
|
-
const
|
|
7615
|
+
const resolvedSrc = htmlEl.currentSrc || htmlEl.src || htmlEl.getAttribute("src") || "";
|
|
7616
|
+
const withoutQueryAndHash = (url) => url.replace(/[?#][\s\S]*$/, "");
|
|
7617
|
+
const isPageItself = withoutQueryAndHash(resolvedSrc) === withoutQueryAndHash(location.href);
|
|
7618
|
+
const src = isPageItself ? "" : resolvedSrc;
|
|
7542
7619
|
const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
|
|
7543
7620
|
const endRaw = htmlEl.getAttribute("data-end");
|
|
7544
7621
|
const durationRaw = htmlEl.getAttribute("data-duration");
|
|
@@ -9808,6 +9885,7 @@ async function runCaptureStage(input) {
|
|
|
9808
9885
|
interleaveFrames: input.interleaveFrames,
|
|
9809
9886
|
dedupPerfs,
|
|
9810
9887
|
pageErrorLines: workerPageErrors,
|
|
9888
|
+
openAttempt: input.openAttempt,
|
|
9811
9889
|
onProgress: (progress) => {
|
|
9812
9890
|
if (progress.latestWorkerPhase) {
|
|
9813
9891
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
@@ -9879,6 +9957,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
|
|
|
9879
9957
|
let transientRetriesUsed = 0;
|
|
9880
9958
|
let resumeFrom = 0;
|
|
9881
9959
|
while (true) {
|
|
9960
|
+
const attempt = input.openAttempt?.();
|
|
9961
|
+
const assertAttemptActive = () => {
|
|
9962
|
+
assertNotAborted();
|
|
9963
|
+
attempt?.throwIfAborted();
|
|
9964
|
+
};
|
|
9882
9965
|
const videoInjector = createRenderVideoFrameInjector();
|
|
9883
9966
|
const session = probeSession ?? await createCaptureSession5(
|
|
9884
9967
|
fileServer.url,
|
|
@@ -9894,10 +9977,10 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
|
|
|
9894
9977
|
probeSession = null;
|
|
9895
9978
|
}
|
|
9896
9979
|
await ensureSessionReady(session);
|
|
9897
|
-
|
|
9980
|
+
assertAttemptActive();
|
|
9898
9981
|
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9899
9982
|
await captureSessionFrames(session, {
|
|
9900
|
-
input,
|
|
9983
|
+
input: { ...input, assertNotAborted: assertAttemptActive },
|
|
9901
9984
|
rangeStart,
|
|
9902
9985
|
rangeEnd,
|
|
9903
9986
|
resumeFrom,
|
|
@@ -11757,7 +11840,17 @@ function captureAttemptMadeProgress(attemptTargetFrameCount, remainingFrameCount
|
|
|
11757
11840
|
function resetCaptureAttemptProgress(job) {
|
|
11758
11841
|
job.framesRendered = 0;
|
|
11759
11842
|
}
|
|
11843
|
+
var FIRST_FRAME_STALL_MS = 3e5;
|
|
11844
|
+
function frameStallRule(job) {
|
|
11845
|
+
const noProgressMs = resolveCaptureStallTimeoutMs();
|
|
11846
|
+
return {
|
|
11847
|
+
progress: () => job.framesRendered ?? 0,
|
|
11848
|
+
noProgressMs,
|
|
11849
|
+
firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
|
|
11850
|
+
};
|
|
11851
|
+
}
|
|
11760
11852
|
function isRecoverableParallelCaptureError(error) {
|
|
11853
|
+
if (error instanceof RenderStageStalledError) return true;
|
|
11761
11854
|
const message = normalizeErrorMessage(error);
|
|
11762
11855
|
if (!message.includes("[Parallel] Capture failed")) return false;
|
|
11763
11856
|
const kind = classifyCaptureFailure3(error).kind;
|
|
@@ -11820,8 +11913,10 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11820
11913
|
)
|
|
11821
11914
|
];
|
|
11822
11915
|
options.dedupPerfs.length = 0;
|
|
11916
|
+
let attemptSignal = options.abortSignal;
|
|
11823
11917
|
try {
|
|
11824
11918
|
for (const tasks of batches) {
|
|
11919
|
+
attemptSignal = options.openAttempt?.(options.abortSignal) ?? options.abortSignal;
|
|
11825
11920
|
const capturedBeforeBatch = countCapturedFrames(
|
|
11826
11921
|
options.totalFrames,
|
|
11827
11922
|
options.framesDir,
|
|
@@ -11834,7 +11929,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11834
11929
|
tasks,
|
|
11835
11930
|
options.captureOptions,
|
|
11836
11931
|
options.createBeforeCaptureHook,
|
|
11837
|
-
|
|
11932
|
+
attemptSignal,
|
|
11838
11933
|
options.onProgress ? (progress) => {
|
|
11839
11934
|
options.onProgress?.({
|
|
11840
11935
|
...progress,
|
|
@@ -11882,7 +11977,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11882
11977
|
currentWorkers = nextWorkers;
|
|
11883
11978
|
missingRanges = remaining;
|
|
11884
11979
|
attempt++;
|
|
11885
|
-
} catch (
|
|
11980
|
+
} catch (caught) {
|
|
11981
|
+
const error = attemptSignal?.aborted && !options.abortSignal?.aborted ? attemptSignal.reason : caught;
|
|
11886
11982
|
const failure = classifyCaptureFailure3(error, { signal: options.abortSignal });
|
|
11887
11983
|
if (failure.kind === "cancelled") {
|
|
11888
11984
|
throw error;
|
|
@@ -13551,37 +13647,44 @@ async function executeRenderPipeline(input) {
|
|
|
13551
13647
|
observability,
|
|
13552
13648
|
"capture_hdr_layered",
|
|
13553
13649
|
captureStageObservationData({ hasHdrContent }),
|
|
13554
|
-
() =>
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
13569
|
-
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13650
|
+
(openAttempt) => {
|
|
13651
|
+
const attempt = openAttempt(executionSignal);
|
|
13652
|
+
return runCaptureHdrStage({
|
|
13653
|
+
job,
|
|
13654
|
+
cfg,
|
|
13655
|
+
plan: layeredPlan,
|
|
13656
|
+
log,
|
|
13657
|
+
projectDir,
|
|
13658
|
+
compiledDir,
|
|
13659
|
+
framesDir,
|
|
13660
|
+
videoOnlyPath,
|
|
13661
|
+
width,
|
|
13662
|
+
height,
|
|
13663
|
+
totalFrames,
|
|
13664
|
+
composition,
|
|
13665
|
+
hasHdrContent,
|
|
13666
|
+
effectiveHdr,
|
|
13667
|
+
nativeHdrVideoIds,
|
|
13668
|
+
nativeHdrImageIds,
|
|
13669
|
+
videoTransfers,
|
|
13670
|
+
imageTransfers,
|
|
13671
|
+
hdrImageSrcPaths,
|
|
13672
|
+
preset,
|
|
13673
|
+
effectiveQuality,
|
|
13674
|
+
effectiveBitrate,
|
|
13675
|
+
fileServer: activeFileServer,
|
|
13676
|
+
buildCaptureOptions,
|
|
13677
|
+
createRenderVideoFrameInjector,
|
|
13678
|
+
hdrDiagnostics,
|
|
13679
|
+
abortSignal: attempt,
|
|
13680
|
+
assertNotAborted: () => {
|
|
13681
|
+
assertNotAborted();
|
|
13682
|
+
attempt.throwIfAborted();
|
|
13683
|
+
},
|
|
13684
|
+
onProgress
|
|
13685
|
+
});
|
|
13686
|
+
},
|
|
13687
|
+
{ stall: frameStallRule(job) }
|
|
13585
13688
|
);
|
|
13586
13689
|
lastBrowserConsole = hdrRes.lastBrowserConsole;
|
|
13587
13690
|
layeredCaptureWarnings.push(...hdrRes.warnings);
|
|
@@ -13946,7 +14049,7 @@ async function executeRenderPipeline(input) {
|
|
|
13946
14049
|
observability,
|
|
13947
14050
|
"capture_disk",
|
|
13948
14051
|
captureStageObservationData({ needsAlpha: diskPlan.needsAlpha }),
|
|
13949
|
-
() => runCaptureStage({
|
|
14052
|
+
(openAttempt) => runCaptureStage({
|
|
13950
14053
|
fileServer: activeFileServer,
|
|
13951
14054
|
workDir,
|
|
13952
14055
|
framesDir,
|
|
@@ -13964,8 +14067,10 @@ async function executeRenderPipeline(input) {
|
|
|
13964
14067
|
assertNotAborted,
|
|
13965
14068
|
onProgress,
|
|
13966
14069
|
onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
|
|
13967
|
-
interleaveFrames: overlap !== null
|
|
13968
|
-
|
|
14070
|
+
interleaveFrames: overlap !== null,
|
|
14071
|
+
openAttempt
|
|
14072
|
+
}),
|
|
14073
|
+
{ stall: frameStallRule(job) }
|
|
13969
14074
|
);
|
|
13970
14075
|
let captureRes;
|
|
13971
14076
|
try {
|
|
@@ -14849,6 +14954,7 @@ function renderConfigFromRequest(request, runtime = {}) {
|
|
|
14849
14954
|
|
|
14850
14955
|
// src/server.ts
|
|
14851
14956
|
var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
|
|
14957
|
+
var SSE_KEEPALIVE_MS = 3e4;
|
|
14852
14958
|
var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
14853
14959
|
"ASSET_MEDIA_TYPE_MISMATCH",
|
|
14854
14960
|
"NOT_MEDIA_PAYLOAD",
|
|
@@ -14856,7 +14962,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
14856
14962
|
"VIDEO_SOURCE_UNRENDERABLE",
|
|
14857
14963
|
"VIDEO_EXTRACTION_FAILED",
|
|
14858
14964
|
"ENCODER_INTERRUPTED",
|
|
14859
|
-
"RENDER_TOO_SLOW"
|
|
14965
|
+
"RENDER_TOO_SLOW",
|
|
14966
|
+
"RENDER_STAGE_STALLED"
|
|
14860
14967
|
]);
|
|
14861
14968
|
function extractSafeRenderErrorCode(error) {
|
|
14862
14969
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
@@ -15370,15 +15477,21 @@ function createRenderHandlers(options = {}) {
|
|
|
15370
15477
|
})
|
|
15371
15478
|
});
|
|
15372
15479
|
}
|
|
15480
|
+
const reportProgress = createSseProgressReporter(stream, requestId);
|
|
15481
|
+
const keepalive = setInterval(() => {
|
|
15482
|
+
Promise.resolve(reportProgress(job, job.currentStage)).catch(() => {
|
|
15483
|
+
});
|
|
15484
|
+
}, SSE_KEEPALIVE_MS);
|
|
15373
15485
|
const release = await renderSemaphore.acquire();
|
|
15374
15486
|
try {
|
|
15375
15487
|
await executeRenderJob(
|
|
15376
15488
|
job,
|
|
15377
15489
|
input.projectDir,
|
|
15378
15490
|
absoluteOutputPath,
|
|
15379
|
-
|
|
15491
|
+
reportProgress,
|
|
15380
15492
|
abortController.signal
|
|
15381
|
-
);
|
|
15493
|
+
).finally(release);
|
|
15494
|
+
clearInterval(keepalive);
|
|
15382
15495
|
const fileSize = outputFileSize(absoluteOutputPath);
|
|
15383
15496
|
const outputToken = store.register(absoluteOutputPath);
|
|
15384
15497
|
const outputUrl = `${outputUrlPrefix}/${outputToken}`;
|
|
@@ -15398,6 +15511,7 @@ function createRenderHandlers(options = {}) {
|
|
|
15398
15511
|
})
|
|
15399
15512
|
});
|
|
15400
15513
|
} catch (error) {
|
|
15514
|
+
clearInterval(keepalive);
|
|
15401
15515
|
await writeRenderStreamFailure({
|
|
15402
15516
|
error,
|
|
15403
15517
|
job,
|
|
@@ -15407,7 +15521,6 @@ function createRenderHandlers(options = {}) {
|
|
|
15407
15521
|
log
|
|
15408
15522
|
});
|
|
15409
15523
|
} finally {
|
|
15410
|
-
release();
|
|
15411
15524
|
c.req.raw.signal.removeEventListener("abort", onRequestAbort);
|
|
15412
15525
|
cleanupTempDir(cleanupProjectDir, log);
|
|
15413
15526
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CaptureFailure } from "@hyperframes/engine";
|
|
1
2
|
import type { ProducerLogger } from "../../logger.js";
|
|
2
3
|
export type RenderObservationStatus = "start" | "end" | "error" | "checkpoint";
|
|
3
4
|
export type RenderObservationValue = string | number | boolean | null;
|
|
@@ -323,5 +324,26 @@ export interface ObserveRenderStageOptions {
|
|
|
323
324
|
* Defaults to "stage still running".
|
|
324
325
|
*/
|
|
325
326
|
heartbeatMessage?: string;
|
|
327
|
+
stall?: StageStallRule;
|
|
326
328
|
}
|
|
327
|
-
|
|
329
|
+
/** Watches a stage's `progress()`; any change, in either direction, is progress. */
|
|
330
|
+
export interface StageStallRule {
|
|
331
|
+
progress: () => number;
|
|
332
|
+
/** Allowed silence once `progress()` has changed in the current attempt. */
|
|
333
|
+
noProgressMs: number;
|
|
334
|
+
/** Allowed silence before the current attempt's first change. */
|
|
335
|
+
firstProgressMs: number;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Classified like the per-call protocol timeout it replaces, so the capture retry ladder retries it.
|
|
339
|
+
* Not retryable on the wire: the producer already spent its retries on it.
|
|
340
|
+
*/
|
|
341
|
+
export declare class RenderStageStalledError extends CaptureFailure {
|
|
342
|
+
readonly code = "RENDER_STAGE_STALLED";
|
|
343
|
+
readonly owner: "system";
|
|
344
|
+
readonly retryable: false;
|
|
345
|
+
constructor(phase: string, silentMs: number, progress: number);
|
|
346
|
+
}
|
|
347
|
+
/** Opens a capture attempt: a fresh no-progress window, and a signal the rule aborts when that window runs out. */
|
|
348
|
+
export type OpenStallAttempt = (parent?: AbortSignal) => AbortSignal;
|
|
349
|
+
export declare function observeRenderStage<T>(recorder: RenderObservabilityRecorder, phase: string, data: RenderObservationData | undefined, fn: (openAttempt: OpenStallAttempt) => Promise<T>, options?: ObserveRenderStageOptions): Promise<T>;
|
|
@@ -35,6 +35,7 @@ import type { FileServerHandle } from "../../fileServer.js";
|
|
|
35
35
|
import type { ProducerLogger } from "../../../logger.js";
|
|
36
36
|
import { type CaptureAttemptSummary, type ProgressCallback, type RenderConfig, type RenderJob } from "../../renderOrchestrator.js";
|
|
37
37
|
import type { SdrDiskCapturePlan } from "../capturePlan.js";
|
|
38
|
+
import type { OpenStallAttempt } from "../observability.js";
|
|
38
39
|
export interface CaptureStageInput {
|
|
39
40
|
fileServer: FileServerHandle;
|
|
40
41
|
workDir: string;
|
|
@@ -85,6 +86,8 @@ export interface CaptureStageInput {
|
|
|
85
86
|
};
|
|
86
87
|
onFrameLanded?: (frame: LandedFrame) => void;
|
|
87
88
|
interleaveFrames?: boolean;
|
|
89
|
+
/** Each capture attempt opens one, so a stalled attempt is retried by the ladders below. */
|
|
90
|
+
openAttempt?: OpenStallAttempt;
|
|
88
91
|
}
|
|
89
92
|
export interface CaptureStageResult {
|
|
90
93
|
/** Final worker count after any adaptive retry. */
|
|
@@ -2,7 +2,7 @@ import { type CanvasResolution, type Fps, type FpsInput, type HfVfxCapture } fro
|
|
|
2
2
|
import { type EngineConfig, type ExtractionPhaseBreakdown, type VideoFrameFormat, type CaptureOptions, type CaptureVideoMetadataHint, type CaptureSession, type BeforeCaptureHook, type ParallelProgress, type WorkerTask, type CapturePerfSummary, type CaptureWarning, type SubTimelineWaitOutcome, type WorkerSizing, type StaticVerificationOutcome, type CaptureFailureKind, type MotionBlurOptions } from "@hyperframes/engine";
|
|
3
3
|
import { type ProducerLogger } from "../logger.js";
|
|
4
4
|
import { type RenderOutputFormat } from "./render/renderFormat.js";
|
|
5
|
-
import { type RenderCaptureObservability, type RenderObservabilitySummary } from "./render/observability.js";
|
|
5
|
+
import { type RenderCaptureObservability, type RenderObservabilitySummary, type OpenStallAttempt, type StageStallRule } from "./render/observability.js";
|
|
6
6
|
import { type HdrPerfSummary } from "./render/hdrPerf.js";
|
|
7
7
|
export declare function sampleDirectoryBytes(dir: string): number;
|
|
8
8
|
export type RenderStatus = "queued" | "preprocessing" | "rendering" | "encoding" | "assembling" | "complete" | "failed" | "cancelled";
|
|
@@ -559,6 +559,10 @@ export declare function captureAttemptMadeProgress(attemptTargetFrameCount: numb
|
|
|
559
559
|
export declare function resetCaptureAttemptProgress(job: {
|
|
560
560
|
framesRendered?: number;
|
|
561
561
|
}): void;
|
|
562
|
+
/** The no-progress rule for capture stages that have no watchdog of their own: disk, HDR, chunk. */
|
|
563
|
+
export declare function frameStallRule(job: {
|
|
564
|
+
framesRendered?: number;
|
|
565
|
+
}): StageStallRule;
|
|
562
566
|
export declare function isRecoverableParallelCaptureError(error: unknown): boolean;
|
|
563
567
|
/**
|
|
564
568
|
* Turn a cryptic memory-exhaustion failure (V8 `Set maximum size exceeded`,
|
|
@@ -599,6 +603,7 @@ export declare function executeDiskCaptureWithAdaptiveRetry(options: {
|
|
|
599
603
|
dedupPerfs: CapturePerfSummary[];
|
|
600
604
|
/** Mutated in place: the workers' page-error console lines, for the render's error count. */
|
|
601
605
|
pageErrorLines?: string[];
|
|
606
|
+
openAttempt?: OpenStallAttempt;
|
|
602
607
|
}): Promise<CaptureAttemptSummary[]>;
|
|
603
608
|
export type RenderConfigInput = Omit<RenderConfig, "fps"> & {
|
|
604
609
|
fps: FpsInput;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.205",
|
|
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/core": "0.8.
|
|
39
|
-
"@hyperframes/engine": "0.8.
|
|
40
|
-
"@hyperframes/lint": "0.8.
|
|
41
|
-
"@hyperframes/parsers": "0.8.
|
|
42
|
-
"@hyperframes/studio-server": "0.8.
|
|
38
|
+
"@hyperframes/core": "0.8.205",
|
|
39
|
+
"@hyperframes/engine": "0.8.205",
|
|
40
|
+
"@hyperframes/lint": "0.8.205",
|
|
41
|
+
"@hyperframes/parsers": "0.8.205",
|
|
42
|
+
"@hyperframes/studio-server": "0.8.205"
|
|
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.205"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|