@hyperframes/producer 0.8.199 → 0.8.201

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.
@@ -54,6 +54,7 @@ import {
54
54
  createVideoFrameInjector,
55
55
  getEncoderPreset as getEncoderPreset3,
56
56
  distributeFrames as distributeFrames2,
57
+ distributeFramesInterleaved as distributeFramesInterleaved2,
57
58
  executeParallelCapture as executeParallelCapture2,
58
59
  mergeWorkerFrames,
59
60
  getSystemTotalMb,
@@ -2114,6 +2115,7 @@ function aggregateDrawElement(perfs, de) {
2114
2115
  blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2115
2116
  blankRecaptures: drain?.blankRecaptures ?? 0,
2116
2117
  boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
2118
+ riskFrames: perfs.reduce((sum, p) => sum + (p.deRiskFrames ?? 0), 0),
2117
2119
  ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
2118
2120
  frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
2119
2121
  };
@@ -3750,11 +3752,21 @@ function createDrainFrameGuard(args) {
3750
3752
  return buf;
3751
3753
  };
3752
3754
  }
3753
- async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
3755
+ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
3754
3756
  let prev = null;
3755
3757
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
3756
3758
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3757
- const guardFrame = (idx, buf) => guard(session, idx, buf);
3759
+ const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3760
+ const captureRisk = async (idx) => {
3761
+ await risk.open();
3762
+ lastProgressAt = Date.now();
3763
+ return raceAgainstStall(
3764
+ risk.capture(idx, frameTime(idx)),
3765
+ stallTimeoutMs - (Date.now() - lastProgressAt),
3766
+ { session, frameIndex: idx, totalFrames, stallTimeoutMs },
3767
+ abortSignal
3768
+ );
3769
+ };
3758
3770
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3759
3771
  let lastProgressAt = Date.now();
3760
3772
  const captureWithStallGuard = (idx, promise) => raceAgainstStall(
@@ -3766,7 +3778,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3766
3778
  const drainPrev = async () => {
3767
3779
  if (!prev) return;
3768
3780
  assertNotAborted();
3769
- const buf = await guardFrame(prev.idx, await prev.encodeResult);
3781
+ const buf = await guardFrame(prev, await prev.encodeResult);
3770
3782
  await reorderBuffer.waitForFrame(prev.idx);
3771
3783
  ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
3772
3784
  reorderBuffer.advanceTo(prev.idx + 1);
@@ -3786,7 +3798,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3786
3798
  const drainBatch = async (batch) => {
3787
3799
  for (const item of batch) {
3788
3800
  assertNotAborted();
3789
- const buf = await guardFrame(item.idx, await item.encodeResult);
3801
+ const buf = await guardFrame(item, await item.encodeResult);
3790
3802
  await reorderBuffer.waitForFrame(item.idx);
3791
3803
  ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
3792
3804
  reorderBuffer.advanceTo(item.idx + 1);
@@ -3804,7 +3816,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3804
3816
  };
3805
3817
  if (batchN > 1 && !session.onBeforeCapture) {
3806
3818
  const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
3807
- const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
3819
+ const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
3808
3820
  let prevBatch = [];
3809
3821
  let i = 0;
3810
3822
  while (i < totalFrames) {
@@ -3821,6 +3833,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3821
3833
  );
3822
3834
  await drainBatch(prevBatch);
3823
3835
  prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
3836
+ } else if (risk?.frames.has(i)) {
3837
+ const buffer = await captureRisk(i);
3838
+ await drainBatch(prevBatch);
3839
+ prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
3840
+ i++;
3824
3841
  } else {
3825
3842
  const { encodeResult } = await captureWithStallGuard(
3826
3843
  i,
@@ -3836,6 +3853,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3836
3853
  }
3837
3854
  for (let i = 0; i < totalFrames; i++) {
3838
3855
  assertNotAborted();
3856
+ if (risk?.frames.has(i)) {
3857
+ const buffer = await captureRisk(i);
3858
+ await drainPrev();
3859
+ prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
3860
+ continue;
3861
+ }
3839
3862
  const time = frameTime(i);
3840
3863
  const { encodeResult } = await captureWithStallGuard(
3841
3864
  i,
@@ -4082,6 +4105,24 @@ async function runCaptureStreamingStage(input) {
4082
4105
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4083
4106
  probeSession = null;
4084
4107
  }
4108
+ let riskSession = null;
4109
+ const openRiskSession = async () => {
4110
+ const opened = await createCaptureSession(
4111
+ fileServer.url,
4112
+ framesDir,
4113
+ buildCaptureOptions(),
4114
+ createRenderVideoFrameInjector(),
4115
+ {
4116
+ ...captureCfg,
4117
+ useDrawElement: false,
4118
+ staticFrameDedup: false,
4119
+ enableBrowserPool: false
4120
+ }
4121
+ );
4122
+ riskSession = opened;
4123
+ await initializeSession(opened);
4124
+ return opened;
4125
+ };
4085
4126
  try {
4086
4127
  if (!session.isInitialized) {
4087
4128
  await initializeSession(session);
@@ -4096,6 +4137,7 @@ async function runCaptureStreamingStage(input) {
4096
4137
  blankDeterministicAccepts: 0,
4097
4138
  blankRecaptures: 0
4098
4139
  };
4140
+ const riskFrames = session.deRiskFrames;
4099
4141
  await runWorkerEncodePipelineLoop(
4100
4142
  session,
4101
4143
  totalFrames,
@@ -4106,7 +4148,16 @@ async function runCaptureStreamingStage(input) {
4106
4148
  onProgress,
4107
4149
  log,
4108
4150
  deDrainStats,
4109
- abortSignal
4151
+ abortSignal,
4152
+ riskFrames && riskFrames.size > 0 ? {
4153
+ frames: riskFrames,
4154
+ // Opened on the first risk frame, in a browser of its own: pages sharing a
4155
+ // browser share a compositor, which starves the drawElement page's paint events.
4156
+ open: async () => {
4157
+ riskSession ??= await openRiskSession();
4158
+ },
4159
+ capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
4160
+ } : void 0
4110
4161
  );
4111
4162
  } else {
4112
4163
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
@@ -4144,6 +4195,7 @@ async function runCaptureStreamingStage(input) {
4144
4195
  } finally {
4145
4196
  lastBrowserConsole = session.browserConsoleBuffer;
4146
4197
  await closeCaptureSession(session);
4198
+ if (riskSession) await closeCaptureSession(riskSession);
4147
4199
  }
4148
4200
  }
4149
4201
  const encodeFrom = job.progress;
@@ -4303,7 +4355,7 @@ function openSegmentedSessionReuse(input, session) {
4303
4355
  async function openSegmentedSession(input) {
4304
4356
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4305
4357
  const videoInjector = input.createRenderVideoFrameInjector();
4306
- const captureOptions = input.buildCaptureOptions();
4358
+ const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4307
4359
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4308
4360
  parallel: (input.workerCount ?? 1) > 1
4309
4361
  });
@@ -5548,6 +5600,7 @@ import {
5548
5600
  parseVideoElements,
5549
5601
  parseImageElements,
5550
5602
  parseAudioElements,
5603
+ isSelfOrAncestorHidden,
5551
5604
  resolveReferencedStart
5552
5605
  } from "@hyperframes/engine";
5553
5606
  function resolveHostEnd(host, hostStart) {
@@ -5608,6 +5661,15 @@ function collectHostWindows(html) {
5608
5661
  }
5609
5662
  return windows;
5610
5663
  }
5664
+ function collectHiddenVideoIds(html) {
5665
+ const { document: document2 } = parseHTML2(html);
5666
+ const hidden = /* @__PURE__ */ new Set();
5667
+ for (const element of document2.querySelectorAll("video")) {
5668
+ const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
5669
+ if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
5670
+ }
5671
+ return hidden;
5672
+ }
5611
5673
  function toAbsoluteWindow(start, end, window2) {
5612
5674
  const absoluteStart = resolveAbsoluteMediaStartSeconds({
5613
5675
  authoredStart: start,
@@ -5641,8 +5703,13 @@ function collectRenderMedia(html) {
5641
5703
  const windows = collectHostWindows(html);
5642
5704
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5643
5705
  const droppedMediaIds = /* @__PURE__ */ new Set();
5706
+ const hiddenIds = collectHiddenVideoIds(html);
5644
5707
  const videos = [];
5645
5708
  for (const video of parseVideoElements(html)) {
5709
+ if (hiddenIds.has(video.id)) {
5710
+ droppedMediaIds.add(video.id);
5711
+ continue;
5712
+ }
5646
5713
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5647
5714
  if (!absolute) {
5648
5715
  droppedMediaIds.add(video.id);
@@ -5852,7 +5919,7 @@ async function runAnimatedGifTranscode(request) {
5852
5919
  failureReason: result.failureReason
5853
5920
  });
5854
5921
  }
5855
- if (result.terminationReason === "deadline") {
5922
+ if (result.terminationReason === "inactivity") {
5856
5923
  throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
5857
5924
  }
5858
5925
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
@@ -6319,10 +6386,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
6319
6386
  function detectThreeDTransformUsage(html) {
6320
6387
  return THREE_D_CONTEXT_PATTERN.test(html);
6321
6388
  }
6322
- var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
6323
- function detectMixBlendModeUsage(html) {
6324
- return MIX_BLEND_MODE_PATTERN.test(html);
6325
- }
6326
6389
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6327
6390
  function detectAncestorBackgroundImage(html) {
6328
6391
  const { document: document2 } = parseHTML4(html);
@@ -6475,8 +6538,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6475
6538
  const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
6476
6539
  let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
6477
6540
  const preResolved = extractResolvedMedia(compiledHtml);
6541
+ const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
6478
6542
  const clampResults = await Promise.all(
6479
- preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
6543
+ preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
6480
6544
  const { duration: maxDuration } = await resolveMediaDuration(
6481
6545
  el.src,
6482
6546
  el.mediaStart,
@@ -7335,7 +7399,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7335
7399
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
7336
7400
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
7337
7401
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
7338
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
7339
7402
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
7340
7403
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
7341
7404
  injectTextRenderingRule(
@@ -7449,7 +7512,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7449
7512
  renderModeHints,
7450
7513
  hasShaderTransitions,
7451
7514
  usesThreeDTransforms,
7452
- usesMixBlendMode,
7453
7515
  hasAncestorBackgroundImage
7454
7516
  };
7455
7517
  }
@@ -7856,13 +7918,6 @@ async function runCompileStage(input) {
7856
7918
  "[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7857
7919
  );
7858
7920
  }
7859
- if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
7860
- cfg.useDrawElement = false;
7861
- deCompileGate = "mix_blend_mode";
7862
- log.info(
7863
- "[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7864
- );
7865
- }
7866
7921
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
7867
7922
  cfg.useDrawElement = false;
7868
7923
  deCompileGate = "ancestor_background_image";
@@ -8156,6 +8211,7 @@ async function runProbeStage(input) {
8156
8211
  height,
8157
8212
  fps: job.config.fps,
8158
8213
  format: captureFormat,
8214
+ transparent: needsAlpha,
8159
8215
  quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
8160
8216
  variables: job.config.variables,
8161
8217
  deviceScaleFactor,
@@ -9257,10 +9313,11 @@ async function constrainAacTruePeak(input) {
9257
9313
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
9258
9314
  return enforceAacTruePeak({
9259
9315
  audioPath: input.audioPath,
9316
+ sourcePath: input.sourcePath,
9260
9317
  targetDurationSeconds: input.targetDurationSeconds,
9261
9318
  signal: input.signal,
9262
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
9263
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9319
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
9320
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9264
9321
  });
9265
9322
  }
9266
9323
  async function enforceAacTruePeak(input) {
@@ -9270,6 +9327,7 @@ async function enforceAacTruePeak(input) {
9270
9327
  const correctedPath = join18(scratchDir, "audio.m4a");
9271
9328
  const measurements = [];
9272
9329
  let attenuationDb = 0;
9330
+ let sourcePeakDbfs;
9273
9331
  let measuredPath = input.audioPath;
9274
9332
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
9275
9333
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -9286,7 +9344,11 @@ async function enforceAacTruePeak(input) {
9286
9344
  }
9287
9345
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
9288
9346
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
9289
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
9347
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
9348
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
9349
+ }
9350
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
9351
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
9290
9352
  const result = await input.encode(
9291
9353
  input.audioPath,
9292
9354
  correctedPath,
@@ -9722,7 +9784,7 @@ async function runCaptureStage(input) {
9722
9784
  );
9723
9785
  }
9724
9786
  }
9725
- const captureOptions = buildCaptureOptions();
9787
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
9726
9788
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
9727
9789
  const checkDiskProjection = createDiskCaptureProjection({
9728
9790
  framesDir,
@@ -9743,6 +9805,7 @@ async function runCaptureStage(input) {
9743
9805
  createBeforeCaptureHook: createRenderVideoFrameInjector,
9744
9806
  abortSignal,
9745
9807
  frameRangeStart: frameRange?.startFrame,
9808
+ interleaveFrames: input.interleaveFrames,
9746
9809
  dedupPerfs,
9747
9810
  pageErrorLines: workerPageErrors,
9748
9811
  onProgress: (progress) => {
@@ -11258,9 +11321,6 @@ async function runEncodeStage(input) {
11258
11321
  return { encodeMs: Date.now() - stage5Start };
11259
11322
  }
11260
11323
  const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
11261
- const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
11262
- const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
11263
- const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
11264
11324
  const frameExt = frameFileExtension2(input.captureImageFormat);
11265
11325
  const framePattern = `frame_%06d.${frameExt}`;
11266
11326
  const encoderOpts = videoEncoderOptions(input, engineCfg);
@@ -11271,7 +11331,7 @@ async function runEncodeStage(input) {
11271
11331
  encoderOpts,
11272
11332
  chunkedEncodeSize,
11273
11333
  abortSignal,
11274
- videoEngineCfg,
11334
+ engineCfg,
11275
11335
  onFramesEncoded
11276
11336
  ) : await encodeFramesFromDir(
11277
11337
  framesDir,
@@ -11279,7 +11339,7 @@ async function runEncodeStage(input) {
11279
11339
  videoOnlyPath,
11280
11340
  encoderOpts,
11281
11341
  abortSignal,
11282
- videoEngineCfg,
11342
+ engineCfg,
11283
11343
  onFramesEncoded
11284
11344
  );
11285
11345
  assertNotAborted();
@@ -11621,16 +11681,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11621
11681
  }
11622
11682
  return ranges;
11623
11683
  }
11624
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
11684
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
11625
11685
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
11626
11686
  const batches = [];
11687
+ const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
11688
+ if (stride > 1) {
11689
+ const spans = /* @__PURE__ */ new Map();
11690
+ for (const range of ranges) {
11691
+ for (let frame = range.startFrame; frame < range.endFrame; frame++) {
11692
+ const span = spans.get(frame % stride);
11693
+ if (span) span.last = frame;
11694
+ else spans.set(frame % stride, { first: frame, last: frame });
11695
+ }
11696
+ }
11697
+ const strided = [...spans.values()];
11698
+ for (let i = 0; i < strided.length; i += workersPerBatch) {
11699
+ const batchIndex = batches.length;
11700
+ batches.push(
11701
+ strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
11702
+ workerId,
11703
+ startFrame: rangeStart + first,
11704
+ endFrame: rangeStart + last + 1,
11705
+ frameStride: stride,
11706
+ outputDir: outputDirFor(batchIndex, workerId),
11707
+ outputFrameOffset: rangeStart
11708
+ }))
11709
+ );
11710
+ }
11711
+ return batches;
11712
+ }
11627
11713
  for (let i = 0; i < ranges.length; i += workersPerBatch) {
11628
11714
  const batchIndex = batches.length;
11629
11715
  const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
11630
11716
  workerId,
11631
11717
  startFrame: rangeStart + range.startFrame,
11632
11718
  endFrame: rangeStart + range.endFrame,
11633
- outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11719
+ outputDir: outputDirFor(batchIndex, workerId),
11634
11720
  outputFrameOffset: rangeStart
11635
11721
  }));
11636
11722
  batches.push(batch);
@@ -11723,8 +11809,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11723
11809
  currentWorkers,
11724
11810
  attemptWorkDir,
11725
11811
  attempt,
11726
- rangeStart
11727
- ) : [distributeFrames2(options.totalFrames, currentWorkers, attemptWorkDir, rangeStart)];
11812
+ rangeStart,
11813
+ options.interleaveFrames ? options.initialWorkerCount : 1
11814
+ ) : [
11815
+ (options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
11816
+ options.totalFrames,
11817
+ currentWorkers,
11818
+ attemptWorkDir,
11819
+ rangeStart
11820
+ )
11821
+ ];
11728
11822
  options.dedupPerfs.length = 0;
11729
11823
  try {
11730
11824
  for (const tasks of batches) {
@@ -12176,6 +12270,9 @@ function resolveDeShortBand(args) {
12176
12270
  function shouldPreferSingleWorkerDrawElement(args) {
12177
12271
  return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12178
12272
  }
12273
+ function probeNeedsScreenshotFrames(session) {
12274
+ return (session?.deRiskFrames?.size ?? 0) > 0;
12275
+ }
12179
12276
  function isProbeDeGated(session) {
12180
12277
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
12181
12278
  }
@@ -12833,6 +12930,7 @@ async function executeRenderPipeline(input) {
12833
12930
  height,
12834
12931
  fps: job.config.fps,
12835
12932
  format: captureImageFormat,
12933
+ transparent: needsAlpha,
12836
12934
  quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
12837
12935
  variables: job.config.variables,
12838
12936
  deviceScaleFactor,
@@ -12854,6 +12952,7 @@ async function executeRenderPipeline(input) {
12854
12952
  // Probe-resolved duration: drawElement self-verification derives its
12855
12953
  // sample frame indices from this so they land inside the drained range.
12856
12954
  compositionDurationSeconds: job.duration,
12955
+ deFrameFallback: true,
12857
12956
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
12858
12957
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
12859
12958
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -12954,7 +13053,7 @@ async function executeRenderPipeline(input) {
12954
13053
  minFrames: deParallelMinFrames,
12955
13054
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12956
13055
  supersampling: deviceScaleFactor !== 1,
12957
- probeDeGated: isProbeDeGated(probeSession),
13056
+ probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
12958
13057
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12959
13058
  routerEnabled: deParallelRouterEnabled,
12960
13059
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12996,6 +13095,8 @@ async function executeRenderPipeline(input) {
12996
13095
  deInversionEligible = false;
12997
13096
  deParallelRouterEligible = false;
12998
13097
  deShortBand = void 0;
13098
+ } else if (probeNeedsScreenshotFrames(probeSession)) {
13099
+ deParallelRouterEligible = false;
12999
13100
  }
13000
13101
  }
13001
13102
  const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
@@ -13187,7 +13288,8 @@ async function executeRenderPipeline(input) {
13187
13288
  deParallelStreamForced
13188
13289
  );
13189
13290
  const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
13190
- if (shouldClampDefaultDrawElement({
13291
+ const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
13292
+ if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
13191
13293
  useDrawElement: cfg.useDrawElement,
13192
13294
  fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
13193
13295
  useStreamingEncode,
@@ -13861,7 +13963,8 @@ async function executeRenderPipeline(input) {
13861
13963
  abortSignal: executionSignal,
13862
13964
  assertNotAborted,
13863
13965
  onProgress,
13864
- onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
13966
+ onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
13967
+ interleaveFrames: overlap !== null
13865
13968
  })
13866
13969
  );
13867
13970
  let captureRes;
@@ -105,6 +105,54 @@ type CliOptions = {
105
105
  */
106
106
  mode: HarnessMode;
107
107
  };
108
+ type TestResult = {
109
+ suite: TestSuite;
110
+ passed: boolean;
111
+ /**
112
+ * Set when `--mode=distributed-simulated` skips a fixture that the
113
+ * distributed pipeline can't run (HDR, NTSC fps, fps outside DISTRIBUTED_FPS).
114
+ * `passed` is `true` for skipped fixtures — skipping is a clean outcome,
115
+ * not a failure — but the summary distinguishes them.
116
+ */
117
+ skipped?: {
118
+ reason: string;
119
+ };
120
+ compilation?: {
121
+ passed: boolean;
122
+ errors: string[];
123
+ warnings: string[];
124
+ };
125
+ visual?: {
126
+ passed: boolean;
127
+ failedFrames: number;
128
+ checkpoints: Array<{
129
+ time: number;
130
+ psnr: number;
131
+ passed: boolean;
132
+ }>;
133
+ };
134
+ audio?: {
135
+ passed: boolean;
136
+ correlation: number;
137
+ lagWindows: number;
138
+ /**
139
+ * Residual Overall RMS (dBFS) of `rendered - snapshot`. Present only
140
+ * when the fixture opts in via `meta.maxAudioResidualRmsDb`.
141
+ * `Number.NEGATIVE_INFINITY` ⇒ perfect cancellation. `NaN` ⇒ residual
142
+ * check could not run (missing ffmpeg, duration mismatch, ...); see
143
+ * `audio.residualError` for the reason.
144
+ */
145
+ residualRmsDb?: number;
146
+ residualError?: string;
147
+ };
148
+ streamDurationParity?: {
149
+ passed: boolean;
150
+ videoDurationSeconds: number;
151
+ audioDurationSeconds: number;
152
+ driftSeconds: number;
153
+ };
154
+ renderedOutputPath?: string;
155
+ };
108
156
  export declare function parseArgs(argv: string[]): CliOptions;
109
157
  export declare function discoverTestSuites(testsDir: string, filterNames: string[], excludeTags?: string[]): TestSuite[];
110
158
  /** The frame a checkpoint time samples. Shared so selection and lookup agree. */
@@ -154,4 +202,9 @@ export type StreamDurationParity = {
154
202
  };
155
203
  export declare function checkStreamDurationParity(videoPath: string): Promise<StreamDurationParity | null>;
156
204
  export declare function createRegressionTempRoot(suiteId: string, parent?: string): string;
205
+ export declare function runTestSuite(suite: TestSuite, options: {
206
+ update: boolean;
207
+ keepTemp: boolean;
208
+ mode: HarnessMode;
209
+ }): Promise<TestResult>;
157
210
  export {};
@@ -9,6 +9,8 @@ export declare function planFrameWeights(input: {
9
9
  width: number;
10
10
  height: number;
11
11
  }): Float64Array;
12
+ export declare function withChunkCaptureCost(slices: ChunkSliceJson[], weights: Float64Array): ChunkSliceJson[];
13
+ export declare function effectiveChunkFrames(frames: number, captureCost?: number): number;
12
14
  /**
13
15
  * Cut `[0, weights.length)` into `chunkCount` ranges of ~equal summed weight, each within
14
16
  * `[minFrames, maxFrames]` where the frame count allows. `null` when the cost is uniform.
@@ -20,8 +20,6 @@ export interface CompiledComposition {
20
20
  hasShaderTransitions: boolean;
21
21
  /** Author HTML/CSS/scripts use a CSS 3D rendering context (pre-CDN-inline scan). */
22
22
  usesThreeDTransforms: boolean;
23
- /** Author HTML/CSS use mix-blend-mode (pre-CDN-inline scan). */
24
- usesMixBlendMode: boolean;
25
23
  /** Ancestors of the composition root carry a background-image (gradient/url). */
26
24
  hasAncestorBackgroundImage: boolean;
27
25
  }
@@ -124,6 +124,8 @@ export declare function constrainAacTruePeak(input: {
124
124
  targetDurationSeconds: number;
125
125
  signal?: AbortSignal;
126
126
  timeoutMs?: number;
127
+ probeTruePeakDbfs?: (audioPath: string, signal?: AbortSignal) => Promise<number>;
128
+ encodeAac?: AacEncoder;
127
129
  }): Promise<{
128
130
  success: boolean;
129
131
  error?: string;
@@ -84,6 +84,7 @@ export interface CaptureStageInput {
84
84
  endFrame: number;
85
85
  };
86
86
  onFrameLanded?: (frame: LandedFrame) => void;
87
+ interleaveFrames?: boolean;
87
88
  }
88
89
  export interface CaptureStageResult {
89
90
  /** Final worker count after any adaptive retry. */
@@ -101,7 +101,7 @@ export interface CompileStageResult {
101
101
  */
102
102
  forceScreenshot: boolean;
103
103
  /** Low-cardinality compile-time gate that disabled default drawElement:
104
- * `3d` | `mix_blend_mode` | `shader_transitions`. Undefined when none fired. */
104
+ * `3d` | `ancestor_background_image` | `shader_transitions`. Undefined when none fired. */
105
105
  deCompileGate?: string;
106
106
  }
107
107
  export declare function runCompileStage(input: CompileStageInput): Promise<CompileStageResult>;
@@ -87,6 +87,7 @@ export interface ChunkSliceJson {
87
87
  startFrame: number;
88
88
  /** Exclusive upper bound — chunk workers iterate frames in `[startFrame, endFrame)`. */
89
89
  endFrame: number;
90
+ captureCost?: number;
90
91
  }
91
92
  /**
92
93
  * Inputs to `freezePlan`. `planDir` already contains `compiled/`,
@@ -6,6 +6,8 @@
6
6
  * and placed by the composition hosts it sits inside.
7
7
  */
8
8
  import { type VideoElement, type ImageElement, type AudioElement } from "@hyperframes/engine";
9
+ /** Ids, as the parsers report them, of videos that are `data-hidden` or sit inside such an element. */
10
+ export declare function collectHiddenVideoIds(html: string): Set<string>;
9
11
  export interface RenderMedia {
10
12
  videos: VideoElement[];
11
13
  audios: AudioElement[];