@hyperframes/producer 0.8.203 → 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/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;
@@ -7516,9 +7580,10 @@ async function discoverMediaFromBrowser(page) {
7516
7580
  const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
7517
7581
  const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
7518
7582
  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 || "";
7583
+ const resolvedSrc = htmlEl.currentSrc || htmlEl.src || htmlEl.getAttribute("src") || "";
7584
+ const withoutQueryAndHash = (url) => url.replace(/[?#][\s\S]*$/, "");
7585
+ const isPageItself = withoutQueryAndHash(resolvedSrc) === withoutQueryAndHash(location.href);
7586
+ const src = isPageItself ? "" : resolvedSrc;
7522
7587
  const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
7523
7588
  const endRaw = htmlEl.getAttribute("data-end");
7524
7589
  const durationRaw = htmlEl.getAttribute("data-duration");
@@ -9824,6 +9889,7 @@ async function runCaptureStage(input) {
9824
9889
  interleaveFrames: input.interleaveFrames,
9825
9890
  dedupPerfs,
9826
9891
  pageErrorLines: workerPageErrors,
9892
+ openAttempt: input.openAttempt,
9827
9893
  onProgress: (progress) => {
9828
9894
  if (progress.latestWorkerPhase) {
9829
9895
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -9895,6 +9961,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
9895
9961
  let transientRetriesUsed = 0;
9896
9962
  let resumeFrom = 0;
9897
9963
  while (true) {
9964
+ const attempt = input.openAttempt?.();
9965
+ const assertAttemptActive = () => {
9966
+ assertNotAborted();
9967
+ attempt?.throwIfAborted();
9968
+ };
9898
9969
  const videoInjector = createRenderVideoFrameInjector();
9899
9970
  const session = probeSession ?? await createCaptureSession5(
9900
9971
  fileServer.url,
@@ -9910,10 +9981,10 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
9910
9981
  probeSession = null;
9911
9982
  }
9912
9983
  await ensureSessionReady(session);
9913
- assertNotAborted();
9984
+ assertAttemptActive();
9914
9985
  lastBrowserConsole = session.browserConsoleBuffer;
9915
9986
  await captureSessionFrames(session, {
9916
- input,
9987
+ input: { ...input, assertNotAborted: assertAttemptActive },
9917
9988
  rangeStart,
9918
9989
  rangeEnd,
9919
9990
  resumeFrom,
@@ -11773,7 +11844,17 @@ function captureAttemptMadeProgress(attemptTargetFrameCount, remainingFrameCount
11773
11844
  function resetCaptureAttemptProgress(job) {
11774
11845
  job.framesRendered = 0;
11775
11846
  }
11847
+ var FIRST_FRAME_STALL_MS = 3e5;
11848
+ function frameStallRule(job) {
11849
+ const noProgressMs = resolveCaptureStallTimeoutMs();
11850
+ return {
11851
+ progress: () => job.framesRendered ?? 0,
11852
+ noProgressMs,
11853
+ firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
11854
+ };
11855
+ }
11776
11856
  function isRecoverableParallelCaptureError(error) {
11857
+ if (error instanceof RenderStageStalledError) return true;
11777
11858
  const message = normalizeErrorMessage(error);
11778
11859
  if (!message.includes("[Parallel] Capture failed")) return false;
11779
11860
  const kind = classifyCaptureFailure3(error).kind;
@@ -11836,8 +11917,10 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11836
11917
  )
11837
11918
  ];
11838
11919
  options.dedupPerfs.length = 0;
11920
+ let attemptSignal = options.abortSignal;
11839
11921
  try {
11840
11922
  for (const tasks of batches) {
11923
+ attemptSignal = options.openAttempt?.(options.abortSignal) ?? options.abortSignal;
11841
11924
  const capturedBeforeBatch = countCapturedFrames(
11842
11925
  options.totalFrames,
11843
11926
  options.framesDir,
@@ -11850,7 +11933,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11850
11933
  tasks,
11851
11934
  options.captureOptions,
11852
11935
  options.createBeforeCaptureHook,
11853
- options.abortSignal,
11936
+ attemptSignal,
11854
11937
  options.onProgress ? (progress) => {
11855
11938
  options.onProgress?.({
11856
11939
  ...progress,
@@ -11898,7 +11981,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11898
11981
  currentWorkers = nextWorkers;
11899
11982
  missingRanges = remaining;
11900
11983
  attempt++;
11901
- } catch (error) {
11984
+ } catch (caught) {
11985
+ const error = attemptSignal?.aborted && !options.abortSignal?.aborted ? attemptSignal.reason : caught;
11902
11986
  const failure = classifyCaptureFailure3(error, { signal: options.abortSignal });
11903
11987
  if (failure.kind === "cancelled") {
11904
11988
  throw error;
@@ -13567,37 +13651,44 @@ async function executeRenderPipeline(input) {
13567
13651
  observability,
13568
13652
  "capture_hdr_layered",
13569
13653
  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
- })
13654
+ (openAttempt) => {
13655
+ const attempt = openAttempt(executionSignal);
13656
+ return runCaptureHdrStage({
13657
+ job,
13658
+ cfg,
13659
+ plan: layeredPlan,
13660
+ log,
13661
+ projectDir,
13662
+ compiledDir,
13663
+ framesDir,
13664
+ videoOnlyPath,
13665
+ width,
13666
+ height,
13667
+ totalFrames,
13668
+ composition,
13669
+ hasHdrContent,
13670
+ effectiveHdr,
13671
+ nativeHdrVideoIds,
13672
+ nativeHdrImageIds,
13673
+ videoTransfers,
13674
+ imageTransfers,
13675
+ hdrImageSrcPaths,
13676
+ preset,
13677
+ effectiveQuality,
13678
+ effectiveBitrate,
13679
+ fileServer: activeFileServer,
13680
+ buildCaptureOptions,
13681
+ createRenderVideoFrameInjector,
13682
+ hdrDiagnostics,
13683
+ abortSignal: attempt,
13684
+ assertNotAborted: () => {
13685
+ assertNotAborted();
13686
+ attempt.throwIfAborted();
13687
+ },
13688
+ onProgress
13689
+ });
13690
+ },
13691
+ { stall: frameStallRule(job) }
13601
13692
  );
13602
13693
  lastBrowserConsole = hdrRes.lastBrowserConsole;
13603
13694
  layeredCaptureWarnings.push(...hdrRes.warnings);
@@ -13962,7 +14053,7 @@ async function executeRenderPipeline(input) {
13962
14053
  observability,
13963
14054
  "capture_disk",
13964
14055
  captureStageObservationData({ needsAlpha: diskPlan.needsAlpha }),
13965
- () => runCaptureStage({
14056
+ (openAttempt) => runCaptureStage({
13966
14057
  fileServer: activeFileServer,
13967
14058
  workDir,
13968
14059
  framesDir,
@@ -13980,8 +14071,10 @@ async function executeRenderPipeline(input) {
13980
14071
  assertNotAborted,
13981
14072
  onProgress,
13982
14073
  onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
13983
- interleaveFrames: overlap !== null
13984
- })
14074
+ interleaveFrames: overlap !== null,
14075
+ openAttempt
14076
+ }),
14077
+ { stall: frameStallRule(job) }
13985
14078
  );
13986
14079
  let captureRes;
13987
14080
  try {
@@ -17639,6 +17732,7 @@ import {
17639
17732
  } from "@hyperframes/core";
17640
17733
  import { parseEncoderOptions } from "@hyperframes/studio-server/encoder-options";
17641
17734
  var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
17735
+ var SSE_KEEPALIVE_MS = 3e4;
17642
17736
  var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
17643
17737
  "ASSET_MEDIA_TYPE_MISMATCH",
17644
17738
  "NOT_MEDIA_PAYLOAD",
@@ -17646,7 +17740,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
17646
17740
  "VIDEO_SOURCE_UNRENDERABLE",
17647
17741
  "VIDEO_EXTRACTION_FAILED",
17648
17742
  "ENCODER_INTERRUPTED",
17649
- "RENDER_TOO_SLOW"
17743
+ "RENDER_TOO_SLOW",
17744
+ "RENDER_STAGE_STALLED"
17650
17745
  ]);
17651
17746
  function extractSafeRenderErrorCode(error) {
17652
17747
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -18160,15 +18255,21 @@ function createRenderHandlers(options = {}) {
18160
18255
  })
18161
18256
  });
18162
18257
  }
18258
+ const reportProgress = createSseProgressReporter(stream, requestId);
18259
+ const keepalive = setInterval(() => {
18260
+ Promise.resolve(reportProgress(job, job.currentStage)).catch(() => {
18261
+ });
18262
+ }, SSE_KEEPALIVE_MS);
18163
18263
  const release = await renderSemaphore.acquire();
18164
18264
  try {
18165
18265
  await executeRenderJob(
18166
18266
  job,
18167
18267
  input.projectDir,
18168
18268
  absoluteOutputPath,
18169
- createSseProgressReporter(stream, requestId),
18269
+ reportProgress,
18170
18270
  abortController.signal
18171
- );
18271
+ ).finally(release);
18272
+ clearInterval(keepalive);
18172
18273
  const fileSize = outputFileSize(absoluteOutputPath);
18173
18274
  const outputToken = store.register(absoluteOutputPath);
18174
18275
  const outputUrl = `${outputUrlPrefix}/${outputToken}`;
@@ -18188,6 +18289,7 @@ function createRenderHandlers(options = {}) {
18188
18289
  })
18189
18290
  });
18190
18291
  } catch (error) {
18292
+ clearInterval(keepalive);
18191
18293
  await writeRenderStreamFailure({
18192
18294
  error,
18193
18295
  job,
@@ -18197,7 +18299,6 @@ function createRenderHandlers(options = {}) {
18197
18299
  log
18198
18300
  });
18199
18301
  } finally {
18200
- release();
18201
18302
  c.req.raw.signal.removeEventListener("abort", onRequestAbort);
18202
18303
  cleanupTempDir(cleanupProjectDir, log);
18203
18304
  }
@@ -20014,29 +20115,36 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20014
20115
  const captureSession = session;
20015
20116
  session = null;
20016
20117
  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) => {
20118
+ await observeRenderStage(
20119
+ new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
20120
+ "capture_chunk",
20121
+ { chunkIndex },
20122
+ (openAttempt) => runCaptureStage({
20123
+ fileServer,
20124
+ workDir,
20125
+ framesDir,
20126
+ job,
20127
+ totalFrames: framesInChunk,
20128
+ cfg: captureCfg,
20129
+ plan: capturePlan,
20130
+ log,
20131
+ probeSession: captureSession,
20132
+ captureAttempts,
20133
+ // This sink records each worker's effective capture mode so a
20134
+ // fallback remains observable to adapters and smoke tests.
20135
+ dedupPerfs: capturePerfs,
20136
+ buildCaptureOptions: () => captureOptions,
20137
+ createRenderVideoFrameInjector: createChunkVideoFrameInjector,
20138
+ abortSignal: void 0,
20139
+ assertNotAborted: () => {
20140
+ },
20141
+ frameRange: range,
20142
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20143
+ interleaveFrames: interleavesCapture(overlap.current !== null, options),
20144
+ openAttempt
20145
+ }),
20146
+ { stall: frameStallRule(job) }
20147
+ ).catch(async (captureError) => {
20040
20148
  await overlap.current?.abort();
20041
20149
  overlap.current = null;
20042
20150
  throw captureError;