@hyperframes/producer 0.8.202 → 0.8.204
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 +382 -30
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +65 -65
- package/dist/index.js +182 -71
- package/dist/public-server.js +149 -48
- package/dist/services/distributed/renderChunk.d.ts +2 -0
- 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;
|
|
@@ -7536,9 +7600,10 @@ async function discoverMediaFromBrowser(page) {
|
|
|
7536
7600
|
const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
|
|
7537
7601
|
const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
|
|
7538
7602
|
if (!id) return;
|
|
7539
|
-
const
|
|
7540
|
-
const
|
|
7541
|
-
const
|
|
7603
|
+
const resolvedSrc = htmlEl.currentSrc || htmlEl.src || htmlEl.getAttribute("src") || "";
|
|
7604
|
+
const withoutQueryAndHash = (url) => url.replace(/[?#][\s\S]*$/, "");
|
|
7605
|
+
const isPageItself = withoutQueryAndHash(resolvedSrc) === withoutQueryAndHash(location.href);
|
|
7606
|
+
const src = isPageItself ? "" : resolvedSrc;
|
|
7542
7607
|
const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
|
|
7543
7608
|
const endRaw = htmlEl.getAttribute("data-end");
|
|
7544
7609
|
const durationRaw = htmlEl.getAttribute("data-duration");
|
|
@@ -9808,6 +9873,7 @@ async function runCaptureStage(input) {
|
|
|
9808
9873
|
interleaveFrames: input.interleaveFrames,
|
|
9809
9874
|
dedupPerfs,
|
|
9810
9875
|
pageErrorLines: workerPageErrors,
|
|
9876
|
+
openAttempt: input.openAttempt,
|
|
9811
9877
|
onProgress: (progress) => {
|
|
9812
9878
|
if (progress.latestWorkerPhase) {
|
|
9813
9879
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
@@ -9879,6 +9945,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
|
|
|
9879
9945
|
let transientRetriesUsed = 0;
|
|
9880
9946
|
let resumeFrom = 0;
|
|
9881
9947
|
while (true) {
|
|
9948
|
+
const attempt = input.openAttempt?.();
|
|
9949
|
+
const assertAttemptActive = () => {
|
|
9950
|
+
assertNotAborted();
|
|
9951
|
+
attempt?.throwIfAborted();
|
|
9952
|
+
};
|
|
9882
9953
|
const videoInjector = createRenderVideoFrameInjector();
|
|
9883
9954
|
const session = probeSession ?? await createCaptureSession5(
|
|
9884
9955
|
fileServer.url,
|
|
@@ -9894,10 +9965,10 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
|
|
|
9894
9965
|
probeSession = null;
|
|
9895
9966
|
}
|
|
9896
9967
|
await ensureSessionReady(session);
|
|
9897
|
-
|
|
9968
|
+
assertAttemptActive();
|
|
9898
9969
|
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9899
9970
|
await captureSessionFrames(session, {
|
|
9900
|
-
input,
|
|
9971
|
+
input: { ...input, assertNotAborted: assertAttemptActive },
|
|
9901
9972
|
rangeStart,
|
|
9902
9973
|
rangeEnd,
|
|
9903
9974
|
resumeFrom,
|
|
@@ -11757,7 +11828,17 @@ function captureAttemptMadeProgress(attemptTargetFrameCount, remainingFrameCount
|
|
|
11757
11828
|
function resetCaptureAttemptProgress(job) {
|
|
11758
11829
|
job.framesRendered = 0;
|
|
11759
11830
|
}
|
|
11831
|
+
var FIRST_FRAME_STALL_MS = 3e5;
|
|
11832
|
+
function frameStallRule(job) {
|
|
11833
|
+
const noProgressMs = resolveCaptureStallTimeoutMs();
|
|
11834
|
+
return {
|
|
11835
|
+
progress: () => job.framesRendered ?? 0,
|
|
11836
|
+
noProgressMs,
|
|
11837
|
+
firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
|
|
11838
|
+
};
|
|
11839
|
+
}
|
|
11760
11840
|
function isRecoverableParallelCaptureError(error) {
|
|
11841
|
+
if (error instanceof RenderStageStalledError) return true;
|
|
11761
11842
|
const message = normalizeErrorMessage(error);
|
|
11762
11843
|
if (!message.includes("[Parallel] Capture failed")) return false;
|
|
11763
11844
|
const kind = classifyCaptureFailure3(error).kind;
|
|
@@ -11820,8 +11901,10 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11820
11901
|
)
|
|
11821
11902
|
];
|
|
11822
11903
|
options.dedupPerfs.length = 0;
|
|
11904
|
+
let attemptSignal = options.abortSignal;
|
|
11823
11905
|
try {
|
|
11824
11906
|
for (const tasks of batches) {
|
|
11907
|
+
attemptSignal = options.openAttempt?.(options.abortSignal) ?? options.abortSignal;
|
|
11825
11908
|
const capturedBeforeBatch = countCapturedFrames(
|
|
11826
11909
|
options.totalFrames,
|
|
11827
11910
|
options.framesDir,
|
|
@@ -11834,7 +11917,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11834
11917
|
tasks,
|
|
11835
11918
|
options.captureOptions,
|
|
11836
11919
|
options.createBeforeCaptureHook,
|
|
11837
|
-
|
|
11920
|
+
attemptSignal,
|
|
11838
11921
|
options.onProgress ? (progress) => {
|
|
11839
11922
|
options.onProgress?.({
|
|
11840
11923
|
...progress,
|
|
@@ -11882,7 +11965,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11882
11965
|
currentWorkers = nextWorkers;
|
|
11883
11966
|
missingRanges = remaining;
|
|
11884
11967
|
attempt++;
|
|
11885
|
-
} catch (
|
|
11968
|
+
} catch (caught) {
|
|
11969
|
+
const error = attemptSignal?.aborted && !options.abortSignal?.aborted ? attemptSignal.reason : caught;
|
|
11886
11970
|
const failure = classifyCaptureFailure3(error, { signal: options.abortSignal });
|
|
11887
11971
|
if (failure.kind === "cancelled") {
|
|
11888
11972
|
throw error;
|
|
@@ -13551,37 +13635,44 @@ async function executeRenderPipeline(input) {
|
|
|
13551
13635
|
observability,
|
|
13552
13636
|
"capture_hdr_layered",
|
|
13553
13637
|
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
|
-
|
|
13638
|
+
(openAttempt) => {
|
|
13639
|
+
const attempt = openAttempt(executionSignal);
|
|
13640
|
+
return runCaptureHdrStage({
|
|
13641
|
+
job,
|
|
13642
|
+
cfg,
|
|
13643
|
+
plan: layeredPlan,
|
|
13644
|
+
log,
|
|
13645
|
+
projectDir,
|
|
13646
|
+
compiledDir,
|
|
13647
|
+
framesDir,
|
|
13648
|
+
videoOnlyPath,
|
|
13649
|
+
width,
|
|
13650
|
+
height,
|
|
13651
|
+
totalFrames,
|
|
13652
|
+
composition,
|
|
13653
|
+
hasHdrContent,
|
|
13654
|
+
effectiveHdr,
|
|
13655
|
+
nativeHdrVideoIds,
|
|
13656
|
+
nativeHdrImageIds,
|
|
13657
|
+
videoTransfers,
|
|
13658
|
+
imageTransfers,
|
|
13659
|
+
hdrImageSrcPaths,
|
|
13660
|
+
preset,
|
|
13661
|
+
effectiveQuality,
|
|
13662
|
+
effectiveBitrate,
|
|
13663
|
+
fileServer: activeFileServer,
|
|
13664
|
+
buildCaptureOptions,
|
|
13665
|
+
createRenderVideoFrameInjector,
|
|
13666
|
+
hdrDiagnostics,
|
|
13667
|
+
abortSignal: attempt,
|
|
13668
|
+
assertNotAborted: () => {
|
|
13669
|
+
assertNotAborted();
|
|
13670
|
+
attempt.throwIfAborted();
|
|
13671
|
+
},
|
|
13672
|
+
onProgress
|
|
13673
|
+
});
|
|
13674
|
+
},
|
|
13675
|
+
{ stall: frameStallRule(job) }
|
|
13585
13676
|
);
|
|
13586
13677
|
lastBrowserConsole = hdrRes.lastBrowserConsole;
|
|
13587
13678
|
layeredCaptureWarnings.push(...hdrRes.warnings);
|
|
@@ -13946,7 +14037,7 @@ async function executeRenderPipeline(input) {
|
|
|
13946
14037
|
observability,
|
|
13947
14038
|
"capture_disk",
|
|
13948
14039
|
captureStageObservationData({ needsAlpha: diskPlan.needsAlpha }),
|
|
13949
|
-
() => runCaptureStage({
|
|
14040
|
+
(openAttempt) => runCaptureStage({
|
|
13950
14041
|
fileServer: activeFileServer,
|
|
13951
14042
|
workDir,
|
|
13952
14043
|
framesDir,
|
|
@@ -13964,8 +14055,10 @@ async function executeRenderPipeline(input) {
|
|
|
13964
14055
|
assertNotAborted,
|
|
13965
14056
|
onProgress,
|
|
13966
14057
|
onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
|
|
13967
|
-
interleaveFrames: overlap !== null
|
|
13968
|
-
|
|
14058
|
+
interleaveFrames: overlap !== null,
|
|
14059
|
+
openAttempt
|
|
14060
|
+
}),
|
|
14061
|
+
{ stall: frameStallRule(job) }
|
|
13969
14062
|
);
|
|
13970
14063
|
let captureRes;
|
|
13971
14064
|
try {
|
|
@@ -14849,6 +14942,7 @@ function renderConfigFromRequest(request, runtime = {}) {
|
|
|
14849
14942
|
|
|
14850
14943
|
// src/server.ts
|
|
14851
14944
|
var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
|
|
14945
|
+
var SSE_KEEPALIVE_MS = 3e4;
|
|
14852
14946
|
var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
14853
14947
|
"ASSET_MEDIA_TYPE_MISMATCH",
|
|
14854
14948
|
"NOT_MEDIA_PAYLOAD",
|
|
@@ -14856,7 +14950,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
14856
14950
|
"VIDEO_SOURCE_UNRENDERABLE",
|
|
14857
14951
|
"VIDEO_EXTRACTION_FAILED",
|
|
14858
14952
|
"ENCODER_INTERRUPTED",
|
|
14859
|
-
"RENDER_TOO_SLOW"
|
|
14953
|
+
"RENDER_TOO_SLOW",
|
|
14954
|
+
"RENDER_STAGE_STALLED"
|
|
14860
14955
|
]);
|
|
14861
14956
|
function extractSafeRenderErrorCode(error) {
|
|
14862
14957
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
@@ -15370,15 +15465,21 @@ function createRenderHandlers(options = {}) {
|
|
|
15370
15465
|
})
|
|
15371
15466
|
});
|
|
15372
15467
|
}
|
|
15468
|
+
const reportProgress = createSseProgressReporter(stream, requestId);
|
|
15469
|
+
const keepalive = setInterval(() => {
|
|
15470
|
+
Promise.resolve(reportProgress(job, job.currentStage)).catch(() => {
|
|
15471
|
+
});
|
|
15472
|
+
}, SSE_KEEPALIVE_MS);
|
|
15373
15473
|
const release = await renderSemaphore.acquire();
|
|
15374
15474
|
try {
|
|
15375
15475
|
await executeRenderJob(
|
|
15376
15476
|
job,
|
|
15377
15477
|
input.projectDir,
|
|
15378
15478
|
absoluteOutputPath,
|
|
15379
|
-
|
|
15479
|
+
reportProgress,
|
|
15380
15480
|
abortController.signal
|
|
15381
|
-
);
|
|
15481
|
+
).finally(release);
|
|
15482
|
+
clearInterval(keepalive);
|
|
15382
15483
|
const fileSize = outputFileSize(absoluteOutputPath);
|
|
15383
15484
|
const outputToken = store.register(absoluteOutputPath);
|
|
15384
15485
|
const outputUrl = `${outputUrlPrefix}/${outputToken}`;
|
|
@@ -15398,6 +15499,7 @@ function createRenderHandlers(options = {}) {
|
|
|
15398
15499
|
})
|
|
15399
15500
|
});
|
|
15400
15501
|
} catch (error) {
|
|
15502
|
+
clearInterval(keepalive);
|
|
15401
15503
|
await writeRenderStreamFailure({
|
|
15402
15504
|
error,
|
|
15403
15505
|
job,
|
|
@@ -15407,7 +15509,6 @@ function createRenderHandlers(options = {}) {
|
|
|
15407
15509
|
log
|
|
15408
15510
|
});
|
|
15409
15511
|
} finally {
|
|
15410
|
-
release();
|
|
15411
15512
|
c.req.raw.signal.removeEventListener("abort", onRequestAbort);
|
|
15412
15513
|
cleanupTempDir(cleanupProjectDir, log);
|
|
15413
15514
|
}
|
|
@@ -115,7 +115,9 @@ export interface RenderChunkOptions {
|
|
|
115
115
|
* this many, one browser per 20 frames; never lowers it.
|
|
116
116
|
*/
|
|
117
117
|
maxCaptureWorkers?: number;
|
|
118
|
+
interleaveCapture?: boolean;
|
|
118
119
|
}
|
|
120
|
+
export declare function interleavesCapture(overlapping: boolean, options?: RenderChunkOptions): boolean;
|
|
119
121
|
/** Capture browsers for a chunk: the machine-wide sizing, raised by the runtime's budget. */
|
|
120
122
|
export declare function resolveChunkCaptureWorkers(framesInChunk: number, machineSized: number, maxCaptureWorkers: number | undefined): number;
|
|
121
123
|
/** The frames a chunk renders: its slice, or a validated sub-range of it. */
|
|
@@ -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.204",
|
|
4
4
|
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"postcss": "^8.4.0",
|
|
36
36
|
"puppeteer": "^25.2.1",
|
|
37
37
|
"puppeteer-core": "^25.2.1",
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/engine": "0.8.204",
|
|
39
|
+
"@hyperframes/lint": "0.8.204",
|
|
40
|
+
"@hyperframes/core": "0.8.204",
|
|
41
|
+
"@hyperframes/parsers": "0.8.204",
|
|
42
|
+
"@hyperframes/studio-server": "0.8.204"
|
|
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.204"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|