@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/README.md CHANGED
@@ -61,13 +61,13 @@ are rejected before a render starts. This is separate from engine sub-frame
61
61
 
62
62
  The producer can render HTML compositions to formats that carry a **true alpha channel** — not chroma key. The same composition that renders an opaque MP4 renders a layerable overlay when you set `format`.
63
63
 
64
- | `format` | Codec / pixel format | Alpha | Audio | Use case |
65
- | ----------------- | --------------------------------- | ----------------------- | ------------------- | --------------------------------------------------------------------------------------- |
66
- | `"mp4"` (default) | H.264 (yuv420p) or H.265 + HDR10 | No | AAC | Streaming, social, default deliverable |
67
- | `"webm"` | VP9 + yuva420p | **True alpha** | Opus | Web playback as overlay (`<video>` over background); supported in Chrome, Edge, Firefox |
68
- | `"mov"` | ProRes 4444 + yuva444p10le | **True alpha + 10-bit** | AAC | Editor ingest (Premiere, Final Cut Pro, DaVinci Resolve) |
69
- | `"png-sequence"` | Numbered RGBA PNGs in a directory | **Lossless alpha** | Sidecar `audio.m4a` | After Effects / Nuke / Fusion, or pipelines that post-process frames before encoding |
70
- | `"hls"` | H.264 (yuv420p) in MPEG-TS | No | AAC rendition | VOD streaming — see [HLS Output](#hls-output) |
64
+ | `format` | Codec / pixel format | Alpha | Audio | Use case |
65
+ | ----------------- | --------------------------------- | ------------------ | ------------------- | --------------------------------------------------------------------------------------- |
66
+ | `"mp4"` (default) | H.264 (yuv420p) or H.265 + HDR10 | No | AAC | Streaming, social, default deliverable |
67
+ | `"webm"` | VP9 + yuva420p | **True alpha** | Opus | Web playback as overlay (`<video>` over background); supported in Chrome, Edge, Firefox |
68
+ | `"mov"` | ProRes 4444 + yuva444p10le | **True alpha** | AAC | Editor ingest (Premiere, Final Cut Pro, DaVinci Resolve) |
69
+ | `"png-sequence"` | Numbered RGBA PNGs in a directory | **Lossless alpha** | Sidecar `audio.m4a` | After Effects / Nuke / Fusion, or pipelines that post-process frames before encoding |
70
+ | `"hls"` | H.264 (yuv420p) in MPEG-TS | No | AAC rendition | VOD streaming — see [HLS Output](#hls-output) |
71
71
 
72
72
  ### Example
73
73
 
@@ -88,7 +88,7 @@ await executeRenderJob(job, "./my-project", "./output.webm");
88
88
  The producer captures Chrome screenshots with the page background forced transparent (`html, body, [data-composition-id] { background: transparent !important }`) and the CDP default background override set to RGBA 0,0,0,0. The captured PNGs carry a real alpha channel and that channel is preserved end-to-end:
89
89
 
90
90
  - VP9 (`webm`) is encoded with `-pix_fmt yuva420p`, `-auto-alt-ref 0`, `-cpu-used 4` by default, and `alpha_mode=1` metadata. Tune the speed/quality tradeoff with `PRODUCER_VP9_CPU_USED` (`-8` to `8`) or local CLI `--vp9-cpu-used`.
91
- - ProRes 4444 (`mov`) is encoded with `-pix_fmt yuva444p10le`.
91
+ - ProRes 4444 (`mov`) uses `-pix_fmt yuva444p10le` and carries a separate alpha plane. Chrome's RGBA capture, including alpha, is 8 bits per channel; the 10-bit pixel format adds no capture precision.
92
92
  - PNG sequences are written without re-encoding (zero-padded `frame_NNNNNN.png`).
93
93
 
94
94
  This is not chroma keying. There is no green/blue background to remove and no "key" tolerance to tune — pixels that were transparent in the browser are transparent in the output.
@@ -96,6 +96,7 @@ import {
96
96
  createVideoFrameInjector,
97
97
  getEncoderPreset as getEncoderPreset3,
98
98
  distributeFrames as distributeFrames2,
99
+ distributeFramesInterleaved as distributeFramesInterleaved2,
99
100
  executeParallelCapture as executeParallelCapture2,
100
101
  mergeWorkerFrames,
101
102
  getSystemTotalMb,
@@ -1540,6 +1541,7 @@ import {
1540
1541
  parseVideoElements,
1541
1542
  parseImageElements,
1542
1543
  parseAudioElements,
1544
+ isSelfOrAncestorHidden,
1543
1545
  resolveReferencedStart
1544
1546
  } from "@hyperframes/engine";
1545
1547
  function resolveHostEnd(host, hostStart) {
@@ -1600,6 +1602,15 @@ function collectHostWindows(html) {
1600
1602
  }
1601
1603
  return windows;
1602
1604
  }
1605
+ function collectHiddenVideoIds(html) {
1606
+ const { document: document2 } = parseHTML2(html);
1607
+ const hidden = /* @__PURE__ */ new Set();
1608
+ for (const element of document2.querySelectorAll("video")) {
1609
+ const id = element.getAttribute(MEDIA_RENDER_ID_ATTR) || element.getAttribute("id");
1610
+ if (id && isSelfOrAncestorHidden(element)) hidden.add(id);
1611
+ }
1612
+ return hidden;
1613
+ }
1603
1614
  function toAbsoluteWindow(start, end, window2) {
1604
1615
  const absoluteStart = resolveAbsoluteMediaStartSeconds({
1605
1616
  authoredStart: start,
@@ -1633,8 +1644,13 @@ function collectRenderMedia(html) {
1633
1644
  const windows = collectHostWindows(html);
1634
1645
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
1635
1646
  const droppedMediaIds = /* @__PURE__ */ new Set();
1647
+ const hiddenIds = collectHiddenVideoIds(html);
1636
1648
  const videos = [];
1637
1649
  for (const video of parseVideoElements(html)) {
1650
+ if (hiddenIds.has(video.id)) {
1651
+ droppedMediaIds.add(video.id);
1652
+ continue;
1653
+ }
1638
1654
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
1639
1655
  if (!absolute) {
1640
1656
  droppedMediaIds.add(video.id);
@@ -1844,7 +1860,7 @@ async function runAnimatedGifTranscode(request) {
1844
1860
  failureReason: result.failureReason
1845
1861
  });
1846
1862
  }
1847
- if (result.terminationReason === "deadline") {
1863
+ if (result.terminationReason === "inactivity") {
1848
1864
  throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
1849
1865
  }
1850
1866
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
@@ -2311,10 +2327,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
2311
2327
  function detectThreeDTransformUsage(html) {
2312
2328
  return THREE_D_CONTEXT_PATTERN.test(html);
2313
2329
  }
2314
- var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
2315
- function detectMixBlendModeUsage(html) {
2316
- return MIX_BLEND_MODE_PATTERN.test(html);
2317
- }
2318
2330
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
2319
2331
  function detectAncestorBackgroundImage(html) {
2320
2332
  const { document: document2 } = parseHTML4(html);
@@ -2467,8 +2479,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
2467
2479
  const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
2468
2480
  let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
2469
2481
  const preResolved = extractResolvedMedia(compiledHtml);
2482
+ const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
2470
2483
  const clampResults = await Promise.all(
2471
- preResolved.filter((el) => !!el.src && !el.loop).map(async (el) => {
2484
+ preResolved.filter((el) => !!el.src && !el.loop && !hiddenVideoIds.has(el.id)).map(async (el) => {
2472
2485
  const { duration: maxDuration } = await resolveMediaDuration(
2473
2486
  el.src,
2474
2487
  el.mediaStart,
@@ -3327,7 +3340,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3327
3340
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
3328
3341
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
3329
3342
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
3330
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
3331
3343
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
3332
3344
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
3333
3345
  injectTextRenderingRule(
@@ -3441,7 +3453,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3441
3453
  renderModeHints,
3442
3454
  hasShaderTransitions,
3443
3455
  usesThreeDTransforms,
3444
- usesMixBlendMode,
3445
3456
  hasAncestorBackgroundImage
3446
3457
  };
3447
3458
  }
@@ -3848,13 +3859,6 @@ async function runCompileStage(input) {
3848
3859
  "[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."
3849
3860
  );
3850
3861
  }
3851
- if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
3852
- cfg.useDrawElement = false;
3853
- deCompileGate = "mix_blend_mode";
3854
- log.info(
3855
- "[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
3856
- );
3857
- }
3858
3862
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
3859
3863
  cfg.useDrawElement = false;
3860
3864
  deCompileGate = "ancestor_background_image";
@@ -4148,6 +4152,7 @@ async function runProbeStage(input) {
4148
4152
  height,
4149
4153
  fps: job.config.fps,
4150
4154
  format: captureFormat,
4155
+ transparent: needsAlpha,
4151
4156
  quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
4152
4157
  variables: job.config.variables,
4153
4158
  deviceScaleFactor,
@@ -5312,10 +5317,11 @@ async function constrainAacTruePeak(input) {
5312
5317
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
5313
5318
  return enforceAacTruePeak({
5314
5319
  audioPath: input.audioPath,
5320
+ sourcePath: input.sourcePath,
5315
5321
  targetDurationSeconds: input.targetDurationSeconds,
5316
5322
  signal: input.signal,
5317
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
5318
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
5323
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
5324
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
5319
5325
  });
5320
5326
  }
5321
5327
  async function enforceAacTruePeak(input) {
@@ -5325,6 +5331,7 @@ async function enforceAacTruePeak(input) {
5325
5331
  const correctedPath = join9(scratchDir, "audio.m4a");
5326
5332
  const measurements = [];
5327
5333
  let attenuationDb = 0;
5334
+ let sourcePeakDbfs;
5328
5335
  let measuredPath = input.audioPath;
5329
5336
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
5330
5337
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -5341,7 +5348,11 @@ async function enforceAacTruePeak(input) {
5341
5348
  }
5342
5349
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
5343
5350
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
5344
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
5351
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
5352
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
5353
+ }
5354
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
5355
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
5345
5356
  const result = await input.encode(
5346
5357
  input.audioPath,
5347
5358
  correctedPath,
@@ -5758,7 +5769,7 @@ async function runCaptureStage(input) {
5758
5769
  );
5759
5770
  }
5760
5771
  }
5761
- const captureOptions = buildCaptureOptions();
5772
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
5762
5773
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
5763
5774
  const checkDiskProjection = createDiskCaptureProjection({
5764
5775
  framesDir,
@@ -5779,6 +5790,7 @@ async function runCaptureStage(input) {
5779
5790
  createBeforeCaptureHook: createRenderVideoFrameInjector,
5780
5791
  abortSignal,
5781
5792
  frameRangeStart: frameRange?.startFrame,
5793
+ interleaveFrames: input.interleaveFrames,
5782
5794
  dedupPerfs,
5783
5795
  pageErrorLines: workerPageErrors,
5784
5796
  onProgress: (progress) => {
@@ -6356,9 +6368,6 @@ async function runEncodeStage(input) {
6356
6368
  return { encodeMs: Date.now() - stage5Start };
6357
6369
  }
6358
6370
  const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
6359
- const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
6360
- const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
6361
- const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
6362
6371
  const frameExt = frameFileExtension2(input.captureImageFormat);
6363
6372
  const framePattern = `frame_%06d.${frameExt}`;
6364
6373
  const encoderOpts = videoEncoderOptions(input, engineCfg);
@@ -6369,7 +6378,7 @@ async function runEncodeStage(input) {
6369
6378
  encoderOpts,
6370
6379
  chunkedEncodeSize,
6371
6380
  abortSignal,
6372
- videoEngineCfg,
6381
+ engineCfg,
6373
6382
  onFramesEncoded
6374
6383
  ) : await encodeFramesFromDir(
6375
6384
  framesDir,
@@ -6377,7 +6386,7 @@ async function runEncodeStage(input) {
6377
6386
  videoOnlyPath,
6378
6387
  encoderOpts,
6379
6388
  abortSignal,
6380
- videoEngineCfg,
6389
+ engineCfg,
6381
6390
  onFramesEncoded
6382
6391
  );
6383
6392
  assertNotAborted();
@@ -6505,16 +6514,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
6505
6514
  }
6506
6515
  return ranges;
6507
6516
  }
6508
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
6517
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
6509
6518
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
6510
6519
  const batches = [];
6520
+ const outputDirFor = (batchIndex, workerId) => join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
6521
+ if (stride > 1) {
6522
+ const spans = /* @__PURE__ */ new Map();
6523
+ for (const range of ranges) {
6524
+ for (let frame = range.startFrame; frame < range.endFrame; frame++) {
6525
+ const span = spans.get(frame % stride);
6526
+ if (span) span.last = frame;
6527
+ else spans.set(frame % stride, { first: frame, last: frame });
6528
+ }
6529
+ }
6530
+ const strided = [...spans.values()];
6531
+ for (let i = 0; i < strided.length; i += workersPerBatch) {
6532
+ const batchIndex = batches.length;
6533
+ batches.push(
6534
+ strided.slice(i, i + workersPerBatch).map(({ first, last }, workerId) => ({
6535
+ workerId,
6536
+ startFrame: rangeStart + first,
6537
+ endFrame: rangeStart + last + 1,
6538
+ frameStride: stride,
6539
+ outputDir: outputDirFor(batchIndex, workerId),
6540
+ outputFrameOffset: rangeStart
6541
+ }))
6542
+ );
6543
+ }
6544
+ return batches;
6545
+ }
6511
6546
  for (let i = 0; i < ranges.length; i += workersPerBatch) {
6512
6547
  const batchIndex = batches.length;
6513
6548
  const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
6514
6549
  workerId,
6515
6550
  startFrame: rangeStart + range.startFrame,
6516
6551
  endFrame: rangeStart + range.endFrame,
6517
- outputDir: join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
6552
+ outputDir: outputDirFor(batchIndex, workerId),
6518
6553
  outputFrameOffset: rangeStart
6519
6554
  }));
6520
6555
  batches.push(batch);
@@ -6572,8 +6607,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6572
6607
  currentWorkers,
6573
6608
  attemptWorkDir,
6574
6609
  attempt,
6575
- rangeStart
6576
- ) : [distributeFrames2(options.totalFrames, currentWorkers, attemptWorkDir, rangeStart)];
6610
+ rangeStart,
6611
+ options.interleaveFrames ? options.initialWorkerCount : 1
6612
+ ) : [
6613
+ (options.interleaveFrames ? distributeFramesInterleaved2 : distributeFrames2)(
6614
+ options.totalFrames,
6615
+ currentWorkers,
6616
+ attemptWorkDir,
6617
+ rangeStart
6618
+ )
6619
+ ];
6577
6620
  options.dedupPerfs.length = 0;
6578
6621
  try {
6579
6622
  for (const tasks of batches) {
@@ -7336,6 +7379,18 @@ function planFrameWeights(input) {
7336
7379
  }
7337
7380
  return weights;
7338
7381
  }
7382
+ function withChunkCaptureCost(slices, weights) {
7383
+ const lightest = weights.reduce((least, weight) => Math.min(least, weight), Infinity);
7384
+ return slices.map((slice) => {
7385
+ let sum2 = 0;
7386
+ for (let frame = slice.startFrame; frame < slice.endFrame; frame++) sum2 += weights[frame];
7387
+ const cost = Math.round(sum2 / (slice.endFrame - slice.startFrame) / lightest * 100) / 100;
7388
+ return cost > 1 ? { ...slice, captureCost: cost } : slice;
7389
+ });
7390
+ }
7391
+ function effectiveChunkFrames(frames, captureCost = 1) {
7392
+ return Math.round(frames * captureCost);
7393
+ }
7339
7394
  function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
7340
7395
  if (isUniformFrameCost(weights)) return null;
7341
7396
  const totalFrames = weights.length;
@@ -8660,7 +8715,10 @@ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSi
8660
8715
  MIN_CHUNK_SIZE,
8661
8716
  Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
8662
8717
  ) : null;
8663
- return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
8718
+ return withChunkCaptureCost(
8719
+ weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize),
8720
+ frameWeights
8721
+ );
8664
8722
  }
8665
8723
  async function readFontSnapshotSha() {
8666
8724
  const module = await import("@hyperframes/core/fonts/embedded-data");
@@ -9651,7 +9709,12 @@ function parseChunkSlices(value) {
9651
9709
  throw new PlanV2IntegrityError(`meta/chunks.json contains duplicate chunk index ${index}`);
9652
9710
  }
9653
9711
  indexes.add(index);
9654
- return { index, startFrame, endFrame };
9712
+ if (chunk.captureCost === void 0) return { index, startFrame, endFrame };
9713
+ const captureCost = chunk.captureCost;
9714
+ if (typeof captureCost !== "number" || !Number.isFinite(captureCost) || captureCost <= 1) {
9715
+ throw new PlanV2IntegrityError(`${field}.captureCost must be a number above 1`);
9716
+ }
9717
+ return { index, startFrame, endFrame, captureCost };
9655
9718
  });
9656
9719
  }
9657
9720
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
@@ -10828,15 +10891,17 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10828
10891
  deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
10829
10892
  )
10830
10893
  });
10894
+ const captureFrames = effectiveChunkFrames(framesInChunk, slice.captureCost);
10831
10895
  const chunkWorkerCount = resolveChunkCaptureWorkers(
10832
- framesInChunk,
10833
- calculateOptimalWorkers2(framesInChunk, void 0, cfg),
10896
+ captureFrames,
10897
+ calculateOptimalWorkers2(captureFrames, void 0, cfg),
10834
10898
  options?.maxCaptureWorkers
10835
10899
  );
10836
10900
  let session = null;
10837
10901
  let outputKind;
10838
10902
  let framesEncoded = 0;
10839
10903
  let sessionBootMs = 0;
10904
+ let videoExtractMs = 0;
10840
10905
  let captureStageMs = 0;
10841
10906
  let encodeStageMs = 0;
10842
10907
  let captureMode;
@@ -10902,6 +10967,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10902
10967
  session = null;
10903
10968
  }
10904
10969
  }
10970
+ const extractStarted = Date.now();
10905
10971
  const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
10906
10972
  planDir,
10907
10973
  planVideos,
@@ -10911,6 +10977,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
10911
10977
  cfg,
10912
10978
  remoteSources,
10913
10979
  shownBoxes
10980
+ }).then((frames) => {
10981
+ videoExtractMs = Date.now() - extractStarted;
10982
+ return frames;
10914
10983
  }) : Promise.resolve([]);
10915
10984
  extraction = deferredVideoFrames;
10916
10985
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
@@ -11039,7 +11108,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11039
11108
  assertNotAborted: () => {
11040
11109
  },
11041
11110
  frameRange: range,
11042
- onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
11111
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
11112
+ interleaveFrames: overlap.current !== null
11043
11113
  }).catch(async (captureError) => {
11044
11114
  await overlap.current?.abort();
11045
11115
  overlap.current = null;
@@ -11128,6 +11198,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11128
11198
  sessionBootMs,
11129
11199
  captureStageMs,
11130
11200
  encodeStageMs,
11201
+ // Wall time of the chunk's own video frame extraction; it overlaps captureStageMs, so do not add them.
11202
+ videoExtractMs,
11131
11203
  workers: chunkWorkerCount,
11132
11204
  captureMode,
11133
11205
  // Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "3af46e671fc1165bd676de2c1e16493105979083a615954aa23ef784de92f7ce",
4
+ "sha256": "74fe41f512dcc9c39366e4f48c8d24e6011f5180f122ab1525d210000618830f",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"