@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/index.js CHANGED
@@ -1238,6 +1238,7 @@ function buildRenderErrorDetails(input) {
1238
1238
 
1239
1239
  // src/services/render/renderEventPublisher.ts
1240
1240
  import { cloneCaptureWarnings } from "@hyperframes/engine";
1241
+ var FLUSH_TIMEOUT_MS = 1e4;
1241
1242
  function snapshotJob(job) {
1242
1243
  return {
1243
1244
  ...job,
@@ -1267,7 +1268,15 @@ var OrderedRenderEventPublisher = class {
1267
1268
  });
1268
1269
  }
1269
1270
  async flush() {
1270
- await this.tail;
1271
+ let timer;
1272
+ const drained = await Promise.race([
1273
+ this.tail.then(() => true),
1274
+ new Promise((resolve12) => {
1275
+ timer = setTimeout(() => resolve12(false), FLUSH_TIMEOUT_MS);
1276
+ })
1277
+ ]);
1278
+ clearTimeout(timer);
1279
+ if (!drained) this.log.warn("Render event sink did not drain; continuing without it");
1271
1280
  }
1272
1281
  };
1273
1282
  function publishRenderFailure(job, failure, onProgress) {
@@ -5100,6 +5109,7 @@ function shouldFallbackToScreenshotAfterCalibrationError(error) {
5100
5109
  // src/services/render/observability.ts
5101
5110
  import { createHash as createHash3 } from "node:crypto";
5102
5111
  import { redactTelemetryString } from "@hyperframes/core";
5112
+ import { CaptureFailure as CaptureFailure2 } from "@hyperframes/engine";
5103
5113
  var MAX_EVENTS = 160;
5104
5114
  var ALLOWED_STRING_DATA_KEYS = /* @__PURE__ */ new Set([
5105
5115
  "browserGpuMode",
@@ -5332,6 +5342,56 @@ function heartbeatTargetMs(index) {
5332
5342
  const overflow = index - HEARTBEAT_RAMP_MS.length + 1;
5333
5343
  return HEARTBEAT_RAMP_END_MS + overflow * HEARTBEAT_REPEAT_MS;
5334
5344
  }
5345
+ var RenderStageStalledError = class extends CaptureFailure2 {
5346
+ code = "RENDER_STAGE_STALLED";
5347
+ owner = "system";
5348
+ retryable = false;
5349
+ constructor(phase, silentMs, progress) {
5350
+ super({
5351
+ kind: "protocol_timeout",
5352
+ message: `[Render] ${phase} stalled: no progress for ${silentMs}ms (stuck at ${progress}).`
5353
+ });
5354
+ this.name = "RenderStageStalledError";
5355
+ }
5356
+ };
5357
+ var STALL_POLL_MS = 5e3;
5358
+ function watchStall(phase, rule) {
5359
+ let last = rule.progress();
5360
+ let budgetMs = rule.firstProgressMs;
5361
+ let lastChangeAt = Date.now();
5362
+ let attempt;
5363
+ let timer;
5364
+ const restartWindow = (ms) => {
5365
+ last = rule.progress();
5366
+ budgetMs = ms;
5367
+ lastChangeAt = Date.now();
5368
+ };
5369
+ const tripped = new Promise((_, reject) => {
5370
+ timer = setInterval(
5371
+ () => {
5372
+ const current = rule.progress();
5373
+ if (current !== last) return restartWindow(rule.noProgressMs);
5374
+ if (Date.now() - lastChangeAt < budgetMs) return;
5375
+ const error = new RenderStageStalledError(phase, budgetMs, current);
5376
+ if (attempt && !attempt.signal.aborted) {
5377
+ attempt.abort(error);
5378
+ return restartWindow(rule.noProgressMs);
5379
+ }
5380
+ clearInterval(timer);
5381
+ reject(error);
5382
+ },
5383
+ Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs)
5384
+ );
5385
+ });
5386
+ const openAttempt = (parent) => {
5387
+ attempt = new AbortController();
5388
+ restartWindow(rule.firstProgressMs);
5389
+ return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
5390
+ };
5391
+ const stalledBy = () => attempt?.signal.aborted ? attempt.signal.reason : void 0;
5392
+ return { tripped, openAttempt, stalledBy, stop: () => clearInterval(timer) };
5393
+ }
5394
+ var openUnwatchedAttempt = (parent) => parent ?? new AbortController().signal;
5335
5395
  async function observeRenderStage(recorder, phase, data, fn, options = {}) {
5336
5396
  const heartbeatMessage = options.heartbeatMessage ?? "stage still running";
5337
5397
  const startedAt = recorder.stageStart(phase, data);
@@ -5353,15 +5413,19 @@ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
5353
5413
  heartbeatTimer.unref?.();
5354
5414
  };
5355
5415
  scheduleNextHeartbeat();
5416
+ const stall = options.stall ? watchStall(phase, options.stall) : void 0;
5356
5417
  const clearHeartbeats = () => {
5357
5418
  clearTimeout(heartbeatTimer);
5419
+ stall?.stop();
5358
5420
  };
5359
5421
  try {
5360
- const result = await fn();
5422
+ const run = fn(stall?.openAttempt ?? openUnwatchedAttempt);
5423
+ const result = await (stall ? Promise.race([run, stall.tripped]) : run);
5361
5424
  clearHeartbeats();
5362
5425
  recorder.stageEnd(phase, startedAt, data);
5363
5426
  return result;
5364
- } catch (error) {
5427
+ } catch (caught) {
5428
+ const error = stall?.stalledBy() ?? caught;
5365
5429
  clearHeartbeats();
5366
5430
  recorder.stageError(phase, startedAt, error, data);
5367
5431
  throw error;
@@ -5545,6 +5609,8 @@ import {
5545
5609
  readDeclaredDefaults,
5546
5610
  parseHostVariableValues,
5547
5611
  headStyleRuns,
5612
+ isAlwaysAppliedStyle,
5613
+ dedupeFontFaceRules,
5548
5614
  inlineScriptRuns,
5549
5615
  parsesAsScript,
5550
5616
  styleElementsFor,
@@ -5837,12 +5903,12 @@ function resolveClipWindowSeconds(img, compositionDuration) {
5837
5903
  }
5838
5904
  return null;
5839
5905
  }
5840
- function resolvePreparedPlayback(metadata, loop, windowSeconds) {
5906
+ function resolvePreparedPlayback(metadata, loopOverride, windowSeconds) {
5841
5907
  const gifDuration = metadata.durationSeconds > 0 ? metadata.durationSeconds : null;
5842
5908
  let loopIterations = 1;
5843
- if (loop) {
5909
+ if (resolveLoop(metadata, loopOverride)) {
5844
5910
  loopIterations = windowSeconds != null && gifDuration != null ? Math.min(MAX_LOOP_ITERATIONS, Math.max(1, Math.ceil(windowSeconds / gifDuration))) : 1;
5845
- } else if (metadata.loopCount != null && metadata.loopCount > 1) {
5911
+ } else if (loopOverride == null && metadata.loopCount != null && metadata.loopCount > 1) {
5846
5912
  loopIterations = Math.min(MAX_LOOP_ITERATIONS, metadata.loopCount);
5847
5913
  }
5848
5914
  let padSeconds = 0;
@@ -6009,9 +6075,14 @@ async function prepareAnimatedGifInputs(html, options) {
6009
6075
  const bytes = readFileSync5(sourcePath);
6010
6076
  const metadata = parseAnimatedGifMetadata(bytes);
6011
6077
  if (!metadata?.animated) continue;
6012
- const loop = resolveLoop(metadata, readLoopOverride(img));
6078
+ const loopOverride = readLoopOverride(img);
6079
+ const loop = resolveLoop(metadata, loopOverride);
6013
6080
  const windowSeconds = resolveClipWindowSeconds(img, compositionDuration);
6014
- const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
6081
+ const { loopIterations, padSeconds } = resolvePreparedPlayback(
6082
+ metadata,
6083
+ loopOverride,
6084
+ windowSeconds
6085
+ );
6015
6086
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
6016
6087
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
6017
6088
  const cachePath = join13(cacheDir, filename);
@@ -6677,6 +6748,11 @@ function coalesceHeadStylesAndBodyScripts(html) {
6677
6748
  const head = document2.querySelector("head");
6678
6749
  const body = document2.querySelector("body");
6679
6750
  if (!head && !body) return html;
6751
+ const liveStyles = [...document2.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
6752
+ const dedupedStyles = dedupeFontFaceRules(liveStyles.map((el) => el.textContent || ""));
6753
+ liveStyles.forEach((el, i) => {
6754
+ el.textContent = dedupedStyles[i] ?? "";
6755
+ });
6680
6756
  const styleEls = head ? Array.from(head.querySelectorAll("style")) : [];
6681
6757
  const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
6682
6758
  for (const run of styleEls.length > 1 ? headStyleRuns(styleEls) : []) {
@@ -7516,9 +7592,10 @@ async function discoverMediaFromBrowser(page) {
7516
7592
  const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
7517
7593
  const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
7518
7594
  if (!id) return;
7519
- const ownSrc = htmlEl.getAttribute("src");
7520
- const blankSrc = ownSrc !== null && ownSrc.trim() === "" && !htmlEl.querySelector("source[src]");
7521
- const src = blankSrc ? "" : htmlEl.currentSrc || htmlEl.src || ownSrc || "";
7595
+ const resolvedSrc = htmlEl.currentSrc || htmlEl.src || htmlEl.getAttribute("src") || "";
7596
+ const withoutQueryAndHash = (url) => url.replace(/[?#][\s\S]*$/, "");
7597
+ const isPageItself = withoutQueryAndHash(resolvedSrc) === withoutQueryAndHash(location.href);
7598
+ const src = isPageItself ? "" : resolvedSrc;
7522
7599
  const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
7523
7600
  const endRaw = htmlEl.getAttribute("data-end");
7524
7601
  const durationRaw = htmlEl.getAttribute("data-duration");
@@ -9824,6 +9901,7 @@ async function runCaptureStage(input) {
9824
9901
  interleaveFrames: input.interleaveFrames,
9825
9902
  dedupPerfs,
9826
9903
  pageErrorLines: workerPageErrors,
9904
+ openAttempt: input.openAttempt,
9827
9905
  onProgress: (progress) => {
9828
9906
  if (progress.latestWorkerPhase) {
9829
9907
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -9895,6 +9973,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
9895
9973
  let transientRetriesUsed = 0;
9896
9974
  let resumeFrom = 0;
9897
9975
  while (true) {
9976
+ const attempt = input.openAttempt?.();
9977
+ const assertAttemptActive = () => {
9978
+ assertNotAborted();
9979
+ attempt?.throwIfAborted();
9980
+ };
9898
9981
  const videoInjector = createRenderVideoFrameInjector();
9899
9982
  const session = probeSession ?? await createCaptureSession5(
9900
9983
  fileServer.url,
@@ -9910,10 +9993,10 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
9910
9993
  probeSession = null;
9911
9994
  }
9912
9995
  await ensureSessionReady(session);
9913
- assertNotAborted();
9996
+ assertAttemptActive();
9914
9997
  lastBrowserConsole = session.browserConsoleBuffer;
9915
9998
  await captureSessionFrames(session, {
9916
- input,
9999
+ input: { ...input, assertNotAborted: assertAttemptActive },
9917
10000
  rangeStart,
9918
10001
  rangeEnd,
9919
10002
  resumeFrom,
@@ -11773,7 +11856,17 @@ function captureAttemptMadeProgress(attemptTargetFrameCount, remainingFrameCount
11773
11856
  function resetCaptureAttemptProgress(job) {
11774
11857
  job.framesRendered = 0;
11775
11858
  }
11859
+ var FIRST_FRAME_STALL_MS = 3e5;
11860
+ function frameStallRule(job) {
11861
+ const noProgressMs = resolveCaptureStallTimeoutMs();
11862
+ return {
11863
+ progress: () => job.framesRendered ?? 0,
11864
+ noProgressMs,
11865
+ firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
11866
+ };
11867
+ }
11776
11868
  function isRecoverableParallelCaptureError(error) {
11869
+ if (error instanceof RenderStageStalledError) return true;
11777
11870
  const message = normalizeErrorMessage(error);
11778
11871
  if (!message.includes("[Parallel] Capture failed")) return false;
11779
11872
  const kind = classifyCaptureFailure3(error).kind;
@@ -11836,8 +11929,10 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11836
11929
  )
11837
11930
  ];
11838
11931
  options.dedupPerfs.length = 0;
11932
+ let attemptSignal = options.abortSignal;
11839
11933
  try {
11840
11934
  for (const tasks of batches) {
11935
+ attemptSignal = options.openAttempt?.(options.abortSignal) ?? options.abortSignal;
11841
11936
  const capturedBeforeBatch = countCapturedFrames(
11842
11937
  options.totalFrames,
11843
11938
  options.framesDir,
@@ -11850,7 +11945,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11850
11945
  tasks,
11851
11946
  options.captureOptions,
11852
11947
  options.createBeforeCaptureHook,
11853
- options.abortSignal,
11948
+ attemptSignal,
11854
11949
  options.onProgress ? (progress) => {
11855
11950
  options.onProgress?.({
11856
11951
  ...progress,
@@ -11898,7 +11993,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11898
11993
  currentWorkers = nextWorkers;
11899
11994
  missingRanges = remaining;
11900
11995
  attempt++;
11901
- } catch (error) {
11996
+ } catch (caught) {
11997
+ const error = attemptSignal?.aborted && !options.abortSignal?.aborted ? attemptSignal.reason : caught;
11902
11998
  const failure = classifyCaptureFailure3(error, { signal: options.abortSignal });
11903
11999
  if (failure.kind === "cancelled") {
11904
12000
  throw error;
@@ -13567,37 +13663,44 @@ async function executeRenderPipeline(input) {
13567
13663
  observability,
13568
13664
  "capture_hdr_layered",
13569
13665
  captureStageObservationData({ hasHdrContent }),
13570
- () => runCaptureHdrStage({
13571
- job,
13572
- cfg,
13573
- plan: layeredPlan,
13574
- log,
13575
- projectDir,
13576
- compiledDir,
13577
- framesDir,
13578
- videoOnlyPath,
13579
- width,
13580
- height,
13581
- totalFrames,
13582
- composition,
13583
- hasHdrContent,
13584
- effectiveHdr,
13585
- nativeHdrVideoIds,
13586
- nativeHdrImageIds,
13587
- videoTransfers,
13588
- imageTransfers,
13589
- hdrImageSrcPaths,
13590
- preset,
13591
- effectiveQuality,
13592
- effectiveBitrate,
13593
- fileServer: activeFileServer,
13594
- buildCaptureOptions,
13595
- createRenderVideoFrameInjector,
13596
- hdrDiagnostics,
13597
- abortSignal: executionSignal,
13598
- assertNotAborted,
13599
- onProgress
13600
- })
13666
+ (openAttempt) => {
13667
+ const attempt = openAttempt(executionSignal);
13668
+ return runCaptureHdrStage({
13669
+ job,
13670
+ cfg,
13671
+ plan: layeredPlan,
13672
+ log,
13673
+ projectDir,
13674
+ compiledDir,
13675
+ framesDir,
13676
+ videoOnlyPath,
13677
+ width,
13678
+ height,
13679
+ totalFrames,
13680
+ composition,
13681
+ hasHdrContent,
13682
+ effectiveHdr,
13683
+ nativeHdrVideoIds,
13684
+ nativeHdrImageIds,
13685
+ videoTransfers,
13686
+ imageTransfers,
13687
+ hdrImageSrcPaths,
13688
+ preset,
13689
+ effectiveQuality,
13690
+ effectiveBitrate,
13691
+ fileServer: activeFileServer,
13692
+ buildCaptureOptions,
13693
+ createRenderVideoFrameInjector,
13694
+ hdrDiagnostics,
13695
+ abortSignal: attempt,
13696
+ assertNotAborted: () => {
13697
+ assertNotAborted();
13698
+ attempt.throwIfAborted();
13699
+ },
13700
+ onProgress
13701
+ });
13702
+ },
13703
+ { stall: frameStallRule(job) }
13601
13704
  );
13602
13705
  lastBrowserConsole = hdrRes.lastBrowserConsole;
13603
13706
  layeredCaptureWarnings.push(...hdrRes.warnings);
@@ -13962,7 +14065,7 @@ async function executeRenderPipeline(input) {
13962
14065
  observability,
13963
14066
  "capture_disk",
13964
14067
  captureStageObservationData({ needsAlpha: diskPlan.needsAlpha }),
13965
- () => runCaptureStage({
14068
+ (openAttempt) => runCaptureStage({
13966
14069
  fileServer: activeFileServer,
13967
14070
  workDir,
13968
14071
  framesDir,
@@ -13980,8 +14083,10 @@ async function executeRenderPipeline(input) {
13980
14083
  assertNotAborted,
13981
14084
  onProgress,
13982
14085
  onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
13983
- interleaveFrames: overlap !== null
13984
- })
14086
+ interleaveFrames: overlap !== null,
14087
+ openAttempt
14088
+ }),
14089
+ { stall: frameStallRule(job) }
13985
14090
  );
13986
14091
  let captureRes;
13987
14092
  try {
@@ -17639,6 +17744,7 @@ import {
17639
17744
  } from "@hyperframes/core";
17640
17745
  import { parseEncoderOptions } from "@hyperframes/studio-server/encoder-options";
17641
17746
  var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
17747
+ var SSE_KEEPALIVE_MS = 3e4;
17642
17748
  var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
17643
17749
  "ASSET_MEDIA_TYPE_MISMATCH",
17644
17750
  "NOT_MEDIA_PAYLOAD",
@@ -17646,7 +17752,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
17646
17752
  "VIDEO_SOURCE_UNRENDERABLE",
17647
17753
  "VIDEO_EXTRACTION_FAILED",
17648
17754
  "ENCODER_INTERRUPTED",
17649
- "RENDER_TOO_SLOW"
17755
+ "RENDER_TOO_SLOW",
17756
+ "RENDER_STAGE_STALLED"
17650
17757
  ]);
17651
17758
  function extractSafeRenderErrorCode(error) {
17652
17759
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -18160,15 +18267,21 @@ function createRenderHandlers(options = {}) {
18160
18267
  })
18161
18268
  });
18162
18269
  }
18270
+ const reportProgress = createSseProgressReporter(stream, requestId);
18271
+ const keepalive = setInterval(() => {
18272
+ Promise.resolve(reportProgress(job, job.currentStage)).catch(() => {
18273
+ });
18274
+ }, SSE_KEEPALIVE_MS);
18163
18275
  const release = await renderSemaphore.acquire();
18164
18276
  try {
18165
18277
  await executeRenderJob(
18166
18278
  job,
18167
18279
  input.projectDir,
18168
18280
  absoluteOutputPath,
18169
- createSseProgressReporter(stream, requestId),
18281
+ reportProgress,
18170
18282
  abortController.signal
18171
- );
18283
+ ).finally(release);
18284
+ clearInterval(keepalive);
18172
18285
  const fileSize = outputFileSize(absoluteOutputPath);
18173
18286
  const outputToken = store.register(absoluteOutputPath);
18174
18287
  const outputUrl = `${outputUrlPrefix}/${outputToken}`;
@@ -18188,6 +18301,7 @@ function createRenderHandlers(options = {}) {
18188
18301
  })
18189
18302
  });
18190
18303
  } catch (error) {
18304
+ clearInterval(keepalive);
18191
18305
  await writeRenderStreamFailure({
18192
18306
  error,
18193
18307
  job,
@@ -18197,7 +18311,6 @@ function createRenderHandlers(options = {}) {
18197
18311
  log
18198
18312
  });
18199
18313
  } finally {
18200
- release();
18201
18314
  c.req.raw.signal.removeEventListener("abort", onRequestAbort);
18202
18315
  cleanupTempDir(cleanupProjectDir, log);
18203
18316
  }
@@ -20014,29 +20127,36 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20014
20127
  const captureSession = session;
20015
20128
  session = null;
20016
20129
  overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus3().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
20017
- await runCaptureStage({
20018
- fileServer,
20019
- workDir,
20020
- framesDir,
20021
- job,
20022
- totalFrames: framesInChunk,
20023
- cfg: captureCfg,
20024
- plan: capturePlan,
20025
- log,
20026
- probeSession: captureSession,
20027
- captureAttempts,
20028
- // This sink records each worker's effective capture mode so a
20029
- // fallback remains observable to adapters and smoke tests.
20030
- dedupPerfs: capturePerfs,
20031
- buildCaptureOptions: () => captureOptions,
20032
- createRenderVideoFrameInjector: createChunkVideoFrameInjector,
20033
- abortSignal: void 0,
20034
- assertNotAborted: () => {
20035
- },
20036
- frameRange: range,
20037
- onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20038
- interleaveFrames: interleavesCapture(overlap.current !== null, options)
20039
- }).catch(async (captureError) => {
20130
+ await observeRenderStage(
20131
+ new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
20132
+ "capture_chunk",
20133
+ { chunkIndex },
20134
+ (openAttempt) => runCaptureStage({
20135
+ fileServer,
20136
+ workDir,
20137
+ framesDir,
20138
+ job,
20139
+ totalFrames: framesInChunk,
20140
+ cfg: captureCfg,
20141
+ plan: capturePlan,
20142
+ log,
20143
+ probeSession: captureSession,
20144
+ captureAttempts,
20145
+ // This sink records each worker's effective capture mode so a
20146
+ // fallback remains observable to adapters and smoke tests.
20147
+ dedupPerfs: capturePerfs,
20148
+ buildCaptureOptions: () => captureOptions,
20149
+ createRenderVideoFrameInjector: createChunkVideoFrameInjector,
20150
+ abortSignal: void 0,
20151
+ assertNotAborted: () => {
20152
+ },
20153
+ frameRange: range,
20154
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20155
+ interleaveFrames: interleavesCapture(overlap.current !== null, options),
20156
+ openAttempt
20157
+ }),
20158
+ { stall: frameStallRule(job) }
20159
+ ).catch(async (captureError) => {
20040
20160
  await overlap.current?.abort();
20041
20161
  overlap.current = null;
20042
20162
  throw captureError;