@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.
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  createVideoFrameInjector,
34
34
  getEncoderPreset as getEncoderPreset3,
35
35
  distributeFrames as distributeFrames2,
36
+ distributeFramesInterleaved as distributeFramesInterleaved2,
36
37
  executeParallelCapture as executeParallelCapture2,
37
38
  mergeWorkerFrames,
38
39
  getSystemTotalMb,
@@ -2094,6 +2095,7 @@ function aggregateDrawElement(perfs, de) {
2094
2095
  blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2095
2096
  blankRecaptures: drain?.blankRecaptures ?? 0,
2096
2097
  boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
2098
+ riskFrames: perfs.reduce((sum2, p) => sum2 + (p.deRiskFrames ?? 0), 0),
2097
2099
  ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
2098
2100
  frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
2099
2101
  };
@@ -3730,11 +3732,21 @@ function createDrainFrameGuard(args) {
3730
3732
  return buf;
3731
3733
  };
3732
3734
  }
3733
- async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
3735
+ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
3734
3736
  let prev = null;
3735
3737
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
3736
3738
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3737
- const guardFrame = (idx, buf) => guard(session, idx, buf);
3739
+ const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3740
+ const captureRisk = async (idx) => {
3741
+ await risk.open();
3742
+ lastProgressAt = Date.now();
3743
+ return raceAgainstStall(
3744
+ risk.capture(idx, frameTime(idx)),
3745
+ stallTimeoutMs - (Date.now() - lastProgressAt),
3746
+ { session, frameIndex: idx, totalFrames, stallTimeoutMs },
3747
+ abortSignal
3748
+ );
3749
+ };
3738
3750
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3739
3751
  let lastProgressAt = Date.now();
3740
3752
  const captureWithStallGuard = (idx, promise) => raceAgainstStall(
@@ -3746,7 +3758,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3746
3758
  const drainPrev = async () => {
3747
3759
  if (!prev) return;
3748
3760
  assertNotAborted();
3749
- const buf = await guardFrame(prev.idx, await prev.encodeResult);
3761
+ const buf = await guardFrame(prev, await prev.encodeResult);
3750
3762
  await reorderBuffer.waitForFrame(prev.idx);
3751
3763
  ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
3752
3764
  reorderBuffer.advanceTo(prev.idx + 1);
@@ -3766,7 +3778,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3766
3778
  const drainBatch = async (batch) => {
3767
3779
  for (const item of batch) {
3768
3780
  assertNotAborted();
3769
- const buf = await guardFrame(item.idx, await item.encodeResult);
3781
+ const buf = await guardFrame(item, await item.encodeResult);
3770
3782
  await reorderBuffer.waitForFrame(item.idx);
3771
3783
  ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
3772
3784
  reorderBuffer.advanceTo(item.idx + 1);
@@ -3784,7 +3796,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3784
3796
  };
3785
3797
  if (batchN > 1 && !session.onBeforeCapture) {
3786
3798
  const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
3787
- const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
3799
+ const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
3788
3800
  let prevBatch = [];
3789
3801
  let i = 0;
3790
3802
  while (i < totalFrames) {
@@ -3801,6 +3813,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3801
3813
  );
3802
3814
  await drainBatch(prevBatch);
3803
3815
  prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
3816
+ } else if (risk?.frames.has(i)) {
3817
+ const buffer = await captureRisk(i);
3818
+ await drainBatch(prevBatch);
3819
+ prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
3820
+ i++;
3804
3821
  } else {
3805
3822
  const { encodeResult } = await captureWithStallGuard(
3806
3823
  i,
@@ -3816,6 +3833,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3816
3833
  }
3817
3834
  for (let i = 0; i < totalFrames; i++) {
3818
3835
  assertNotAborted();
3836
+ if (risk?.frames.has(i)) {
3837
+ const buffer = await captureRisk(i);
3838
+ await drainPrev();
3839
+ prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
3840
+ continue;
3841
+ }
3819
3842
  const time = frameTime(i);
3820
3843
  const { encodeResult } = await captureWithStallGuard(
3821
3844
  i,
@@ -4062,6 +4085,24 @@ async function runCaptureStreamingStage(input) {
4062
4085
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4063
4086
  probeSession = null;
4064
4087
  }
4088
+ let riskSession = null;
4089
+ const openRiskSession = async () => {
4090
+ const opened = await createCaptureSession(
4091
+ fileServer.url,
4092
+ framesDir,
4093
+ buildCaptureOptions(),
4094
+ createRenderVideoFrameInjector(),
4095
+ {
4096
+ ...captureCfg,
4097
+ useDrawElement: false,
4098
+ staticFrameDedup: false,
4099
+ enableBrowserPool: false
4100
+ }
4101
+ );
4102
+ riskSession = opened;
4103
+ await initializeSession(opened);
4104
+ return opened;
4105
+ };
4065
4106
  try {
4066
4107
  if (!session.isInitialized) {
4067
4108
  await initializeSession(session);
@@ -4076,6 +4117,7 @@ async function runCaptureStreamingStage(input) {
4076
4117
  blankDeterministicAccepts: 0,
4077
4118
  blankRecaptures: 0
4078
4119
  };
4120
+ const riskFrames = session.deRiskFrames;
4079
4121
  await runWorkerEncodePipelineLoop(
4080
4122
  session,
4081
4123
  totalFrames,
@@ -4086,7 +4128,16 @@ async function runCaptureStreamingStage(input) {
4086
4128
  onProgress,
4087
4129
  log,
4088
4130
  deDrainStats,
4089
- abortSignal
4131
+ abortSignal,
4132
+ riskFrames && riskFrames.size > 0 ? {
4133
+ frames: riskFrames,
4134
+ // Opened on the first risk frame, in a browser of its own: pages sharing a
4135
+ // browser share a compositor, which starves the drawElement page's paint events.
4136
+ open: async () => {
4137
+ riskSession ??= await openRiskSession();
4138
+ },
4139
+ capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
4140
+ } : void 0
4090
4141
  );
4091
4142
  } else {
4092
4143
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
@@ -4124,6 +4175,7 @@ async function runCaptureStreamingStage(input) {
4124
4175
  } finally {
4125
4176
  lastBrowserConsole = session.browserConsoleBuffer;
4126
4177
  await closeCaptureSession(session);
4178
+ if (riskSession) await closeCaptureSession(riskSession);
4127
4179
  }
4128
4180
  }
4129
4181
  const encodeFrom = job.progress;
@@ -4283,7 +4335,7 @@ function openSegmentedSessionReuse(input, session) {
4283
4335
  async function openSegmentedSession(input) {
4284
4336
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4285
4337
  const videoInjector = input.createRenderVideoFrameInjector();
4286
- const captureOptions = input.buildCaptureOptions();
4338
+ const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4287
4339
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4288
4340
  parallel: (input.workerCount ?? 1) > 1
4289
4341
  });
@@ -5528,6 +5580,7 @@ import {
5528
5580
  parseVideoElements,
5529
5581
  parseImageElements,
5530
5582
  parseAudioElements,
5583
+ isSelfOrAncestorHidden,
5531
5584
  resolveReferencedStart
5532
5585
  } from "@hyperframes/engine";
5533
5586
  function resolveHostEnd(host, hostStart) {
@@ -5588,6 +5641,15 @@ function collectHostWindows(html) {
5588
5641
  }
5589
5642
  return windows;
5590
5643
  }
5644
+ function collectHiddenVideoIds(html) {
5645
+ const { document: document2 } = parseHTML2(html);
5646
+ const hidden = /* @__PURE__ */ new Set();
5647
+ for (const element of document2.querySelectorAll("video")) {
5648
+ const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
5649
+ if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
5650
+ }
5651
+ return hidden;
5652
+ }
5591
5653
  function toAbsoluteWindow(start, end, window2) {
5592
5654
  const absoluteStart = resolveAbsoluteMediaStartSeconds({
5593
5655
  authoredStart: start,
@@ -5621,8 +5683,13 @@ function collectRenderMedia(html) {
5621
5683
  const windows = collectHostWindows(html);
5622
5684
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5623
5685
  const droppedMediaIds = /* @__PURE__ */ new Set();
5686
+ const hiddenIds = collectHiddenVideoIds(html);
5624
5687
  const videos = [];
5625
5688
  for (const video of parseVideoElements(html)) {
5689
+ if (hiddenIds.has(video.id)) {
5690
+ droppedMediaIds.add(video.id);
5691
+ continue;
5692
+ }
5626
5693
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5627
5694
  if (!absolute) {
5628
5695
  droppedMediaIds.add(video.id);
@@ -5832,7 +5899,7 @@ async function runAnimatedGifTranscode(request) {
5832
5899
  failureReason: result.failureReason
5833
5900
  });
5834
5901
  }
5835
- if (result.terminationReason === "deadline") {
5902
+ if (result.terminationReason === "inactivity") {
5836
5903
  throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
5837
5904
  }
5838
5905
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
@@ -6299,10 +6366,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
6299
6366
  function detectThreeDTransformUsage(html) {
6300
6367
  return THREE_D_CONTEXT_PATTERN.test(html);
6301
6368
  }
6302
- var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
6303
- function detectMixBlendModeUsage(html) {
6304
- return MIX_BLEND_MODE_PATTERN.test(html);
6305
- }
6306
6369
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6307
6370
  function detectAncestorBackgroundImage(html) {
6308
6371
  const { document: document2 } = parseHTML4(html);
@@ -6455,8 +6518,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6455
6518
  const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
6456
6519
  let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
6457
6520
  const preResolved = extractResolvedMedia(compiledHtml);
6521
+ const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
6458
6522
  const clampResults = await Promise.all(
6459
- preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
6523
+ preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
6460
6524
  const { duration: maxDuration } = await resolveMediaDuration(
6461
6525
  el.src,
6462
6526
  el.mediaStart,
@@ -7315,7 +7379,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7315
7379
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
7316
7380
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
7317
7381
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
7318
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
7319
7382
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
7320
7383
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
7321
7384
  injectTextRenderingRule(
@@ -7429,7 +7492,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7429
7492
  renderModeHints,
7430
7493
  hasShaderTransitions,
7431
7494
  usesThreeDTransforms,
7432
- usesMixBlendMode,
7433
7495
  hasAncestorBackgroundImage
7434
7496
  };
7435
7497
  }
@@ -7836,13 +7898,6 @@ async function runCompileStage(input) {
7836
7898
  "[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."
7837
7899
  );
7838
7900
  }
7839
- if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
7840
- cfg.useDrawElement = false;
7841
- deCompileGate = "mix_blend_mode";
7842
- log.info(
7843
- "[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7844
- );
7845
- }
7846
7901
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
7847
7902
  cfg.useDrawElement = false;
7848
7903
  deCompileGate = "ancestor_background_image";
@@ -8136,6 +8191,7 @@ async function runProbeStage(input) {
8136
8191
  height,
8137
8192
  fps: job.config.fps,
8138
8193
  format: captureFormat,
8194
+ transparent: needsAlpha,
8139
8195
  quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
8140
8196
  variables: job.config.variables,
8141
8197
  deviceScaleFactor,
@@ -9273,10 +9329,11 @@ async function constrainAacTruePeak(input) {
9273
9329
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
9274
9330
  return enforceAacTruePeak({
9275
9331
  audioPath: input.audioPath,
9332
+ sourcePath: input.sourcePath,
9276
9333
  targetDurationSeconds: input.targetDurationSeconds,
9277
9334
  signal: input.signal,
9278
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
9279
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9335
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
9336
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9280
9337
  });
9281
9338
  }
9282
9339
  async function enforceAacTruePeak(input) {
@@ -9286,6 +9343,7 @@ async function enforceAacTruePeak(input) {
9286
9343
  const correctedPath = join18(scratchDir, "audio.m4a");
9287
9344
  const measurements = [];
9288
9345
  let attenuationDb = 0;
9346
+ let sourcePeakDbfs;
9289
9347
  let measuredPath = input.audioPath;
9290
9348
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
9291
9349
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -9302,7 +9360,11 @@ async function enforceAacTruePeak(input) {
9302
9360
  }
9303
9361
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
9304
9362
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
9305
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
9363
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
9364
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
9365
+ }
9366
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
9367
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
9306
9368
  const result = await input.encode(
9307
9369
  input.audioPath,
9308
9370
  correctedPath,
@@ -9738,7 +9800,7 @@ async function runCaptureStage(input) {
9738
9800
  );
9739
9801
  }
9740
9802
  }
9741
- const captureOptions = buildCaptureOptions();
9803
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
9742
9804
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
9743
9805
  const checkDiskProjection = createDiskCaptureProjection({
9744
9806
  framesDir,
@@ -9759,6 +9821,7 @@ async function runCaptureStage(input) {
9759
9821
  createBeforeCaptureHook: createRenderVideoFrameInjector,
9760
9822
  abortSignal,
9761
9823
  frameRangeStart: frameRange?.startFrame,
9824
+ interleaveFrames: input.interleaveFrames,
9762
9825
  dedupPerfs,
9763
9826
  pageErrorLines: workerPageErrors,
9764
9827
  onProgress: (progress) => {
@@ -11274,9 +11337,6 @@ async function runEncodeStage(input) {
11274
11337
  return { encodeMs: Date.now() - stage5Start };
11275
11338
  }
11276
11339
  const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
11277
- const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
11278
- const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
11279
- const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
11280
11340
  const frameExt = frameFileExtension2(input.captureImageFormat);
11281
11341
  const framePattern = `frame_%06d.${frameExt}`;
11282
11342
  const encoderOpts = videoEncoderOptions(input, engineCfg);
@@ -11287,7 +11347,7 @@ async function runEncodeStage(input) {
11287
11347
  encoderOpts,
11288
11348
  chunkedEncodeSize,
11289
11349
  abortSignal,
11290
- videoEngineCfg,
11350
+ engineCfg,
11291
11351
  onFramesEncoded
11292
11352
  ) : await encodeFramesFromDir(
11293
11353
  framesDir,
@@ -11295,7 +11355,7 @@ async function runEncodeStage(input) {
11295
11355
  videoOnlyPath,
11296
11356
  encoderOpts,
11297
11357
  abortSignal,
11298
- videoEngineCfg,
11358
+ engineCfg,
11299
11359
  onFramesEncoded
11300
11360
  );
11301
11361
  assertNotAborted();
@@ -11637,16 +11697,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11637
11697
  }
11638
11698
  return ranges;
11639
11699
  }
11640
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
11700
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
11641
11701
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
11642
11702
  const batches = [];
11703
+ const outputDirFor = (batchIndex, workerId) => join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
11704
+ if (stride > 1) {
11705
+ const spans = /* @__PURE__ */ new Map();
11706
+ for (const range of ranges) {
11707
+ for (let frame = range.startFrame; frame < range.endFrame; frame++) {
11708
+ const span = spans.get(frame % stride);
11709
+ if (span) span.last = frame;
11710
+ else spans.set(frame % stride, { first: frame, last: frame });
11711
+ }
11712
+ }
11713
+ const strided = [...spans.values()];
11714
+ for (let i = 0; i < strided.length; i += workersPerBatch) {
11715
+ const batchIndex = batches.length;
11716
+ batches.push(
11717
+ strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
11718
+ workerId,
11719
+ startFrame: rangeStart + first,
11720
+ endFrame: rangeStart + last + 1,
11721
+ frameStride: stride,
11722
+ outputDir: outputDirFor(batchIndex, workerId),
11723
+ outputFrameOffset: rangeStart
11724
+ }))
11725
+ );
11726
+ }
11727
+ return batches;
11728
+ }
11643
11729
  for (let i = 0; i < ranges.length; i += workersPerBatch) {
11644
11730
  const batchIndex = batches.length;
11645
11731
  const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
11646
11732
  workerId,
11647
11733
  startFrame: rangeStart + range.startFrame,
11648
11734
  endFrame: rangeStart + range.endFrame,
11649
- outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11735
+ outputDir: outputDirFor(batchIndex, workerId),
11650
11736
  outputFrameOffset: rangeStart
11651
11737
  }));
11652
11738
  batches.push(batch);
@@ -11739,8 +11825,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11739
11825
  currentWorkers,
11740
11826
  attemptWorkDir,
11741
11827
  attempt,
11742
- rangeStart
11743
- ) : [distributeFrames2(options.totalFrames, currentWorkers, attemptWorkDir, rangeStart)];
11828
+ rangeStart,
11829
+ options.interleaveFrames ? options.initialWorkerCount : 1
11830
+ ) : [
11831
+ (options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
11832
+ options.totalFrames,
11833
+ currentWorkers,
11834
+ attemptWorkDir,
11835
+ rangeStart
11836
+ )
11837
+ ];
11744
11838
  options.dedupPerfs.length = 0;
11745
11839
  try {
11746
11840
  for (const tasks of batches) {
@@ -12192,6 +12286,9 @@ function resolveDeShortBand(args) {
12192
12286
  function shouldPreferSingleWorkerDrawElement(args) {
12193
12287
  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;
12194
12288
  }
12289
+ function probeNeedsScreenshotFrames(session) {
12290
+ return (session?.deRiskFrames?.size ?? 0) > 0;
12291
+ }
12195
12292
  function isProbeDeGated(session) {
12196
12293
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
12197
12294
  }
@@ -12849,6 +12946,7 @@ async function executeRenderPipeline(input) {
12849
12946
  height,
12850
12947
  fps: job.config.fps,
12851
12948
  format: captureImageFormat,
12949
+ transparent: needsAlpha,
12852
12950
  quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
12853
12951
  variables: job.config.variables,
12854
12952
  deviceScaleFactor,
@@ -12870,6 +12968,7 @@ async function executeRenderPipeline(input) {
12870
12968
  // Probe-resolved duration: drawElement self-verification derives its
12871
12969
  // sample frame indices from this so they land inside the drained range.
12872
12970
  compositionDurationSeconds: job.duration,
12971
+ deFrameFallback: true,
12873
12972
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
12874
12973
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
12875
12974
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -12970,7 +13069,7 @@ async function executeRenderPipeline(input) {
12970
13069
  minFrames: deParallelMinFrames,
12971
13070
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12972
13071
  supersampling: deviceScaleFactor !== 1,
12973
- probeDeGated: isProbeDeGated(probeSession),
13072
+ probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
12974
13073
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12975
13074
  routerEnabled: deParallelRouterEnabled,
12976
13075
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -13012,6 +13111,8 @@ async function executeRenderPipeline(input) {
13012
13111
  deInversionEligible = false;
13013
13112
  deParallelRouterEligible = false;
13014
13113
  deShortBand = void 0;
13114
+ } else if (probeNeedsScreenshotFrames(probeSession)) {
13115
+ deParallelRouterEligible = false;
13015
13116
  }
13016
13117
  }
13017
13118
  const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
@@ -13203,7 +13304,8 @@ async function executeRenderPipeline(input) {
13203
13304
  deParallelStreamForced
13204
13305
  );
13205
13306
  const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
13206
- if (shouldClampDefaultDrawElement({
13307
+ const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
13308
+ if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
13207
13309
  useDrawElement: cfg.useDrawElement,
13208
13310
  fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
13209
13311
  useStreamingEncode,
@@ -13877,7 +13979,8 @@ async function executeRenderPipeline(input) {
13877
13979
  abortSignal: executionSignal,
13878
13980
  assertNotAborted,
13879
13981
  onProgress,
13880
- onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
13982
+ onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
13983
+ interleaveFrames: overlap !== null
13881
13984
  })
13882
13985
  );
13883
13986
  let captureRes;
@@ -14827,6 +14930,18 @@ function planFrameWeights(input) {
14827
14930
  }
14828
14931
  return weights;
14829
14932
  }
14933
+ function withChunkCaptureCost(slices, weights) {
14934
+ const lightest = weights.reduce((least, weight) => Math.min(least, weight), Infinity);
14935
+ return slices.map((slice) => {
14936
+ let sum2 = 0;
14937
+ for (let frame = slice.startFrame; frame < slice.endFrame; frame++) sum2 += weights[frame];
14938
+ const cost = Math.round(sum2 / (slice.endFrame - slice.startFrame) / lightest * 100) / 100;
14939
+ return cost > 1 ? { ...slice, captureCost: cost } : slice;
14940
+ });
14941
+ }
14942
+ function effectiveChunkFrames(frames, captureCost = 1) {
14943
+ return Math.round(frames * captureCost);
14944
+ }
14830
14945
  function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
14831
14946
  if (isUniformFrameCost(weights)) return null;
14832
14947
  const totalFrames = weights.length;
@@ -16147,7 +16262,10 @@ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSi
16147
16262
  MIN_CHUNK_SIZE,
16148
16263
  Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
16149
16264
  ) : null;
16150
- return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
16265
+ return withChunkCaptureCost(
16266
+ weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize),
16267
+ frameWeights
16268
+ );
16151
16269
  }
16152
16270
  async function readFontSnapshotSha() {
16153
16271
  const module = await import("@hyperframes/core/fonts/embedded-data");
@@ -18514,7 +18632,12 @@ function parseChunkSlices(value) {
18514
18632
  throw new PlanV2IntegrityError(`meta/chunks.json contains duplicate chunk index ${index}`);
18515
18633
  }
18516
18634
  indexes.add(index);
18517
- return { index, startFrame, endFrame };
18635
+ if (chunk.captureCost === void 0) return { index, startFrame, endFrame };
18636
+ const captureCost = chunk.captureCost;
18637
+ if (typeof captureCost !== "number" || !Number.isFinite(captureCost) || captureCost <= 1) {
18638
+ throw new PlanV2IntegrityError(`${field}.captureCost must be a number above 1`);
18639
+ }
18640
+ return { index, startFrame, endFrame, captureCost };
18518
18641
  });
18519
18642
  }
18520
18643
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
@@ -19691,15 +19814,17 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19691
19814
  deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
19692
19815
  )
19693
19816
  });
19817
+ const captureFrames = effectiveChunkFrames(framesInChunk, slice.captureCost);
19694
19818
  const chunkWorkerCount = resolveChunkCaptureWorkers(
19695
- framesInChunk,
19696
- calculateOptimalWorkers2(framesInChunk, void 0, cfg),
19819
+ captureFrames,
19820
+ calculateOptimalWorkers2(captureFrames, void 0, cfg),
19697
19821
  options?.maxCaptureWorkers
19698
19822
  );
19699
19823
  let session = null;
19700
19824
  let outputKind;
19701
19825
  let framesEncoded = 0;
19702
19826
  let sessionBootMs = 0;
19827
+ let videoExtractMs = 0;
19703
19828
  let captureStageMs = 0;
19704
19829
  let encodeStageMs = 0;
19705
19830
  let captureMode;
@@ -19765,6 +19890,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19765
19890
  session = null;
19766
19891
  }
19767
19892
  }
19893
+ const extractStarted = Date.now();
19768
19894
  const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
19769
19895
  planDir,
19770
19896
  planVideos,
@@ -19774,6 +19900,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19774
19900
  cfg,
19775
19901
  remoteSources,
19776
19902
  shownBoxes
19903
+ }).then((frames) => {
19904
+ videoExtractMs = Date.now() - extractStarted;
19905
+ return frames;
19777
19906
  }) : Promise.resolve([]);
19778
19907
  extraction = deferredVideoFrames;
19779
19908
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
@@ -19902,7 +20031,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19902
20031
  assertNotAborted: () => {
19903
20032
  },
19904
20033
  frameRange: range,
19905
- onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
20034
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20035
+ interleaveFrames: overlap.current !== null
19906
20036
  }).catch(async (captureError) => {
19907
20037
  await overlap.current?.abort();
19908
20038
  overlap.current = null;
@@ -19991,6 +20121,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19991
20121
  sessionBootMs,
19992
20122
  captureStageMs,
19993
20123
  encodeStageMs,
20124
+ // Wall time of the chunk's own video frame extraction; it overlaps captureStageMs, so do not add them.
20125
+ videoExtractMs,
19994
20126
  workers: chunkWorkerCount,
19995
20127
  captureMode,
19996
20128
  // Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.