@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/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)}`);
@@ -2463,8 +2479,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
2463
2479
  const markedStaticHtml = markInferredVariableMediaDurations(staticCompiled, mediaUnresolved);
2464
2480
  let compiledHtml = resolutions.length > 0 ? injectDurations(markedStaticHtml, resolutions) : markedStaticHtml;
2465
2481
  const preResolved = extractResolvedMedia(compiledHtml);
2482
+ const hiddenVideoIds = collectHiddenVideoIds(compiledHtml);
2466
2483
  const clampResults = await Promise.all(
2467
- 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) => {
2468
2485
  const { duration: maxDuration } = await resolveMediaDuration(
2469
2486
  el.src,
2470
2487
  el.mediaStart,
@@ -4135,6 +4152,7 @@ async function runProbeStage(input) {
4135
4152
  height,
4136
4153
  fps: job.config.fps,
4137
4154
  format: captureFormat,
4155
+ transparent: needsAlpha,
4138
4156
  quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
4139
4157
  variables: job.config.variables,
4140
4158
  deviceScaleFactor,
@@ -5772,6 +5790,7 @@ async function runCaptureStage(input) {
5772
5790
  createBeforeCaptureHook: createRenderVideoFrameInjector,
5773
5791
  abortSignal,
5774
5792
  frameRangeStart: frameRange?.startFrame,
5793
+ interleaveFrames: input.interleaveFrames,
5775
5794
  dedupPerfs,
5776
5795
  pageErrorLines: workerPageErrors,
5777
5796
  onProgress: (progress) => {
@@ -6349,9 +6368,6 @@ async function runEncodeStage(input) {
6349
6368
  return { encodeMs: Date.now() - stage5Start };
6350
6369
  }
6351
6370
  const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
6352
- const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
6353
- const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
6354
- const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
6355
6371
  const frameExt = frameFileExtension2(input.captureImageFormat);
6356
6372
  const framePattern = `frame_%06d.${frameExt}`;
6357
6373
  const encoderOpts = videoEncoderOptions(input, engineCfg);
@@ -6362,7 +6378,7 @@ async function runEncodeStage(input) {
6362
6378
  encoderOpts,
6363
6379
  chunkedEncodeSize,
6364
6380
  abortSignal,
6365
- videoEngineCfg,
6381
+ engineCfg,
6366
6382
  onFramesEncoded
6367
6383
  ) : await encodeFramesFromDir(
6368
6384
  framesDir,
@@ -6370,7 +6386,7 @@ async function runEncodeStage(input) {
6370
6386
  videoOnlyPath,
6371
6387
  encoderOpts,
6372
6388
  abortSignal,
6373
- videoEngineCfg,
6389
+ engineCfg,
6374
6390
  onFramesEncoded
6375
6391
  );
6376
6392
  assertNotAborted();
@@ -6498,16 +6514,42 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
6498
6514
  }
6499
6515
  return ranges;
6500
6516
  }
6501
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
6517
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
6502
6518
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
6503
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
+ }
6504
6546
  for (let i = 0; i < ranges.length; i += workersPerBatch) {
6505
6547
  const batchIndex = batches.length;
6506
6548
  const batch = ranges.slice(i, i + workersPerBatch).map((range, workerId) => ({
6507
6549
  workerId,
6508
6550
  startFrame: rangeStart + range.startFrame,
6509
6551
  endFrame: rangeStart + range.endFrame,
6510
- outputDir: join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
6552
+ outputDir: outputDirFor(batchIndex, workerId),
6511
6553
  outputFrameOffset: rangeStart
6512
6554
  }));
6513
6555
  batches.push(batch);
@@ -6565,8 +6607,16 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6565
6607
  currentWorkers,
6566
6608
  attemptWorkDir,
6567
6609
  attempt,
6568
- rangeStart
6569
- ) : [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
+ ];
6570
6620
  options.dedupPerfs.length = 0;
6571
6621
  try {
6572
6622
  for (const tasks of batches) {
@@ -11058,7 +11108,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11058
11108
  assertNotAborted: () => {
11059
11109
  },
11060
11110
  frameRange: range,
11061
- onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
11111
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
11112
+ interleaveFrames: overlap.current !== null
11062
11113
  }).catch(async (captureError) => {
11063
11114
  await overlap.current?.abort();
11064
11115
  overlap.current = null;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "72ff9fd22c5222e47fb7ec1b9bb2e3643440846a146df5ced666c818e2027d86",
4
+ "sha256": "58749315991d8ab9f1dc5f8d6968335e10241f89793d85fc83f8aada8dba5a19",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"