@hyperframes/producer 0.8.199 → 0.8.200

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
@@ -2094,6 +2094,7 @@ function aggregateDrawElement(perfs, de) {
2094
2094
  blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2095
2095
  blankRecaptures: drain?.blankRecaptures ?? 0,
2096
2096
  boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
2097
+ riskFrames: perfs.reduce((sum2, p) => sum2 + (p.deRiskFrames ?? 0), 0),
2097
2098
  ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
2098
2099
  frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
2099
2100
  };
@@ -3730,11 +3731,21 @@ function createDrainFrameGuard(args) {
3730
3731
  return buf;
3731
3732
  };
3732
3733
  }
3733
- async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
3734
+ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
3734
3735
  let prev = null;
3735
3736
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
3736
3737
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3737
- const guardFrame = (idx, buf) => guard(session, idx, buf);
3738
+ const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3739
+ const captureRisk = async (idx) => {
3740
+ await risk.open();
3741
+ lastProgressAt = Date.now();
3742
+ return raceAgainstStall(
3743
+ risk.capture(idx, frameTime(idx)),
3744
+ stallTimeoutMs - (Date.now() - lastProgressAt),
3745
+ { session, frameIndex: idx, totalFrames, stallTimeoutMs },
3746
+ abortSignal
3747
+ );
3748
+ };
3738
3749
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3739
3750
  let lastProgressAt = Date.now();
3740
3751
  const captureWithStallGuard = (idx, promise) => raceAgainstStall(
@@ -3746,7 +3757,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3746
3757
  const drainPrev = async () => {
3747
3758
  if (!prev) return;
3748
3759
  assertNotAborted();
3749
- const buf = await guardFrame(prev.idx, await prev.encodeResult);
3760
+ const buf = await guardFrame(prev, await prev.encodeResult);
3750
3761
  await reorderBuffer.waitForFrame(prev.idx);
3751
3762
  ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
3752
3763
  reorderBuffer.advanceTo(prev.idx + 1);
@@ -3766,7 +3777,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3766
3777
  const drainBatch = async (batch) => {
3767
3778
  for (const item of batch) {
3768
3779
  assertNotAborted();
3769
- const buf = await guardFrame(item.idx, await item.encodeResult);
3780
+ const buf = await guardFrame(item, await item.encodeResult);
3770
3781
  await reorderBuffer.waitForFrame(item.idx);
3771
3782
  ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
3772
3783
  reorderBuffer.advanceTo(item.idx + 1);
@@ -3784,7 +3795,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3784
3795
  };
3785
3796
  if (batchN > 1 && !session.onBeforeCapture) {
3786
3797
  const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
3787
- const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
3798
+ const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
3788
3799
  let prevBatch = [];
3789
3800
  let i = 0;
3790
3801
  while (i < totalFrames) {
@@ -3801,6 +3812,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3801
3812
  );
3802
3813
  await drainBatch(prevBatch);
3803
3814
  prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
3815
+ } else if (risk?.frames.has(i)) {
3816
+ const buffer = await captureRisk(i);
3817
+ await drainBatch(prevBatch);
3818
+ prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
3819
+ i++;
3804
3820
  } else {
3805
3821
  const { encodeResult } = await captureWithStallGuard(
3806
3822
  i,
@@ -3816,6 +3832,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3816
3832
  }
3817
3833
  for (let i = 0; i < totalFrames; i++) {
3818
3834
  assertNotAborted();
3835
+ if (risk?.frames.has(i)) {
3836
+ const buffer = await captureRisk(i);
3837
+ await drainPrev();
3838
+ prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
3839
+ continue;
3840
+ }
3819
3841
  const time = frameTime(i);
3820
3842
  const { encodeResult } = await captureWithStallGuard(
3821
3843
  i,
@@ -4062,6 +4084,24 @@ async function runCaptureStreamingStage(input) {
4062
4084
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4063
4085
  probeSession = null;
4064
4086
  }
4087
+ let riskSession = null;
4088
+ const openRiskSession = async () => {
4089
+ const opened = await createCaptureSession(
4090
+ fileServer.url,
4091
+ framesDir,
4092
+ buildCaptureOptions(),
4093
+ createRenderVideoFrameInjector(),
4094
+ {
4095
+ ...captureCfg,
4096
+ useDrawElement: false,
4097
+ staticFrameDedup: false,
4098
+ enableBrowserPool: false
4099
+ }
4100
+ );
4101
+ riskSession = opened;
4102
+ await initializeSession(opened);
4103
+ return opened;
4104
+ };
4065
4105
  try {
4066
4106
  if (!session.isInitialized) {
4067
4107
  await initializeSession(session);
@@ -4076,6 +4116,7 @@ async function runCaptureStreamingStage(input) {
4076
4116
  blankDeterministicAccepts: 0,
4077
4117
  blankRecaptures: 0
4078
4118
  };
4119
+ const riskFrames = session.deRiskFrames;
4079
4120
  await runWorkerEncodePipelineLoop(
4080
4121
  session,
4081
4122
  totalFrames,
@@ -4086,7 +4127,16 @@ async function runCaptureStreamingStage(input) {
4086
4127
  onProgress,
4087
4128
  log,
4088
4129
  deDrainStats,
4089
- abortSignal
4130
+ abortSignal,
4131
+ riskFrames && riskFrames.size > 0 ? {
4132
+ frames: riskFrames,
4133
+ // Opened on the first risk frame, in a browser of its own: pages sharing a
4134
+ // browser share a compositor, which starves the drawElement page's paint events.
4135
+ open: async () => {
4136
+ riskSession ??= await openRiskSession();
4137
+ },
4138
+ capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
4139
+ } : void 0
4090
4140
  );
4091
4141
  } else {
4092
4142
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
@@ -4124,6 +4174,7 @@ async function runCaptureStreamingStage(input) {
4124
4174
  } finally {
4125
4175
  lastBrowserConsole = session.browserConsoleBuffer;
4126
4176
  await closeCaptureSession(session);
4177
+ if (riskSession) await closeCaptureSession(riskSession);
4127
4178
  }
4128
4179
  }
4129
4180
  const encodeFrom = job.progress;
@@ -4283,7 +4334,7 @@ function openSegmentedSessionReuse(input, session) {
4283
4334
  async function openSegmentedSession(input) {
4284
4335
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4285
4336
  const videoInjector = input.createRenderVideoFrameInjector();
4286
- const captureOptions = input.buildCaptureOptions();
4337
+ const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4287
4338
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4288
4339
  parallel: (input.workerCount ?? 1) > 1
4289
4340
  });
@@ -6299,10 +6350,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
6299
6350
  function detectThreeDTransformUsage(html) {
6300
6351
  return THREE_D_CONTEXT_PATTERN.test(html);
6301
6352
  }
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
6353
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6307
6354
  function detectAncestorBackgroundImage(html) {
6308
6355
  const { document: document2 } = parseHTML4(html);
@@ -7315,7 +7362,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7315
7362
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
7316
7363
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
7317
7364
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
7318
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
7319
7365
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
7320
7366
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
7321
7367
  injectTextRenderingRule(
@@ -7429,7 +7475,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7429
7475
  renderModeHints,
7430
7476
  hasShaderTransitions,
7431
7477
  usesThreeDTransforms,
7432
- usesMixBlendMode,
7433
7478
  hasAncestorBackgroundImage
7434
7479
  };
7435
7480
  }
@@ -7836,13 +7881,6 @@ async function runCompileStage(input) {
7836
7881
  "[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
7882
  );
7838
7883
  }
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
7884
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
7847
7885
  cfg.useDrawElement = false;
7848
7886
  deCompileGate = "ancestor_background_image";
@@ -9273,10 +9311,11 @@ async function constrainAacTruePeak(input) {
9273
9311
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
9274
9312
  return enforceAacTruePeak({
9275
9313
  audioPath: input.audioPath,
9314
+ sourcePath: input.sourcePath,
9276
9315
  targetDurationSeconds: input.targetDurationSeconds,
9277
9316
  signal: input.signal,
9278
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
9279
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9317
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
9318
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9280
9319
  });
9281
9320
  }
9282
9321
  async function enforceAacTruePeak(input) {
@@ -9286,6 +9325,7 @@ async function enforceAacTruePeak(input) {
9286
9325
  const correctedPath = join18(scratchDir, "audio.m4a");
9287
9326
  const measurements = [];
9288
9327
  let attenuationDb = 0;
9328
+ let sourcePeakDbfs;
9289
9329
  let measuredPath = input.audioPath;
9290
9330
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
9291
9331
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -9302,7 +9342,11 @@ async function enforceAacTruePeak(input) {
9302
9342
  }
9303
9343
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
9304
9344
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
9305
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
9345
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
9346
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
9347
+ }
9348
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
9349
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
9306
9350
  const result = await input.encode(
9307
9351
  input.audioPath,
9308
9352
  correctedPath,
@@ -9738,7 +9782,7 @@ async function runCaptureStage(input) {
9738
9782
  );
9739
9783
  }
9740
9784
  }
9741
- const captureOptions = buildCaptureOptions();
9785
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
9742
9786
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
9743
9787
  const checkDiskProjection = createDiskCaptureProjection({
9744
9788
  framesDir,
@@ -12192,6 +12236,9 @@ function resolveDeShortBand(args) {
12192
12236
  function shouldPreferSingleWorkerDrawElement(args) {
12193
12237
  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
12238
  }
12239
+ function probeNeedsScreenshotFrames(session) {
12240
+ return (session?.deRiskFrames?.size ?? 0) > 0;
12241
+ }
12195
12242
  function isProbeDeGated(session) {
12196
12243
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
12197
12244
  }
@@ -12870,6 +12917,7 @@ async function executeRenderPipeline(input) {
12870
12917
  // Probe-resolved duration: drawElement self-verification derives its
12871
12918
  // sample frame indices from this so they land inside the drained range.
12872
12919
  compositionDurationSeconds: job.duration,
12920
+ deFrameFallback: true,
12873
12921
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
12874
12922
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
12875
12923
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -12970,7 +13018,7 @@ async function executeRenderPipeline(input) {
12970
13018
  minFrames: deParallelMinFrames,
12971
13019
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12972
13020
  supersampling: deviceScaleFactor !== 1,
12973
- probeDeGated: isProbeDeGated(probeSession),
13021
+ probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
12974
13022
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12975
13023
  routerEnabled: deParallelRouterEnabled,
12976
13024
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -13012,6 +13060,8 @@ async function executeRenderPipeline(input) {
13012
13060
  deInversionEligible = false;
13013
13061
  deParallelRouterEligible = false;
13014
13062
  deShortBand = void 0;
13063
+ } else if (probeNeedsScreenshotFrames(probeSession)) {
13064
+ deParallelRouterEligible = false;
13015
13065
  }
13016
13066
  }
13017
13067
  const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
@@ -13203,7 +13253,8 @@ async function executeRenderPipeline(input) {
13203
13253
  deParallelStreamForced
13204
13254
  );
13205
13255
  const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
13206
- if (shouldClampDefaultDrawElement({
13256
+ const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
13257
+ if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
13207
13258
  useDrawElement: cfg.useDrawElement,
13208
13259
  fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
13209
13260
  useStreamingEncode,
@@ -14827,6 +14878,18 @@ function planFrameWeights(input) {
14827
14878
  }
14828
14879
  return weights;
14829
14880
  }
14881
+ function withChunkCaptureCost(slices, weights) {
14882
+ const lightest = weights.reduce((least, weight) => Math.min(least, weight), Infinity);
14883
+ return slices.map((slice) => {
14884
+ let sum2 = 0;
14885
+ for (let frame = slice.startFrame; frame < slice.endFrame; frame++) sum2 += weights[frame];
14886
+ const cost = Math.round(sum2 / (slice.endFrame - slice.startFrame) / lightest * 100) / 100;
14887
+ return cost > 1 ? { ...slice, captureCost: cost } : slice;
14888
+ });
14889
+ }
14890
+ function effectiveChunkFrames(frames, captureCost = 1) {
14891
+ return Math.round(frames * captureCost);
14892
+ }
14830
14893
  function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
14831
14894
  if (isUniformFrameCost(weights)) return null;
14832
14895
  const totalFrames = weights.length;
@@ -16147,7 +16210,10 @@ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSi
16147
16210
  MIN_CHUNK_SIZE,
16148
16211
  Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
16149
16212
  ) : null;
16150
- return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
16213
+ return withChunkCaptureCost(
16214
+ weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize),
16215
+ frameWeights
16216
+ );
16151
16217
  }
16152
16218
  async function readFontSnapshotSha() {
16153
16219
  const module = await import("@hyperframes/core/fonts/embedded-data");
@@ -18514,7 +18580,12 @@ function parseChunkSlices(value) {
18514
18580
  throw new PlanV2IntegrityError(`meta/chunks.json contains duplicate chunk index ${index}`);
18515
18581
  }
18516
18582
  indexes.add(index);
18517
- return { index, startFrame, endFrame };
18583
+ if (chunk.captureCost === void 0) return { index, startFrame, endFrame };
18584
+ const captureCost = chunk.captureCost;
18585
+ if (typeof captureCost !== "number" || !Number.isFinite(captureCost) || captureCost <= 1) {
18586
+ throw new PlanV2IntegrityError(`${field}.captureCost must be a number above 1`);
18587
+ }
18588
+ return { index, startFrame, endFrame, captureCost };
18518
18589
  });
18519
18590
  }
18520
18591
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
@@ -19691,15 +19762,17 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19691
19762
  deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
19692
19763
  )
19693
19764
  });
19765
+ const captureFrames = effectiveChunkFrames(framesInChunk, slice.captureCost);
19694
19766
  const chunkWorkerCount = resolveChunkCaptureWorkers(
19695
- framesInChunk,
19696
- calculateOptimalWorkers2(framesInChunk, void 0, cfg),
19767
+ captureFrames,
19768
+ calculateOptimalWorkers2(captureFrames, void 0, cfg),
19697
19769
  options?.maxCaptureWorkers
19698
19770
  );
19699
19771
  let session = null;
19700
19772
  let outputKind;
19701
19773
  let framesEncoded = 0;
19702
19774
  let sessionBootMs = 0;
19775
+ let videoExtractMs = 0;
19703
19776
  let captureStageMs = 0;
19704
19777
  let encodeStageMs = 0;
19705
19778
  let captureMode;
@@ -19765,6 +19838,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19765
19838
  session = null;
19766
19839
  }
19767
19840
  }
19841
+ const extractStarted = Date.now();
19768
19842
  const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
19769
19843
  planDir,
19770
19844
  planVideos,
@@ -19774,6 +19848,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19774
19848
  cfg,
19775
19849
  remoteSources,
19776
19850
  shownBoxes
19851
+ }).then((frames) => {
19852
+ videoExtractMs = Date.now() - extractStarted;
19853
+ return frames;
19777
19854
  }) : Promise.resolve([]);
19778
19855
  extraction = deferredVideoFrames;
19779
19856
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
@@ -19991,6 +20068,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19991
20068
  sessionBootMs,
19992
20069
  captureStageMs,
19993
20070
  encodeStageMs,
20071
+ // Wall time of the chunk's own video frame extraction; it overlaps captureStageMs, so do not add them.
20072
+ videoExtractMs,
19994
20073
  workers: chunkWorkerCount,
19995
20074
  captureMode,
19996
20075
  // Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
@@ -2114,6 +2114,7 @@ function aggregateDrawElement(perfs, de) {
2114
2114
  blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2115
2115
  blankRecaptures: drain?.blankRecaptures ?? 0,
2116
2116
  boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
2117
+ riskFrames: perfs.reduce((sum, p) => sum + (p.deRiskFrames ?? 0), 0),
2117
2118
  ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
2118
2119
  frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
2119
2120
  };
@@ -3750,11 +3751,21 @@ function createDrainFrameGuard(args) {
3750
3751
  return buf;
3751
3752
  };
3752
3753
  }
3753
- async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
3754
+ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
3754
3755
  let prev = null;
3755
3756
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
3756
3757
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3757
- const guardFrame = (idx, buf) => guard(session, idx, buf);
3758
+ const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3759
+ const captureRisk = async (idx) => {
3760
+ await risk.open();
3761
+ lastProgressAt = Date.now();
3762
+ return raceAgainstStall(
3763
+ risk.capture(idx, frameTime(idx)),
3764
+ stallTimeoutMs - (Date.now() - lastProgressAt),
3765
+ { session, frameIndex: idx, totalFrames, stallTimeoutMs },
3766
+ abortSignal
3767
+ );
3768
+ };
3758
3769
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3759
3770
  let lastProgressAt = Date.now();
3760
3771
  const captureWithStallGuard = (idx, promise) => raceAgainstStall(
@@ -3766,7 +3777,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3766
3777
  const drainPrev = async () => {
3767
3778
  if (!prev) return;
3768
3779
  assertNotAborted();
3769
- const buf = await guardFrame(prev.idx, await prev.encodeResult);
3780
+ const buf = await guardFrame(prev, await prev.encodeResult);
3770
3781
  await reorderBuffer.waitForFrame(prev.idx);
3771
3782
  ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
3772
3783
  reorderBuffer.advanceTo(prev.idx + 1);
@@ -3786,7 +3797,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3786
3797
  const drainBatch = async (batch) => {
3787
3798
  for (const item of batch) {
3788
3799
  assertNotAborted();
3789
- const buf = await guardFrame(item.idx, await item.encodeResult);
3800
+ const buf = await guardFrame(item, await item.encodeResult);
3790
3801
  await reorderBuffer.waitForFrame(item.idx);
3791
3802
  ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
3792
3803
  reorderBuffer.advanceTo(item.idx + 1);
@@ -3804,7 +3815,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3804
3815
  };
3805
3816
  if (batchN > 1 && !session.onBeforeCapture) {
3806
3817
  const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
3807
- const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
3818
+ const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
3808
3819
  let prevBatch = [];
3809
3820
  let i = 0;
3810
3821
  while (i < totalFrames) {
@@ -3821,6 +3832,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3821
3832
  );
3822
3833
  await drainBatch(prevBatch);
3823
3834
  prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
3835
+ } else if (risk?.frames.has(i)) {
3836
+ const buffer = await captureRisk(i);
3837
+ await drainBatch(prevBatch);
3838
+ prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
3839
+ i++;
3824
3840
  } else {
3825
3841
  const { encodeResult } = await captureWithStallGuard(
3826
3842
  i,
@@ -3836,6 +3852,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3836
3852
  }
3837
3853
  for (let i = 0; i < totalFrames; i++) {
3838
3854
  assertNotAborted();
3855
+ if (risk?.frames.has(i)) {
3856
+ const buffer = await captureRisk(i);
3857
+ await drainPrev();
3858
+ prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
3859
+ continue;
3860
+ }
3839
3861
  const time = frameTime(i);
3840
3862
  const { encodeResult } = await captureWithStallGuard(
3841
3863
  i,
@@ -4082,6 +4104,24 @@ async function runCaptureStreamingStage(input) {
4082
4104
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4083
4105
  probeSession = null;
4084
4106
  }
4107
+ let riskSession = null;
4108
+ const openRiskSession = async () => {
4109
+ const opened = await createCaptureSession(
4110
+ fileServer.url,
4111
+ framesDir,
4112
+ buildCaptureOptions(),
4113
+ createRenderVideoFrameInjector(),
4114
+ {
4115
+ ...captureCfg,
4116
+ useDrawElement: false,
4117
+ staticFrameDedup: false,
4118
+ enableBrowserPool: false
4119
+ }
4120
+ );
4121
+ riskSession = opened;
4122
+ await initializeSession(opened);
4123
+ return opened;
4124
+ };
4085
4125
  try {
4086
4126
  if (!session.isInitialized) {
4087
4127
  await initializeSession(session);
@@ -4096,6 +4136,7 @@ async function runCaptureStreamingStage(input) {
4096
4136
  blankDeterministicAccepts: 0,
4097
4137
  blankRecaptures: 0
4098
4138
  };
4139
+ const riskFrames = session.deRiskFrames;
4099
4140
  await runWorkerEncodePipelineLoop(
4100
4141
  session,
4101
4142
  totalFrames,
@@ -4106,7 +4147,16 @@ async function runCaptureStreamingStage(input) {
4106
4147
  onProgress,
4107
4148
  log,
4108
4149
  deDrainStats,
4109
- abortSignal
4150
+ abortSignal,
4151
+ riskFrames && riskFrames.size > 0 ? {
4152
+ frames: riskFrames,
4153
+ // Opened on the first risk frame, in a browser of its own: pages sharing a
4154
+ // browser share a compositor, which starves the drawElement page's paint events.
4155
+ open: async () => {
4156
+ riskSession ??= await openRiskSession();
4157
+ },
4158
+ capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
4159
+ } : void 0
4110
4160
  );
4111
4161
  } else {
4112
4162
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
@@ -4144,6 +4194,7 @@ async function runCaptureStreamingStage(input) {
4144
4194
  } finally {
4145
4195
  lastBrowserConsole = session.browserConsoleBuffer;
4146
4196
  await closeCaptureSession(session);
4197
+ if (riskSession) await closeCaptureSession(riskSession);
4147
4198
  }
4148
4199
  }
4149
4200
  const encodeFrom = job.progress;
@@ -4303,7 +4354,7 @@ function openSegmentedSessionReuse(input, session) {
4303
4354
  async function openSegmentedSession(input) {
4304
4355
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4305
4356
  const videoInjector = input.createRenderVideoFrameInjector();
4306
- const captureOptions = input.buildCaptureOptions();
4357
+ const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4307
4358
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4308
4359
  parallel: (input.workerCount ?? 1) > 1
4309
4360
  });
@@ -6319,10 +6370,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
6319
6370
  function detectThreeDTransformUsage(html) {
6320
6371
  return THREE_D_CONTEXT_PATTERN.test(html);
6321
6372
  }
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
6373
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6327
6374
  function detectAncestorBackgroundImage(html) {
6328
6375
  const { document: document2 } = parseHTML4(html);
@@ -7335,7 +7382,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7335
7382
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
7336
7383
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
7337
7384
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
7338
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
7339
7385
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
7340
7386
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
7341
7387
  injectTextRenderingRule(
@@ -7449,7 +7495,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7449
7495
  renderModeHints,
7450
7496
  hasShaderTransitions,
7451
7497
  usesThreeDTransforms,
7452
- usesMixBlendMode,
7453
7498
  hasAncestorBackgroundImage
7454
7499
  };
7455
7500
  }
@@ -7856,13 +7901,6 @@ async function runCompileStage(input) {
7856
7901
  "[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
7902
  );
7858
7903
  }
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
7904
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
7867
7905
  cfg.useDrawElement = false;
7868
7906
  deCompileGate = "ancestor_background_image";
@@ -9257,10 +9295,11 @@ async function constrainAacTruePeak(input) {
9257
9295
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
9258
9296
  return enforceAacTruePeak({
9259
9297
  audioPath: input.audioPath,
9298
+ sourcePath: input.sourcePath,
9260
9299
  targetDurationSeconds: input.targetDurationSeconds,
9261
9300
  signal: input.signal,
9262
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
9263
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9301
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
9302
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9264
9303
  });
9265
9304
  }
9266
9305
  async function enforceAacTruePeak(input) {
@@ -9270,6 +9309,7 @@ async function enforceAacTruePeak(input) {
9270
9309
  const correctedPath = join18(scratchDir, "audio.m4a");
9271
9310
  const measurements = [];
9272
9311
  let attenuationDb = 0;
9312
+ let sourcePeakDbfs;
9273
9313
  let measuredPath = input.audioPath;
9274
9314
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
9275
9315
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -9286,7 +9326,11 @@ async function enforceAacTruePeak(input) {
9286
9326
  }
9287
9327
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
9288
9328
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
9289
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
9329
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
9330
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
9331
+ }
9332
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
9333
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
9290
9334
  const result = await input.encode(
9291
9335
  input.audioPath,
9292
9336
  correctedPath,
@@ -9722,7 +9766,7 @@ async function runCaptureStage(input) {
9722
9766
  );
9723
9767
  }
9724
9768
  }
9725
- const captureOptions = buildCaptureOptions();
9769
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
9726
9770
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
9727
9771
  const checkDiskProjection = createDiskCaptureProjection({
9728
9772
  framesDir,
@@ -12176,6 +12220,9 @@ function resolveDeShortBand(args) {
12176
12220
  function shouldPreferSingleWorkerDrawElement(args) {
12177
12221
  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
12222
  }
12223
+ function probeNeedsScreenshotFrames(session) {
12224
+ return (session?.deRiskFrames?.size ?? 0) > 0;
12225
+ }
12179
12226
  function isProbeDeGated(session) {
12180
12227
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
12181
12228
  }
@@ -12854,6 +12901,7 @@ async function executeRenderPipeline(input) {
12854
12901
  // Probe-resolved duration: drawElement self-verification derives its
12855
12902
  // sample frame indices from this so they land inside the drained range.
12856
12903
  compositionDurationSeconds: job.duration,
12904
+ deFrameFallback: true,
12857
12905
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
12858
12906
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
12859
12907
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -12954,7 +13002,7 @@ async function executeRenderPipeline(input) {
12954
13002
  minFrames: deParallelMinFrames,
12955
13003
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12956
13004
  supersampling: deviceScaleFactor !== 1,
12957
- probeDeGated: isProbeDeGated(probeSession),
13005
+ probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
12958
13006
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12959
13007
  routerEnabled: deParallelRouterEnabled,
12960
13008
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12996,6 +13044,8 @@ async function executeRenderPipeline(input) {
12996
13044
  deInversionEligible = false;
12997
13045
  deParallelRouterEligible = false;
12998
13046
  deShortBand = void 0;
13047
+ } else if (probeNeedsScreenshotFrames(probeSession)) {
13048
+ deParallelRouterEligible = false;
12999
13049
  }
13000
13050
  }
13001
13051
  const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
@@ -13187,7 +13237,8 @@ async function executeRenderPipeline(input) {
13187
13237
  deParallelStreamForced
13188
13238
  );
13189
13239
  const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
13190
- if (shouldClampDefaultDrawElement({
13240
+ const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
13241
+ if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
13191
13242
  useDrawElement: cfg.useDrawElement,
13192
13243
  fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
13193
13244
  useStreamingEncode,
@@ -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;