@hyperframes/producer 0.8.200 → 0.8.202

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,
@@ -5579,6 +5580,7 @@ import {
5579
5580
  parseVideoElements,
5580
5581
  parseImageElements,
5581
5582
  parseAudioElements,
5583
+ isSelfOrAncestorHidden,
5582
5584
  resolveReferencedStart
5583
5585
  } from "@hyperframes/engine";
5584
5586
  function resolveHostEnd(host, hostStart) {
@@ -5639,6 +5641,15 @@ function collectHostWindows(html) {
5639
5641
  }
5640
5642
  return windows;
5641
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
+ }
5642
5653
  function toAbsoluteWindow(start, end, window2) {
5643
5654
  const absoluteStart = resolveAbsoluteMediaStartSeconds({
5644
5655
  authoredStart: start,
@@ -5672,8 +5683,13 @@ function collectRenderMedia(html) {
5672
5683
  const windows = collectHostWindows(html);
5673
5684
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5674
5685
  const droppedMediaIds = /* @__PURE__ */ new Set();
5686
+ const hiddenIds = collectHiddenVideoIds(html);
5675
5687
  const videos = [];
5676
5688
  for (const video of parseVideoElements(html)) {
5689
+ if (hiddenIds.has(video.id)) {
5690
+ droppedMediaIds.add(video.id);
5691
+ continue;
5692
+ }
5677
5693
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5678
5694
  if (!absolute) {
5679
5695
  droppedMediaIds.add(video.id);
@@ -5883,7 +5899,7 @@ async function runAnimatedGifTranscode(request) {
5883
5899
  failureReason: result.failureReason
5884
5900
  });
5885
5901
  }
5886
- if (result.terminationReason === "deadline") {
5902
+ if (result.terminationReason === "inactivity") {
5887
5903
  throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
5888
5904
  }
5889
5905
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
@@ -6502,8 +6518,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6502
6518
  const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
6503
6519
  let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
6504
6520
  const preResolved = extractResolvedMedia(compiledHtml);
6521
+ const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
6505
6522
  const clampResults = await Promise.all(
6506
- 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) => {
6507
6524
  const { duration: maxDuration } = await resolveMediaDuration(
6508
6525
  el.src,
6509
6526
  el.mediaStart,
@@ -8174,6 +8191,7 @@ async function runProbeStage(input) {
8174
8191
  height,
8175
8192
  fps: job.config.fps,
8176
8193
  format: captureFormat,
8194
+ transparent: needsAlpha,
8177
8195
  quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
8178
8196
  variables: job.config.variables,
8179
8197
  deviceScaleFactor,
@@ -9803,6 +9821,7 @@ async function runCaptureStage(input) {
9803
9821
  createBeforeCaptureHook: createRenderVideoFrameInjector,
9804
9822
  abortSignal,
9805
9823
  frameRangeStart: frameRange?.startFrame,
9824
+ interleaveFrames: input.interleaveFrames,
9806
9825
  dedupPerfs,
9807
9826
  pageErrorLines: workerPageErrors,
9808
9827
  onProgress: (progress) => {
@@ -11318,9 +11337,6 @@ async function runEncodeStage(input) {
11318
11337
  return { encodeMs: Date.now() - stage5Start };
11319
11338
  }
11320
11339
  const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
11321
- const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
11322
- const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
11323
- const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
11324
11340
  const frameExt = frameFileExtension2(input.captureImageFormat);
11325
11341
  const framePattern = `frame_%06d.${frameExt}`;
11326
11342
  const encoderOpts = videoEncoderOptions(input, engineCfg);
@@ -11331,7 +11347,7 @@ async function runEncodeStage(input) {
11331
11347
  encoderOpts,
11332
11348
  chunkedEncodeSize,
11333
11349
  abortSignal,
11334
- videoEngineCfg,
11350
+ engineCfg,
11335
11351
  onFramesEncoded
11336
11352
  ) : await encodeFramesFromDir(
11337
11353
  framesDir,
@@ -11339,7 +11355,7 @@ async function runEncodeStage(input) {
11339
11355
  videoOnlyPath,
11340
11356
  encoderOpts,
11341
11357
  abortSignal,
11342
- videoEngineCfg,
11358
+ engineCfg,
11343
11359
  onFramesEncoded
11344
11360
  );
11345
11361
  assertNotAborted();
@@ -11681,16 +11697,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11681
11697
  }
11682
11698
  return ranges;
11683
11699
  }
11684
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
11700
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
11685
11701
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
11686
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
+ }
11687
11729
  for (let i = 0; i < ranges.length; i += workersPerBatch) {
11688
11730
  const batchIndex = batches.length;
11689
11731
  const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
11690
11732
  workerId,
11691
11733
  startFrame: rangeStart + range.startFrame,
11692
11734
  endFrame: rangeStart + range.endFrame,
11693
- outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11735
+ outputDir: outputDirFor(batchIndex, workerId),
11694
11736
  outputFrameOffset: rangeStart
11695
11737
  }));
11696
11738
  batches.push(batch);
@@ -11783,8 +11825,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11783
11825
  currentWorkers,
11784
11826
  attemptWorkDir,
11785
11827
  attempt,
11786
- rangeStart
11787
- ) : [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
+ ];
11788
11838
  options.dedupPerfs.length = 0;
11789
11839
  try {
11790
11840
  for (const tasks of batches) {
@@ -12896,6 +12946,7 @@ async function executeRenderPipeline(input) {
12896
12946
  height,
12897
12947
  fps: job.config.fps,
12898
12948
  format: captureImageFormat,
12949
+ transparent: needsAlpha,
12899
12950
  quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
12900
12951
  variables: job.config.variables,
12901
12952
  deviceScaleFactor,
@@ -13928,7 +13979,8 @@ async function executeRenderPipeline(input) {
13928
13979
  abortSignal: executionSignal,
13929
13980
  assertNotAborted,
13930
13981
  onProgress,
13931
- onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
13982
+ onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
13983
+ interleaveFrames: overlap !== null
13932
13984
  })
13933
13985
  );
13934
13986
  let captureRes;
@@ -19979,7 +20031,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19979
20031
  assertNotAborted: () => {
19980
20032
  },
19981
20033
  frameRange: range,
19982
- onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
20034
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20035
+ interleaveFrames: overlap.current !== null
19983
20036
  }).catch(async (captureError) => {
19984
20037
  await overlap.current?.abort();
19985
20038
  overlap.current = null;
@@ -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,
@@ -5599,6 +5600,7 @@ import {
5599
5600
  parseVideoElements,
5600
5601
  parseImageElements,
5601
5602
  parseAudioElements,
5603
+ isSelfOrAncestorHidden,
5602
5604
  resolveReferencedStart
5603
5605
  } from "@hyperframes/engine";
5604
5606
  function resolveHostEnd(host, hostStart) {
@@ -5659,6 +5661,15 @@ function collectHostWindows(html) {
5659
5661
  }
5660
5662
  return windows;
5661
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
+ }
5662
5673
  function toAbsoluteWindow(start, end, window2) {
5663
5674
  const absoluteStart = resolveAbsoluteMediaStartSeconds({
5664
5675
  authoredStart: start,
@@ -5692,8 +5703,13 @@ function collectRenderMedia(html) {
5692
5703
  const windows = collectHostWindows(html);
5693
5704
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5694
5705
  const droppedMediaIds = /* @__PURE__ */ new Set();
5706
+ const hiddenIds = collectHiddenVideoIds(html);
5695
5707
  const videos = [];
5696
5708
  for (const video of parseVideoElements(html)) {
5709
+ if (hiddenIds.has(video.id)) {
5710
+ droppedMediaIds.add(video.id);
5711
+ continue;
5712
+ }
5697
5713
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5698
5714
  if (!absolute) {
5699
5715
  droppedMediaIds.add(video.id);
@@ -5903,7 +5919,7 @@ async function runAnimatedGifTranscode(request) {
5903
5919
  failureReason: result.failureReason
5904
5920
  });
5905
5921
  }
5906
- if (result.terminationReason === "deadline") {
5922
+ if (result.terminationReason === "inactivity") {
5907
5923
  throw new Error(`Animated GIF transcode timed out after ${timeout}ms`);
5908
5924
  }
5909
5925
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
@@ -6522,8 +6538,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6522
6538
  const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
6523
6539
  let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
6524
6540
  const preResolved = extractResolvedMedia(compiledHtml);
6541
+ const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
6525
6542
  const clampResults = await Promise.all(
6526
- 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) => {
6527
6544
  const { duration: maxDuration } = await resolveMediaDuration(
6528
6545
  el.src,
6529
6546
  el.mediaStart,
@@ -8194,6 +8211,7 @@ async function runProbeStage(input) {
8194
8211
  height,
8195
8212
  fps: job.config.fps,
8196
8213
  format: captureFormat,
8214
+ transparent: needsAlpha,
8197
8215
  quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
8198
8216
  variables: job.config.variables,
8199
8217
  deviceScaleFactor,
@@ -9787,6 +9805,7 @@ async function runCaptureStage(input) {
9787
9805
  createBeforeCaptureHook: createRenderVideoFrameInjector,
9788
9806
  abortSignal,
9789
9807
  frameRangeStart: frameRange?.startFrame,
9808
+ interleaveFrames: input.interleaveFrames,
9790
9809
  dedupPerfs,
9791
9810
  pageErrorLines: workerPageErrors,
9792
9811
  onProgress: (progress) => {
@@ -11302,9 +11321,6 @@ async function runEncodeStage(input) {
11302
11321
  return { encodeMs: Date.now() - stage5Start };
11303
11322
  }
11304
11323
  const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
11305
- const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
11306
- const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
11307
- const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
11308
11324
  const frameExt = frameFileExtension2(input.captureImageFormat);
11309
11325
  const framePattern = `frame_%06d.${frameExt}`;
11310
11326
  const encoderOpts = videoEncoderOptions(input, engineCfg);
@@ -11315,7 +11331,7 @@ async function runEncodeStage(input) {
11315
11331
  encoderOpts,
11316
11332
  chunkedEncodeSize,
11317
11333
  abortSignal,
11318
- videoEngineCfg,
11334
+ engineCfg,
11319
11335
  onFramesEncoded
11320
11336
  ) : await encodeFramesFromDir(
11321
11337
  framesDir,
@@ -11323,7 +11339,7 @@ async function runEncodeStage(input) {
11323
11339
  videoOnlyPath,
11324
11340
  encoderOpts,
11325
11341
  abortSignal,
11326
- videoEngineCfg,
11342
+ engineCfg,
11327
11343
  onFramesEncoded
11328
11344
  );
11329
11345
  assertNotAborted();
@@ -11665,16 +11681,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11665
11681
  }
11666
11682
  return ranges;
11667
11683
  }
11668
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
11684
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
11669
11685
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
11670
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
+ }
11671
11713
  for (let i = 0; i < ranges.length; i += workersPerBatch) {
11672
11714
  const batchIndex = batches.length;
11673
11715
  const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
11674
11716
  workerId,
11675
11717
  startFrame: rangeStart + range.startFrame,
11676
11718
  endFrame: rangeStart + range.endFrame,
11677
- outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11719
+ outputDir: outputDirFor(batchIndex, workerId),
11678
11720
  outputFrameOffset: rangeStart
11679
11721
  }));
11680
11722
  batches.push(batch);
@@ -11767,8 +11809,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11767
11809
  currentWorkers,
11768
11810
  attemptWorkDir,
11769
11811
  attempt,
11770
- rangeStart
11771
- ) : [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
+ ];
11772
11822
  options.dedupPerfs.length = 0;
11773
11823
  try {
11774
11824
  for (const tasks of batches) {
@@ -12880,6 +12930,7 @@ async function executeRenderPipeline(input) {
12880
12930
  height,
12881
12931
  fps: job.config.fps,
12882
12932
  format: captureImageFormat,
12933
+ transparent: needsAlpha,
12883
12934
  quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
12884
12935
  variables: job.config.variables,
12885
12936
  deviceScaleFactor,
@@ -13912,7 +13963,8 @@ async function executeRenderPipeline(input) {
13912
13963
  abortSignal: executionSignal,
13913
13964
  assertNotAborted,
13914
13965
  onProgress,
13915
- onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
13966
+ onFrameLanded: (frame) => overlap?.onFrameLanded(frame),
13967
+ interleaveFrames: overlap !== null
13916
13968
  })
13917
13969
  );
13918
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 {};
@@ -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. */
@@ -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[];
@@ -29,51 +29,40 @@ export interface RenderConfig {
29
29
  /** Segmented capture: keep renders/.hf-segments/<planHash> after a successful render. */
30
30
  keepSegments?: boolean;
31
31
  /**
32
- * Output container format. Defaults to `"mp4"`; existing renders are
33
- * unaffected unless this field is set explicitly.
32
+ * Output container format. Defaults to `"mp4"`; set explicitly for other formats.
34
33
  *
35
- * - `"mp4"`: H.264 by default, or H.265 + HDR10 when HDR auto-detect
36
- * engages or `hdrMode: "force-hdr"` is set. Opaque. The
37
- * default streaming/social deliverable. Faststart is applied so the
38
- * `moov` atom sits at the file start and the file plays from a
39
- * partial download.
40
- * - `"webm"`: VP9 + `yuva420p` pixel format → **true alpha channel**, no
41
- * chroma key. Plays in Chrome, Edge, and Firefox; Safari support for
42
- * alpha-WebM is incomplete. Use this when the output should drop
43
- * straight into a `<video>` over a colored background on the web.
44
- * Audio is muxed as Opus.
45
- * - `"mov"`: ProRes 4444 + `yuva444p10le` → **true alpha channel +
46
- * 10-bit color**. Sized for editor ingest (Premiere, Final Cut Pro,
47
- * DaVinci Resolve), not direct web playback. Audio is muxed as AAC.
48
- * - `"gif"`: animated GIF encoded from captured RGBA frames with a two-pass
49
- * FFmpeg palette (`palettegen` + `paletteuse`). Use for PRs, READMEs,
50
- * and docs where inline autoplay matters more than file size. No audio
51
- * stream; transparency is binary because GIF has no partial alpha.
52
- * - `"png-sequence"`: a directory of zero-padded RGBA PNGs
53
- * (`frame_000001.png` …). Lossless alpha, largest on disk, no muxed
54
- * audio (an `audio.m4a` sidecar is written alongside the PNGs when
55
- * the composition has audio elements). Use for After Effects / Nuke
56
- * / Fusion ingest, or when frames need post-processing before
57
- * encoding. `outputPath` is treated as a directory; it is created if
58
- * it doesn't exist.
59
- * - `"hls"`: HLS VOD package — the same opaque H.264 + AAC encode as
60
- * `"mp4"`, stream-copied (no re-encode) into a directory of MPEG-TS
61
- * segments: `master.m3u8`, `video.m3u8` + `video_%05d.ts`, and
62
- * `audio.m3u8` + `audio_%05d.ts` when the composition has audio.
63
- * Segments are fixed-length (see {@link RenderConfig.hlsSegmentSeconds})
64
- * and every one starts on an IDR frame, because the encoder's GOP is
65
- * locked to `hlsSegmentSeconds × fps`. Like `"png-sequence"`,
66
- * `outputPath` is treated as a directory. SDR only, software encoder
67
- * only, and not available in distributed / Lambda / Cloud Run mode.
34
+ * - `"mp4"`: opaque H.264, or H.265 + HDR10 with HDR auto-detection or
35
+ * `hdrMode: "force-hdr"`. For streaming/social delivery; faststart puts the
36
+ * `moov` atom first so playback can begin during a partial download.
37
+ * - `"webm"`: VP9 + `yuva420p` with true alpha (no chroma key) and Opus audio.
38
+ * For `<video>` overlays in Chrome, Edge and Firefox; Safari's alpha-WebM
39
+ * support is incomplete.
40
+ * - `"mov"`: ProRes 4444 + `yuva444p10le` with true alpha and AAC audio.
41
+ * Chrome captures 8-bit RGBA; encoding it as 10-bit ProRes does not add
42
+ * capture precision. For editor ingest (Premiere, Final Cut Pro, DaVinci
43
+ * Resolve), rather than direct web playback.
44
+ * - `"gif"`: animated GIF from RGBA captures, using FFmpeg's two-pass
45
+ * `palettegen` + `paletteuse`. For inline autoplay in PRs, READMEs and docs
46
+ * when file size matters less. No audio; transparency is binary, not partial.
47
+ * - `"png-sequence"`: zero-padded RGBA PNGs (`frame_000001.png` ...), with
48
+ * lossless alpha and the largest disk footprint. No muxed audio; an
49
+ * `audio.m4a` sidecar is written when the composition contains audio.
50
+ * For After Effects / Nuke / Fusion or post-processing before encoding.
51
+ * `outputPath` is a directory, created if it does not exist.
52
+ * - `"hls"`: HLS VOD using the opaque H.264 + AAC encode from `"mp4"`,
53
+ * stream-copied without re-encoding into MPEG-TS in the `outputPath` directory:
54
+ * `master.m3u8`, `video.m3u8` + `video_%05d.ts`, and `audio.m3u8` +
55
+ * `audio_%05d.ts` when the composition contains audio. Segment length is
56
+ * {@link RenderConfig.hlsSegmentSeconds}; the encoder's GOP is locked to
57
+ * that length in frames, so each segment starts on an IDR frame.
58
+ * SDR and software encoders only; unavailable in distributed rendering,
59
+ * including Lambda and Cloud Run.
68
60
  *
69
- * Alpha output (`"webm"`, `"mov"`, `"png-sequence"`, `"gif"`) automatically
70
- * forces screenshot capture (Chrome's BeginFrame compositor does not
71
- * preserve alpha on Linux headless-shell) and disables HDR — HDR +
72
- * alpha is not a supported combination, a warning is logged and HDR
73
- * falls back to SDR. The transparent-background CSS is injected by
74
- * the engine's `initTransparentBackground` helper, so authors should
75
- * not paint a fullscreen `body` / `#root` background in their
76
- * compositions when targeting alpha output.
61
+ * Alpha formats (`"webm"`, `"mov"`, `"png-sequence"`, `"gif"`) force screenshot
62
+ * capture because BeginFrame does not preserve alpha on Linux headless-shell.
63
+ * HDR + alpha is unsupported: a warning is logged and output falls back to SDR.
64
+ * The engine's `initTransparentBackground` injects transparent-background CSS;
65
+ * avoid painting a fullscreen `body` / `#root` background for alpha output.
77
66
  */
78
67
  format?: RenderOutputFormat;
79
68
  /** GIF Netscape loop count. 0 means infinite looping. Only used with `format: "gif"`. */
@@ -481,7 +470,7 @@ interface ExtractedVideoReadinessInput {
481
470
  }
482
471
  export declare function collectVideoMetadataHints(extractedVideos: readonly ExtractedVideoReadinessInput[]): CaptureVideoMetadataHint[];
483
472
  export declare function findMissingFrameRanges(totalFrames: number, framesDir: string, frameExt: "jpg" | "png"): FrameRange[];
484
- export declare function buildMissingFrameRetryBatches(ranges: FrameRange[], maxWorkers: number, workDir: string, attempt: number, rangeStart?: number): WorkerTask[][];
473
+ export declare function buildMissingFrameRetryBatches(ranges: FrameRange[], maxWorkers: number, workDir: string, attempt: number, rangeStart?: number, stride?: number): WorkerTask[][];
485
474
  /**
486
475
  * The capture mode this render will REPORT, pre-capture.
487
476
  *
@@ -596,6 +585,8 @@ export declare function executeDiskCaptureWithAdaptiveRetry(options: {
596
585
  createBeforeCaptureHook: () => BeforeCaptureHook | null;
597
586
  abortSignal?: AbortSignal;
598
587
  onProgress?: (progress: ParallelProgress) => void;
588
+ /** Workers take frames round-robin so an in-order consumer (the overlapped encoder) is never starved. */
589
+ interleaveFrames?: boolean;
599
590
  cfg: EngineConfig;
600
591
  log: ProducerLogger;
601
592
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.200",
3
+ "version": "0.8.202",
4
4
  "description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,11 +35,11 @@
35
35
  "postcss": "^8.4.0",
36
36
  "puppeteer": "^25.2.1",
37
37
  "puppeteer-core": "^25.2.1",
38
- "@hyperframes/core": "0.8.200",
39
- "@hyperframes/studio-server": "0.8.200",
40
- "@hyperframes/lint": "0.8.200",
41
- "@hyperframes/engine": "0.8.200",
42
- "@hyperframes/parsers": "0.8.200"
38
+ "@hyperframes/core": "0.8.202",
39
+ "@hyperframes/engine": "0.8.202",
40
+ "@hyperframes/lint": "0.8.202",
41
+ "@hyperframes/studio-server": "0.8.202",
42
+ "@hyperframes/parsers": "0.8.202"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.10",
@@ -48,7 +48,7 @@
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^4.1.11",
51
- "@hyperframes/aws-lambda": "0.8.200"
51
+ "@hyperframes/aws-lambda": "0.8.202"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"