@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.
@@ -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
- await this.tail;
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 result = await fn();
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 (error) {
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, loop, windowSeconds) {
5926
+ function resolvePreparedPlayback(metadata, loopOverride, windowSeconds) {
5861
5927
  const gifDuration = metadata.durationSeconds > 0 ? metadata.durationSeconds : null;
5862
5928
  let loopIterations = 1;
5863
- if (loop) {
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 loop = resolveLoop(metadata, readLoopOverride(img));
6098
+ const loopOverride = readLoopOverride(img);
6099
+ const loop = resolveLoop(metadata, loopOverride);
6033
6100
  const windowSeconds = resolveClipWindowSeconds(img, compositionDuration);
6034
- const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
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 ownSrc = htmlEl.getAttribute("src");
7540
- const blankSrc = ownSrc !== null && ownSrc.trim() === "" && !htmlEl.querySelector("source[src]");
7541
- const src = blankSrc ? "" : htmlEl.currentSrc || htmlEl.src || ownSrc || "";
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
- assertNotAborted();
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
- options.abortSignal,
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 (error) {
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
- () => runCaptureHdrStage({
13555
- job,
13556
- cfg,
13557
- plan: layeredPlan,
13558
- log,
13559
- projectDir,
13560
- compiledDir,
13561
- framesDir,
13562
- videoOnlyPath,
13563
- width,
13564
- height,
13565
- totalFrames,
13566
- composition,
13567
- hasHdrContent,
13568
- effectiveHdr,
13569
- nativeHdrVideoIds,
13570
- nativeHdrImageIds,
13571
- videoTransfers,
13572
- imageTransfers,
13573
- hdrImageSrcPaths,
13574
- preset,
13575
- effectiveQuality,
13576
- effectiveBitrate,
13577
- fileServer: activeFileServer,
13578
- buildCaptureOptions,
13579
- createRenderVideoFrameInjector,
13580
- hdrDiagnostics,
13581
- abortSignal: executionSignal,
13582
- assertNotAborted,
13583
- onProgress
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
- createSseProgressReporter(stream, requestId),
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
- export declare function observeRenderStage<T>(recorder: RenderObservabilityRecorder, phase: string, data: RenderObservationData | undefined, fn: () => Promise<T>, options?: ObserveRenderStageOptions): Promise<T>;
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.203",
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.203",
39
- "@hyperframes/engine": "0.8.203",
40
- "@hyperframes/lint": "0.8.203",
41
- "@hyperframes/parsers": "0.8.203",
42
- "@hyperframes/studio-server": "0.8.203"
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.203"
51
+ "@hyperframes/aws-lambda": "0.8.205"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"