@hyperframes/producer 0.8.214 → 0.8.217

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.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * or any web content via configurable page contracts and hooks.
7
7
  */
8
8
  export { createRenderJob, executeRenderJob, resolveRenderDebugDir, RenderCancelledError, RenderQualityError, applyRenderWarningPolicy, type RenderConfig, type RenderConfigInput, type RenderJob, type RenderStatus, type RenderOutcome, type RenderStrictness, type RenderWarning, type RenderPerfSummary, type ProgressCallback, } from "./services/renderOrchestrator.js";
9
+ export { extractSafeRenderErrorCode } from "./services/safeRenderErrorCode.js";
9
10
  export { RENDER_JOB_DIR, createOwnedRenderDir, listAbandonedRenderDirs, } from "./services/render/renderDirOwner.js";
10
11
  export { RENDER_REQUEST_VERSION, createRenderRequest, distributedConfigFromRequest, parseRenderRequest, renderConfigFromRequest, renderRequestFromDistributedConfig, serializeRenderRequest, type CreateRenderRequestInput, type DistributedRenderOptions, type RenderRequest, type RenderRequestOptions, } from "./renderRequest.js";
11
12
  export { type BrowserDiagnosticSummary, type RenderCaptureObservability, type RenderObservabilitySummary, type RenderObservationData, type RenderObservationEvent, type RenderObservationStatus, } from "./services/render/observability.js";
package/dist/index.js CHANGED
@@ -3690,6 +3690,10 @@ function resolveCaptureStallTimeoutMs() {
3690
3690
  const parsed = raw ? Number(raw) : Number.NaN;
3691
3691
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
3692
3692
  }
3693
+ function stallBudgetMs(stallTimeoutMs, framesDone, beginFrame, cfg) {
3694
+ const deadline = cfg.beginFrameFirstFrameDeadlineMs;
3695
+ return beginFrame && framesDone === 0 && deadline ? Math.min(stallTimeoutMs, deadline) : stallTimeoutMs;
3696
+ }
3693
3697
  function captureModeLabel(mode) {
3694
3698
  if (mode === "drawelement") return "drawElement";
3695
3699
  if (mode === "beginframe") return "BeginFrame";
@@ -4062,14 +4066,20 @@ async function runCaptureStreamingStage(input) {
4062
4066
  const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
4063
4067
  const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
4064
4068
  let stalled = false;
4069
+ const budget = () => stallBudgetMs(
4070
+ stallTimeoutMs,
4071
+ resumeFrom + lastCapturedFrames,
4072
+ captureLabel === "BeginFrame",
4073
+ cfg
4074
+ );
4065
4075
  const stallTimer = setInterval(
4066
4076
  () => {
4067
- if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
4077
+ if (!watchdogArmed || Date.now() - lastProgressAt <= budget()) return;
4068
4078
  stalled = true;
4069
4079
  const stallErr = new ParallelCaptureStallError({
4070
4080
  variant: "drain",
4071
4081
  captureLabel,
4072
- stallTimeoutMs,
4082
+ stallTimeoutMs: budget(),
4073
4083
  lastCapturedFrames: resumeFrom + lastCapturedFrames,
4074
4084
  totalFrames,
4075
4085
  phaseSummary: phaseSummary()
@@ -4077,7 +4087,7 @@ async function runCaptureStreamingStage(input) {
4077
4087
  parallelReorder.abort(stallErr);
4078
4088
  stallController.abort();
4079
4089
  },
4080
- Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
4090
+ Math.min(budget(), DE_STALL_POLL_MS)
4081
4091
  );
4082
4092
  try {
4083
4093
  workerResults = await executeParallelCapture(
@@ -4158,7 +4168,7 @@ async function runCaptureStreamingStage(input) {
4158
4168
  throw new ParallelCaptureStallError({
4159
4169
  variant: "pool",
4160
4170
  captureLabel,
4161
- stallTimeoutMs,
4171
+ stallTimeoutMs: budget(),
4162
4172
  lastCapturedFrames: resumeFrom + lastCapturedFrames,
4163
4173
  totalFrames,
4164
4174
  phaseSummary: phaseSummary()
@@ -4259,10 +4269,16 @@ async function runCaptureStreamingStage(input) {
4259
4269
  for (let i = 0; i < totalFrames; i++) {
4260
4270
  assertNotAborted();
4261
4271
  const time = i * job.config.fps.den / job.config.fps.num;
4272
+ const budget = stallBudgetMs(
4273
+ stallTimeoutMs,
4274
+ i,
4275
+ session.captureMode === "beginframe",
4276
+ cfg
4277
+ );
4262
4278
  const { buffer } = await raceAgainstStall(
4263
4279
  captureFrameToBuffer(session, i, time),
4264
- stallTimeoutMs - (Date.now() - lastProgressAt),
4265
- { session, frameIndex: i, totalFrames, stallTimeoutMs },
4280
+ budget - (Date.now() - lastProgressAt),
4281
+ { session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
4266
4282
  abortSignal
4267
4283
  );
4268
4284
  await reorderBuffer.waitForFrame(i);
@@ -5480,6 +5496,7 @@ function watchStall(phase, rule) {
5480
5496
  let lastChangeAt = Date.now();
5481
5497
  let attempt;
5482
5498
  let timer;
5499
+ let inTail = false;
5483
5500
  const restartWindow = (ms) => {
5484
5501
  last = rule.progress();
5485
5502
  budgetMs = ms;
@@ -5489,6 +5506,10 @@ function watchStall(phase, rule) {
5489
5506
  timer = setInterval(
5490
5507
  () => {
5491
5508
  const current = rule.progress();
5509
+ if (rule.complete?.() && !inTail) {
5510
+ inTail = true;
5511
+ return restartWindow(rule.tailMs ?? rule.noProgressMs);
5512
+ }
5492
5513
  if (current !== last) return restartWindow(rule.noProgressMs);
5493
5514
  if (Date.now() - lastChangeAt < budgetMs) return;
5494
5515
  const error = new RenderStageStalledError(phase, budgetMs, current);
@@ -5504,6 +5525,7 @@ function watchStall(phase, rule) {
5504
5525
  });
5505
5526
  const openAttempt = (parent) => {
5506
5527
  attempt = new AbortController();
5528
+ inTail = false;
5507
5529
  restartWindow(rule.firstProgressMs);
5508
5530
  return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
5509
5531
  };
@@ -8495,7 +8517,8 @@ async function runCompileStage(input) {
8495
8517
  }
8496
8518
 
8497
8519
  // src/services/render/stages/probeStage.ts
8498
- import { join as join17 } from "node:path";
8520
+ import { createHash as createHash7 } from "node:crypto";
8521
+ import { basename as basename5, join as join17 } from "node:path";
8499
8522
  import { parseHTML as parseHTML6 } from "linkedom";
8500
8523
  import {
8501
8524
  closeCaptureSession as closeCaptureSession4,
@@ -8503,6 +8526,7 @@ import {
8503
8526
  createCaptureSession as createCaptureSession4,
8504
8527
  deriveBeginFrameProbeTimeTicks,
8505
8528
  getCompositionDuration,
8529
+ getCompositionLengthSource,
8506
8530
  initializeSession as initializeSession4,
8507
8531
  isTransientBrowserError,
8508
8532
  probeBeginFrameLiveness,
@@ -8641,6 +8665,13 @@ function hasRuntimeMediaChanges(html) {
8641
8665
  function probeRequiresBrowser(args) {
8642
8666
  return args.durationSeconds <= 0 || args.unresolvedCompositionCount > 0 || args.hasAutoStart || args.hasScriptedAudio || args.hasVariableMedia || args.hasInsertedMedia;
8643
8667
  }
8668
+ function reportedLengthSource(source) {
8669
+ if (!source) return null;
8670
+ if (source.from !== "media") return { from: source.from };
8671
+ const name = basename5(source.src.split(/[?#]/, 1)[0] ?? "");
8672
+ const srcHash = createHash7("sha256").update(name).digest("hex").slice(0, 12);
8673
+ return { from: "media", tag: source.tag, trimmed: source.trimmed, srcHash };
8674
+ }
8644
8675
  async function runProbeStage(input) {
8645
8676
  const {
8646
8677
  projectDir,
@@ -8662,6 +8693,7 @@ async function runProbeStage(input) {
8662
8693
  const probeCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
8663
8694
  let fileServer = null;
8664
8695
  let probeSession = null;
8696
+ let lengthSource = input.composition.duration > 0 ? { from: "declared" } : null;
8665
8697
  let beginFrameStalled = false;
8666
8698
  let lastBrowserConsole = [];
8667
8699
  const probeStart = Date.now();
@@ -8823,10 +8855,12 @@ async function runProbeStage(input) {
8823
8855
  if (composition.duration <= 0) {
8824
8856
  log.info("Discovering composition duration...");
8825
8857
  const discoveredDuration = await getCompositionDuration(session);
8858
+ lengthSource = reportedLengthSource(await getCompositionLengthSource(session));
8826
8859
  assertNotAborted();
8827
8860
  log.info("Probed composition duration from browser", {
8828
8861
  discoveredDuration,
8829
- staticDuration: compiled.staticDuration
8862
+ staticDuration: compiled.staticDuration,
8863
+ lengthSource
8830
8864
  });
8831
8865
  composition.duration = discoveredDuration;
8832
8866
  } else {
@@ -9115,6 +9149,7 @@ async function runProbeStage(input) {
9115
9149
  probeSession,
9116
9150
  lastBrowserConsole,
9117
9151
  duration,
9152
+ lengthSource,
9118
9153
  totalFrames,
9119
9154
  browserProbeMs,
9120
9155
  beginFrameStalled
@@ -9639,7 +9674,7 @@ function checkedDbfs(value) {
9639
9674
  }
9640
9675
  return value;
9641
9676
  }
9642
- async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
9677
+ async function scanTruePeak(audioPath, trim, timeoutMs, signal, seekSeconds = 0) {
9643
9678
  let firstBlock;
9644
9679
  let laterBlocks = Number.NEGATIVE_INFINITY;
9645
9680
  let partialLine = "";
@@ -9655,7 +9690,21 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
9655
9690
  }
9656
9691
  };
9657
9692
  const result = await runFfmpeg4(
9658
- ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
9693
+ [
9694
+ ...seekSeconds > 0 ? ["-ss", String(seekSeconds)] : [],
9695
+ ...[
9696
+ "-i",
9697
+ audioPath,
9698
+ "-map",
9699
+ "0:a:0",
9700
+ "-vn",
9701
+ "-af",
9702
+ `${trim}ebur128=peak=true`,
9703
+ "-f",
9704
+ "null",
9705
+ "-"
9706
+ ]
9707
+ ],
9659
9708
  { signal, timeout: timeoutMs, onStderr: readLines }
9660
9709
  );
9661
9710
  if (!result.success) {
@@ -9681,12 +9730,15 @@ async function measureTruePeakDbfs(audioPath, options) {
9681
9730
  const scans = await Promise.all(
9682
9731
  Array.from({ length: count }, (_, k) => {
9683
9732
  const start = k === 0 ? 0 : cut(k) - block;
9684
- const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
9733
+ const seekSeconds = Math.max(0, Math.floor(start / sampleRate) - 1);
9734
+ const skipped = seekSeconds * sampleRate;
9735
+ const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1) - skipped}`;
9685
9736
  return scanTruePeak(
9686
9737
  audioPath,
9687
- `atrim=start_sample=${start}${end},`,
9738
+ `atrim=start_sample=${start - skipped}${end},`,
9688
9739
  timeoutMs,
9689
- segmentSignal
9740
+ segmentSignal,
9741
+ seekSeconds
9690
9742
  );
9691
9743
  })
9692
9744
  ).catch((error) => {
@@ -12315,12 +12367,15 @@ function resetCaptureAttemptProgress(job) {
12315
12367
  job.framesRendered = 0;
12316
12368
  }
12317
12369
  var FIRST_FRAME_STALL_MS = 3e5;
12318
- function frameStallRule(job) {
12370
+ var TAIL_STALL_MS = FIRST_FRAME_STALL_MS;
12371
+ function frameStallRule(job, totalFrames) {
12319
12372
  const noProgressMs = resolveCaptureStallTimeoutMs();
12320
12373
  return {
12321
12374
  progress: () => job.framesRendered ?? 0,
12322
12375
  noProgressMs,
12323
- firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
12376
+ firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs),
12377
+ complete: () => (job.framesRendered ?? 0) >= totalFrames,
12378
+ tailMs: Math.max(TAIL_STALL_MS, noProgressMs)
12324
12379
  };
12325
12380
  }
12326
12381
  function isRecoverableParallelCaptureError(error) {
@@ -13337,6 +13392,12 @@ async function executeRenderPipeline(input) {
13337
13392
  }
13338
13393
  observability.checkpoint("browser_probe", "duration resolved", {
13339
13394
  durationSeconds: probeResult.duration,
13395
+ lengthFrom: probeResult.lengthSource?.from ?? null,
13396
+ ...probeResult.lengthSource?.from === "media" ? {
13397
+ lengthTag: probeResult.lengthSource.tag,
13398
+ lengthTrimmed: probeResult.lengthSource.trimmed,
13399
+ lengthSrcHash: probeResult.lengthSource.srcHash
13400
+ } : {},
13340
13401
  totalFrames,
13341
13402
  compositionHash,
13342
13403
  beginFrameStalled: probeResult.beginFrameStalled
@@ -13522,6 +13583,14 @@ async function executeRenderPipeline(input) {
13522
13583
  // sample frame indices from this so they land inside the drained range.
13523
13584
  compositionDurationSeconds: job.duration,
13524
13585
  deFrameFallback: true,
13586
+ // A line per scanned chunk, so a long effect scan reads as preparing, not as a stall.
13587
+ onEffectScanProgress: (scanned, total) => updateJobStatus(
13588
+ job,
13589
+ "rendering",
13590
+ `Checking effects, ${scanned} of ${total} frames`,
13591
+ 25,
13592
+ onProgress
13593
+ ),
13525
13594
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
13526
13595
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
13527
13596
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -14206,7 +14275,7 @@ async function executeRenderPipeline(input) {
14206
14275
  onProgress
14207
14276
  });
14208
14277
  },
14209
- { stall: frameStallRule(job) }
14278
+ { stall: frameStallRule(job, totalFrames) }
14210
14279
  );
14211
14280
  lastBrowserConsole = hdrRes.lastBrowserConsole;
14212
14281
  layeredCaptureWarnings.push(...hdrRes.warnings);
@@ -14651,7 +14720,7 @@ async function executeRenderPipeline(input) {
14651
14720
  interleaveFrames: overlap !== null,
14652
14721
  openAttempt
14653
14722
  }),
14654
- { stall: frameStallRule(job) }
14723
+ { stall: frameStallRule(job, totalFrames) }
14655
14724
  );
14656
14725
  let captureRes;
14657
14726
  try {
@@ -14966,6 +15035,25 @@ async function executeRenderPipeline(input) {
14966
15035
  }
14967
15036
  }
14968
15037
 
15038
+ // src/services/safeRenderErrorCode.ts
15039
+ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
15040
+ "ASSET_MEDIA_TYPE_MISMATCH",
15041
+ "HEIF_IMAGE_CONVERSION_FAILED",
15042
+ "HEIF_TOOL_UNAVAILABLE",
15043
+ "NOT_MEDIA_PAYLOAD",
15044
+ "INVALID_VIDEO_METADATA",
15045
+ "VIDEO_SOURCE_UNRENDERABLE",
15046
+ "VIDEO_EXTRACTION_FAILED",
15047
+ "ENCODER_INTERRUPTED",
15048
+ "RENDER_TOO_SLOW",
15049
+ "RENDER_STAGE_STALLED"
15050
+ ]);
15051
+ function extractSafeRenderErrorCode(error) {
15052
+ if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
15053
+ const code = error.code;
15054
+ return typeof code === "string" && SAFE_RENDER_ERROR_CODES.has(code) ? code : void 0;
15055
+ }
15056
+
14969
15057
  // src/renderRequest.ts
14970
15058
  import {
14971
15059
  PRORES_PROFILE_NAMES as PRORES_PROFILE_NAMES2,
@@ -15742,12 +15830,12 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
15742
15830
  }
15743
15831
 
15744
15832
  // src/services/render/stages/planHash.ts
15745
- import { createHash as createHash7 } from "node:crypto";
15833
+ import { createHash as createHash8 } from "node:crypto";
15746
15834
  import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync5, readSync as readSync4 } from "node:fs";
15747
15835
  var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
15748
15836
  var FIELD_DELIMITER = Buffer.from([0]);
15749
15837
  function computePlanHash(input) {
15750
- const hash = createHash7("sha256");
15838
+ const hash = createHash8("sha256");
15751
15839
  hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
15752
15840
  hash.update(input.compositionHtml);
15753
15841
  hash.update(FIELD_DELIMITER);
@@ -15794,12 +15882,12 @@ function canonicalJsonStringify(value) {
15794
15882
  throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
15795
15883
  }
15796
15884
  function sha256Hex(bytes) {
15797
- const h = createHash7("sha256");
15885
+ const h = createHash8("sha256");
15798
15886
  h.update(bytes);
15799
15887
  return h.digest("hex");
15800
15888
  }
15801
15889
  function sha256File(path) {
15802
- const hash = createHash7("sha256");
15890
+ const hash = createHash8("sha256");
15803
15891
  const buffer = Buffer.allocUnsafe(1024 * 1024);
15804
15892
  const fd = openSync5(path, "r");
15805
15893
  try {
@@ -15814,7 +15902,7 @@ function sha256File(path) {
15814
15902
  return hash.digest("hex");
15815
15903
  }
15816
15904
  async function sha256FileAsync(path) {
15817
- const hash = createHash7("sha256");
15905
+ const hash = createHash8("sha256");
15818
15906
  for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
15819
15907
  hash.update(chunk);
15820
15908
  }
@@ -16585,7 +16673,7 @@ async function muxChunkWithAudio(input) {
16585
16673
  }
16586
16674
 
16587
16675
  // src/services/distributed/planSize.ts
16588
- import { createHash as createHash8 } from "node:crypto";
16676
+ import { createHash as createHash9 } from "node:crypto";
16589
16677
  import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
16590
16678
  import { extname as extname5, join as join34, relative as relative4 } from "node:path";
16591
16679
  var MAX_TOP_COMPONENTS = 10;
@@ -16615,7 +16703,7 @@ var PLAN_ROOT_CATEGORY = {
16615
16703
  "plan.json": "metadata"
16616
16704
  };
16617
16705
  function hashComponent(value) {
16618
- return createHash8("sha256").update(value).digest("hex").slice(0, 12);
16706
+ return createHash9("sha256").update(value).digest("hex").slice(0, 12);
16619
16707
  }
16620
16708
  function classifyFile(segments, rootKind) {
16621
16709
  const rootName = segments[0] ?? "";
@@ -18366,23 +18454,6 @@ import {
18366
18454
  import { parseEncoderOptions } from "@hyperframes/studio-server/encoder-options";
18367
18455
  var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
18368
18456
  var SSE_KEEPALIVE_MS = 3e4;
18369
- var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
18370
- "ASSET_MEDIA_TYPE_MISMATCH",
18371
- "HEIF_IMAGE_CONVERSION_FAILED",
18372
- "HEIF_TOOL_UNAVAILABLE",
18373
- "NOT_MEDIA_PAYLOAD",
18374
- "INVALID_VIDEO_METADATA",
18375
- "VIDEO_SOURCE_UNRENDERABLE",
18376
- "VIDEO_EXTRACTION_FAILED",
18377
- "ENCODER_INTERRUPTED",
18378
- "RENDER_TOO_SLOW",
18379
- "RENDER_STAGE_STALLED"
18380
- ]);
18381
- function extractSafeRenderErrorCode(error) {
18382
- if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
18383
- const code = error.code;
18384
- return typeof code === "string" && SAFE_RENDER_ERROR_CODES.has(code) ? code : void 0;
18385
- }
18386
18457
  function extractSafeRenderErrorMetadata(error) {
18387
18458
  const errorCode = extractSafeRenderErrorCode(error);
18388
18459
  if (!errorCode || typeof error !== "object" || error === null) return void 0;
@@ -20834,7 +20905,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20834
20905
  openAttempt,
20835
20906
  verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
20836
20907
  }),
20837
- { stall: frameStallRule(job) }
20908
+ { stall: frameStallRule(job, framesInChunk) }
20838
20909
  ).catch(async (captureError) => {
20839
20910
  await overlap.current?.abort();
20840
20911
  overlap.current = null;
@@ -21055,6 +21126,7 @@ export {
21055
21126
  defaultLogger,
21056
21127
  distributedConfigFromRequest,
21057
21128
  executeRenderJob,
21129
+ extractSafeRenderErrorCode,
21058
21130
  getCapturePerfSummary4 as getCapturePerfSummary,
21059
21131
  getCompositionDuration2 as getCompositionDuration,
21060
21132
  getDistributedRenderCapabilities,