@hyperframes/producer 0.8.214 → 0.8.216

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
@@ -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
@@ -14206,7 +14267,7 @@ async function executeRenderPipeline(input) {
14206
14267
  onProgress
14207
14268
  });
14208
14269
  },
14209
- { stall: frameStallRule(job) }
14270
+ { stall: frameStallRule(job, totalFrames) }
14210
14271
  );
14211
14272
  lastBrowserConsole = hdrRes.lastBrowserConsole;
14212
14273
  layeredCaptureWarnings.push(...hdrRes.warnings);
@@ -14651,7 +14712,7 @@ async function executeRenderPipeline(input) {
14651
14712
  interleaveFrames: overlap !== null,
14652
14713
  openAttempt
14653
14714
  }),
14654
- { stall: frameStallRule(job) }
14715
+ { stall: frameStallRule(job, totalFrames) }
14655
14716
  );
14656
14717
  let captureRes;
14657
14718
  try {
@@ -15742,12 +15803,12 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
15742
15803
  }
15743
15804
 
15744
15805
  // src/services/render/stages/planHash.ts
15745
- import { createHash as createHash7 } from "node:crypto";
15806
+ import { createHash as createHash8 } from "node:crypto";
15746
15807
  import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync5, readSync as readSync4 } from "node:fs";
15747
15808
  var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
15748
15809
  var FIELD_DELIMITER = Buffer.from([0]);
15749
15810
  function computePlanHash(input) {
15750
- const hash = createHash7("sha256");
15811
+ const hash = createHash8("sha256");
15751
15812
  hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
15752
15813
  hash.update(input.compositionHtml);
15753
15814
  hash.update(FIELD_DELIMITER);
@@ -15794,12 +15855,12 @@ function canonicalJsonStringify(value) {
15794
15855
  throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
15795
15856
  }
15796
15857
  function sha256Hex(bytes) {
15797
- const h = createHash7("sha256");
15858
+ const h = createHash8("sha256");
15798
15859
  h.update(bytes);
15799
15860
  return h.digest("hex");
15800
15861
  }
15801
15862
  function sha256File(path) {
15802
- const hash = createHash7("sha256");
15863
+ const hash = createHash8("sha256");
15803
15864
  const buffer = Buffer.allocUnsafe(1024 * 1024);
15804
15865
  const fd = openSync5(path, "r");
15805
15866
  try {
@@ -15814,7 +15875,7 @@ function sha256File(path) {
15814
15875
  return hash.digest("hex");
15815
15876
  }
15816
15877
  async function sha256FileAsync(path) {
15817
- const hash = createHash7("sha256");
15878
+ const hash = createHash8("sha256");
15818
15879
  for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
15819
15880
  hash.update(chunk);
15820
15881
  }
@@ -16585,7 +16646,7 @@ async function muxChunkWithAudio(input) {
16585
16646
  }
16586
16647
 
16587
16648
  // src/services/distributed/planSize.ts
16588
- import { createHash as createHash8 } from "node:crypto";
16649
+ import { createHash as createHash9 } from "node:crypto";
16589
16650
  import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
16590
16651
  import { extname as extname5, join as join34, relative as relative4 } from "node:path";
16591
16652
  var MAX_TOP_COMPONENTS = 10;
@@ -16615,7 +16676,7 @@ var PLAN_ROOT_CATEGORY = {
16615
16676
  "plan.json": "metadata"
16616
16677
  };
16617
16678
  function hashComponent(value) {
16618
- return createHash8("sha256").update(value).digest("hex").slice(0, 12);
16679
+ return createHash9("sha256").update(value).digest("hex").slice(0, 12);
16619
16680
  }
16620
16681
  function classifyFile(segments, rootKind) {
16621
16682
  const rootName = segments[0] ?? "";
@@ -20834,7 +20895,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20834
20895
  openAttempt,
20835
20896
  verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
20836
20897
  }),
20837
- { stall: frameStallRule(job) }
20898
+ { stall: frameStallRule(job, framesInChunk) }
20838
20899
  ).catch(async (captureError) => {
20839
20900
  await overlap.current?.abort();
20840
20901
  overlap.current = null;
@@ -3707,6 +3707,10 @@ function resolveCaptureStallTimeoutMs() {
3707
3707
  const parsed = raw ? Number(raw) : Number.NaN;
3708
3708
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
3709
3709
  }
3710
+ function stallBudgetMs(stallTimeoutMs, framesDone, beginFrame, cfg) {
3711
+ const deadline = cfg.beginFrameFirstFrameDeadlineMs;
3712
+ return beginFrame && framesDone === 0 && deadline ? Math.min(stallTimeoutMs, deadline) : stallTimeoutMs;
3713
+ }
3710
3714
  function captureModeLabel(mode) {
3711
3715
  if (mode === "drawelement") return "drawElement";
3712
3716
  if (mode === "beginframe") return "BeginFrame";
@@ -4079,14 +4083,20 @@ async function runCaptureStreamingStage(input) {
4079
4083
  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}`;
4080
4084
  const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
4081
4085
  let stalled = false;
4086
+ const budget = () => stallBudgetMs(
4087
+ stallTimeoutMs,
4088
+ resumeFrom + lastCapturedFrames,
4089
+ captureLabel === "BeginFrame",
4090
+ cfg
4091
+ );
4082
4092
  const stallTimer = setInterval(
4083
4093
  () => {
4084
- if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
4094
+ if (!watchdogArmed || Date.now() - lastProgressAt <= budget()) return;
4085
4095
  stalled = true;
4086
4096
  const stallErr = new ParallelCaptureStallError({
4087
4097
  variant: "drain",
4088
4098
  captureLabel,
4089
- stallTimeoutMs,
4099
+ stallTimeoutMs: budget(),
4090
4100
  lastCapturedFrames: resumeFrom + lastCapturedFrames,
4091
4101
  totalFrames,
4092
4102
  phaseSummary: phaseSummary()
@@ -4094,7 +4104,7 @@ async function runCaptureStreamingStage(input) {
4094
4104
  parallelReorder.abort(stallErr);
4095
4105
  stallController.abort();
4096
4106
  },
4097
- Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
4107
+ Math.min(budget(), DE_STALL_POLL_MS)
4098
4108
  );
4099
4109
  try {
4100
4110
  workerResults = await executeParallelCapture(
@@ -4175,7 +4185,7 @@ async function runCaptureStreamingStage(input) {
4175
4185
  throw new ParallelCaptureStallError({
4176
4186
  variant: "pool",
4177
4187
  captureLabel,
4178
- stallTimeoutMs,
4188
+ stallTimeoutMs: budget(),
4179
4189
  lastCapturedFrames: resumeFrom + lastCapturedFrames,
4180
4190
  totalFrames,
4181
4191
  phaseSummary: phaseSummary()
@@ -4276,10 +4286,16 @@ async function runCaptureStreamingStage(input) {
4276
4286
  for (let i = 0; i < totalFrames; i++) {
4277
4287
  assertNotAborted();
4278
4288
  const time = i * job.config.fps.den / job.config.fps.num;
4289
+ const budget = stallBudgetMs(
4290
+ stallTimeoutMs,
4291
+ i,
4292
+ session.captureMode === "beginframe",
4293
+ cfg
4294
+ );
4279
4295
  const { buffer } = await raceAgainstStall(
4280
4296
  captureFrameToBuffer(session, i, time),
4281
- stallTimeoutMs - (Date.now() - lastProgressAt),
4282
- { session, frameIndex: i, totalFrames, stallTimeoutMs },
4297
+ budget - (Date.now() - lastProgressAt),
4298
+ { session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
4283
4299
  abortSignal
4284
4300
  );
4285
4301
  await reorderBuffer.waitForFrame(i);
@@ -5497,6 +5513,7 @@ function watchStall(phase, rule) {
5497
5513
  let lastChangeAt = Date.now();
5498
5514
  let attempt;
5499
5515
  let timer;
5516
+ let inTail = false;
5500
5517
  const restartWindow = (ms) => {
5501
5518
  last = rule.progress();
5502
5519
  budgetMs = ms;
@@ -5506,6 +5523,10 @@ function watchStall(phase, rule) {
5506
5523
  timer = setInterval(
5507
5524
  () => {
5508
5525
  const current = rule.progress();
5526
+ if (rule.complete?.() && !inTail) {
5527
+ inTail = true;
5528
+ return restartWindow(rule.tailMs ?? rule.noProgressMs);
5529
+ }
5509
5530
  if (current !== last) return restartWindow(rule.noProgressMs);
5510
5531
  if (Date.now() - lastChangeAt < budgetMs) return;
5511
5532
  const error = new RenderStageStalledError(phase, budgetMs, current);
@@ -5521,6 +5542,7 @@ function watchStall(phase, rule) {
5521
5542
  });
5522
5543
  const openAttempt = (parent) => {
5523
5544
  attempt = new AbortController();
5545
+ inTail = false;
5524
5546
  restartWindow(rule.firstProgressMs);
5525
5547
  return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
5526
5548
  };
@@ -8512,7 +8534,8 @@ async function runCompileStage(input) {
8512
8534
  }
8513
8535
 
8514
8536
  // src/services/render/stages/probeStage.ts
8515
- import { join as join17 } from "node:path";
8537
+ import { createHash as createHash7 } from "node:crypto";
8538
+ import { basename as basename5, join as join17 } from "node:path";
8516
8539
  import { parseHTML as parseHTML6 } from "linkedom";
8517
8540
  import {
8518
8541
  closeCaptureSession as closeCaptureSession4,
@@ -8520,6 +8543,7 @@ import {
8520
8543
  createCaptureSession as createCaptureSession4,
8521
8544
  deriveBeginFrameProbeTimeTicks,
8522
8545
  getCompositionDuration,
8546
+ getCompositionLengthSource,
8523
8547
  initializeSession as initializeSession4,
8524
8548
  isTransientBrowserError,
8525
8549
  probeBeginFrameLiveness,
@@ -8658,6 +8682,13 @@ function hasRuntimeMediaChanges(html) {
8658
8682
  function probeRequiresBrowser(args) {
8659
8683
  return args.durationSeconds <= 0 || args.unresolvedCompositionCount > 0 || args.hasAutoStart || args.hasScriptedAudio || args.hasVariableMedia || args.hasInsertedMedia;
8660
8684
  }
8685
+ function reportedLengthSource(source) {
8686
+ if (!source) return null;
8687
+ if (source.from !== "media") return { from: source.from };
8688
+ const name = basename5(source.src.split(/[?#]/, 1)[0] ?? "");
8689
+ const srcHash = createHash7("sha256").update(name).digest("hex").slice(0, 12);
8690
+ return { from: "media", tag: source.tag, trimmed: source.trimmed, srcHash };
8691
+ }
8661
8692
  async function runProbeStage(input) {
8662
8693
  const {
8663
8694
  projectDir,
@@ -8679,6 +8710,7 @@ async function runProbeStage(input) {
8679
8710
  const probeCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
8680
8711
  let fileServer = null;
8681
8712
  let probeSession = null;
8713
+ let lengthSource = input.composition.duration > 0 ? { from: "declared" } : null;
8682
8714
  let beginFrameStalled = false;
8683
8715
  let lastBrowserConsole = [];
8684
8716
  const probeStart = Date.now();
@@ -8840,10 +8872,12 @@ async function runProbeStage(input) {
8840
8872
  if (composition.duration <= 0) {
8841
8873
  log.info("Discovering composition duration...");
8842
8874
  const discoveredDuration = await getCompositionDuration(session);
8875
+ lengthSource = reportedLengthSource(await getCompositionLengthSource(session));
8843
8876
  assertNotAborted();
8844
8877
  log.info("Probed composition duration from browser", {
8845
8878
  discoveredDuration,
8846
- staticDuration: compiled.staticDuration
8879
+ staticDuration: compiled.staticDuration,
8880
+ lengthSource
8847
8881
  });
8848
8882
  composition.duration = discoveredDuration;
8849
8883
  } else {
@@ -9132,6 +9166,7 @@ async function runProbeStage(input) {
9132
9166
  probeSession,
9133
9167
  lastBrowserConsole,
9134
9168
  duration,
9169
+ lengthSource,
9135
9170
  totalFrames,
9136
9171
  browserProbeMs,
9137
9172
  beginFrameStalled
@@ -9619,7 +9654,7 @@ function checkedDbfs(value) {
9619
9654
  }
9620
9655
  return value;
9621
9656
  }
9622
- async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
9657
+ async function scanTruePeak(audioPath, trim, timeoutMs, signal, seekSeconds = 0) {
9623
9658
  let firstBlock;
9624
9659
  let laterBlocks = Number.NEGATIVE_INFINITY;
9625
9660
  let partialLine = "";
@@ -9635,7 +9670,21 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
9635
9670
  }
9636
9671
  };
9637
9672
  const result = await runFfmpeg4(
9638
- ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
9673
+ [
9674
+ ...seekSeconds > 0 ? ["-ss", String(seekSeconds)] : [],
9675
+ ...[
9676
+ "-i",
9677
+ audioPath,
9678
+ "-map",
9679
+ "0:a:0",
9680
+ "-vn",
9681
+ "-af",
9682
+ `${trim}ebur128=peak=true`,
9683
+ "-f",
9684
+ "null",
9685
+ "-"
9686
+ ]
9687
+ ],
9639
9688
  { signal, timeout: timeoutMs, onStderr: readLines }
9640
9689
  );
9641
9690
  if (!result.success) {
@@ -9661,12 +9710,15 @@ async function measureTruePeakDbfs(audioPath, options) {
9661
9710
  const scans = await Promise.all(
9662
9711
  Array.from({ length: count }, (_, k) => {
9663
9712
  const start = k === 0 ? 0 : cut(k) - block;
9664
- const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
9713
+ const seekSeconds = Math.max(0, Math.floor(start / sampleRate) - 1);
9714
+ const skipped = seekSeconds * sampleRate;
9715
+ const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1) - skipped}`;
9665
9716
  return scanTruePeak(
9666
9717
  audioPath,
9667
- `atrim=start_sample=${start}${end},`,
9718
+ `atrim=start_sample=${start - skipped}${end},`,
9668
9719
  timeoutMs,
9669
- segmentSignal
9720
+ segmentSignal,
9721
+ seekSeconds
9670
9722
  );
9671
9723
  })
9672
9724
  ).catch((error) => {
@@ -12295,12 +12347,15 @@ function resetCaptureAttemptProgress(job) {
12295
12347
  job.framesRendered = 0;
12296
12348
  }
12297
12349
  var FIRST_FRAME_STALL_MS = 3e5;
12298
- function frameStallRule(job) {
12350
+ var TAIL_STALL_MS = FIRST_FRAME_STALL_MS;
12351
+ function frameStallRule(job, totalFrames) {
12299
12352
  const noProgressMs = resolveCaptureStallTimeoutMs();
12300
12353
  return {
12301
12354
  progress: () => job.framesRendered ?? 0,
12302
12355
  noProgressMs,
12303
- firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
12356
+ firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs),
12357
+ complete: () => (job.framesRendered ?? 0) >= totalFrames,
12358
+ tailMs: Math.max(TAIL_STALL_MS, noProgressMs)
12304
12359
  };
12305
12360
  }
12306
12361
  function isRecoverableParallelCaptureError(error) {
@@ -13317,6 +13372,12 @@ async function executeRenderPipeline(input) {
13317
13372
  }
13318
13373
  observability.checkpoint("browser_probe", "duration resolved", {
13319
13374
  durationSeconds: probeResult.duration,
13375
+ lengthFrom: probeResult.lengthSource?.from ?? null,
13376
+ ...probeResult.lengthSource?.from === "media" ? {
13377
+ lengthTag: probeResult.lengthSource.tag,
13378
+ lengthTrimmed: probeResult.lengthSource.trimmed,
13379
+ lengthSrcHash: probeResult.lengthSource.srcHash
13380
+ } : {},
13320
13381
  totalFrames,
13321
13382
  compositionHash,
13322
13383
  beginFrameStalled: probeResult.beginFrameStalled
@@ -14186,7 +14247,7 @@ async function executeRenderPipeline(input) {
14186
14247
  onProgress
14187
14248
  });
14188
14249
  },
14189
- { stall: frameStallRule(job) }
14250
+ { stall: frameStallRule(job, totalFrames) }
14190
14251
  );
14191
14252
  lastBrowserConsole = hdrRes.lastBrowserConsole;
14192
14253
  layeredCaptureWarnings.push(...hdrRes.warnings);
@@ -14631,7 +14692,7 @@ async function executeRenderPipeline(input) {
14631
14692
  interleaveFrames: overlap !== null,
14632
14693
  openAttempt
14633
14694
  }),
14634
- { stall: frameStallRule(job) }
14695
+ { stall: frameStallRule(job, totalFrames) }
14635
14696
  );
14636
14697
  let captureRes;
14637
14698
  try {
@@ -333,6 +333,10 @@ export interface StageStallRule {
333
333
  noProgressMs: number;
334
334
  /** Allowed silence before the current attempt's first change. */
335
335
  firstProgressMs: number;
336
+ /** True once every unit of work has landed: nothing more can change `progress()`, only the tail work runs. */
337
+ complete?: () => boolean;
338
+ /** Allowed silence once `complete()` holds; defaults to `noProgressMs`. */
339
+ tailMs?: number;
336
340
  }
337
341
  /**
338
342
  * Classified like the per-call protocol timeout it replaces, so the capture retry ladder retries it.
@@ -26,7 +26,7 @@
26
26
  * regex, the same first-10/first-5 slicing, and the same `console.warn`
27
27
  * prefix.
28
28
  */
29
- import { type CaptureSession, type EngineConfig, type SubTimelineWaitMemo } from "@hyperframes/engine";
29
+ import { type CaptureSession, type CompositionLengthSource, type EngineConfig, type SubTimelineWaitMemo } from "@hyperframes/engine";
30
30
  import type { CompiledComposition } from "../../htmlCompiler.js";
31
31
  import { type FileServerHandle } from "../../fileServer.js";
32
32
  import type { ProducerLogger } from "../../../logger.js";
@@ -67,6 +67,8 @@ export interface ProbeStageResult {
67
67
  lastBrowserConsole: string[];
68
68
  /** Composition duration (post-probe). Guaranteed > 0 — the stage throws on <= 0. */
69
69
  duration: number;
70
+ /** Which input set `duration`; null when the runtime did not say. */
71
+ lengthSource: ReportedLengthSource | null;
70
72
  totalFrames: number;
71
73
  /** Wall-clock ms for the entire probe phase (near-zero when `needsBrowser` was false). */
72
74
  browserProbeMs: number;
@@ -113,6 +115,16 @@ export declare function probeRequiresBrowser(args: {
113
115
  hasVariableMedia: boolean;
114
116
  hasInsertedMedia: boolean;
115
117
  }): boolean;
118
+ export type ReportedLengthSource = {
119
+ from: Exclude<CompositionLengthSource["from"], "media">;
120
+ } | {
121
+ from: "media";
122
+ tag: string;
123
+ trimmed: boolean;
124
+ srcHash: string;
125
+ };
126
+ /** The length source with a media clip's src reduced to a hash of its file name, so no path leaves the render. */
127
+ export declare function reportedLengthSource(source: CompositionLengthSource | null): ReportedLengthSource | null;
116
128
  export declare function runProbeStage(input: ProbeStageInput): Promise<ProbeStageResult>;
117
129
  /**
118
130
  * Preview/render parity for `muted` media: the runtime keeps muted elements
@@ -567,10 +567,11 @@ export declare function captureAttemptMadeProgress(attemptTargetFrameCount: numb
567
567
  export declare function resetCaptureAttemptProgress(job: {
568
568
  framesRendered?: number;
569
569
  }): void;
570
- /** The no-progress rule for capture stages that have no watchdog of their own: disk, HDR, chunk. */
570
+ /** The no-progress rule for capture stages that have no watchdog of their own: disk, HDR, chunk. Once
571
+ * `totalFrames` have landed the frame count can no longer change, so the stage is judged on the tail budget. */
571
572
  export declare function frameStallRule(job: {
572
573
  framesRendered?: number;
573
- }): StageStallRule;
574
+ }, totalFrames: number): StageStallRule;
574
575
  export declare function isRecoverableParallelCaptureError(error: unknown): boolean;
575
576
  /**
576
577
  * Turn a cryptic memory-exhaustion failure (V8 `Set maximum size exceeded`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.214",
3
+ "version": "0.8.216",
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.11.0",
37
37
  "puppeteer-core": "^25.11.0",
38
- "@hyperframes/lint": "0.8.214",
39
- "@hyperframes/core": "0.8.214",
40
- "@hyperframes/engine": "0.8.214",
41
- "@hyperframes/parsers": "0.8.214",
42
- "@hyperframes/studio-server": "0.8.214"
38
+ "@hyperframes/core": "0.8.216",
39
+ "@hyperframes/engine": "0.8.216",
40
+ "@hyperframes/lint": "0.8.216",
41
+ "@hyperframes/studio-server": "0.8.216",
42
+ "@hyperframes/parsers": "0.8.216"
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.214"
51
+ "@hyperframes/aws-lambda": "0.8.216"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"