@hyperframes/producer 0.5.0-alpha.12 → 0.5.0-alpha.14

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.
@@ -101459,7 +101459,8 @@ var DEFAULT_CONFIG = {
101459
101459
  forceScreenshot: false,
101460
101460
  enableChunkedEncode: false,
101461
101461
  chunkSizeFrames: 360,
101462
- enableStreamingEncode: false,
101462
+ enableStreamingEncode: true,
101463
+ streamingEncodeMaxDurationSeconds: 240,
101463
101464
  ffmpegEncodeTimeout: 6e5,
101464
101465
  ffmpegProcessTimeout: 3e5,
101465
101466
  ffmpegStreamingTimeout: 6e5,
@@ -101521,6 +101522,13 @@ function resolveConfig(overrides) {
101521
101522
  "PRODUCER_ENABLE_STREAMING_ENCODE",
101522
101523
  DEFAULT_CONFIG.enableStreamingEncode
101523
101524
  ),
101525
+ streamingEncodeMaxDurationSeconds: Math.max(
101526
+ 0,
101527
+ envNum(
101528
+ "PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS",
101529
+ DEFAULT_CONFIG.streamingEncodeMaxDurationSeconds
101530
+ )
101531
+ ),
101524
101532
  ffmpegEncodeTimeout: envNum("FFMPEG_ENCODE_TIMEOUT_MS", DEFAULT_CONFIG.ffmpegEncodeTimeout),
101525
101533
  ffmpegProcessTimeout: envNum("FFMPEG_PROCESS_TIMEOUT_MS", DEFAULT_CONFIG.ffmpegProcessTimeout),
101526
101534
  ffmpegStreamingTimeout: envNum(
@@ -104463,6 +104471,44 @@ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100)
104463
104471
  }
104464
104472
  return Boolean(await page.evaluate(expression));
104465
104473
  }
104474
+ async function applyVideoMetadataHints(page, hints) {
104475
+ if (!hints || hints.length === 0) return;
104476
+ await page.evaluate(
104477
+ (metadataHints) => {
104478
+ for (const hint of metadataHints) {
104479
+ if (!hint.id || !Number.isFinite(hint.width) || !Number.isFinite(hint.height) || hint.width <= 0 || hint.height <= 0) {
104480
+ continue;
104481
+ }
104482
+ const video = document.getElementById(hint.id);
104483
+ if (!video) continue;
104484
+ if (!video.hasAttribute("width")) video.setAttribute("width", String(hint.width));
104485
+ if (!video.hasAttribute("height")) video.setAttribute("height", String(hint.height));
104486
+ const computed = window.getComputedStyle(video);
104487
+ if (!video.style.aspectRatio && (!computed.aspectRatio || computed.aspectRatio === "auto")) {
104488
+ video.style.aspectRatio = `${hint.width} / ${hint.height}`;
104489
+ }
104490
+ }
104491
+ },
104492
+ [...hints]
104493
+ );
104494
+ }
104495
+ async function waitForOptionalTailwindReady(page, timeoutMs) {
104496
+ const hasTailwindReady = await page.evaluate(
104497
+ `(() => { const ready = window.__tailwindReady; return !!ready && typeof ready.then === "function"; })()`
104498
+ );
104499
+ if (!hasTailwindReady) return;
104500
+ const ready = await Promise.race([
104501
+ page.evaluate(
104502
+ `Promise.resolve(window.__tailwindReady).then(() => true, () => false)`
104503
+ ),
104504
+ new Promise((resolve14) => setTimeout(() => resolve14(false), timeoutMs))
104505
+ ]);
104506
+ if (!ready) {
104507
+ throw new Error(
104508
+ `[FrameCapture] window.__tailwindReady not resolved after ${timeoutMs}ms. Tailwind browser runtime must finish before frame capture starts.`
104509
+ );
104510
+ }
104511
+ }
104466
104512
  async function initializeSession(session) {
104467
104513
  const { page, serverUrl } = session;
104468
104514
  page.on("console", (msg) => {
@@ -104506,6 +104552,7 @@ async function initializeSession(session) {
104506
104552
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
104507
104553
  );
104508
104554
  }
104555
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
104509
104556
  const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
104510
104557
  const videosReady = await pollPageExpression(
104511
104558
  page,
@@ -104518,6 +104565,7 @@ async function initializeSession(session) {
104518
104565
  );
104519
104566
  }
104520
104567
  await page.evaluate(`document.fonts?.ready`);
104568
+ await waitForOptionalTailwindReady(page, pageReadyTimeout2);
104521
104569
  if (session.options.format === "png") {
104522
104570
  await initTransparentBackground(session.page);
104523
104571
  }
@@ -104572,6 +104620,7 @@ async function initializeSession(session) {
104572
104620
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
104573
104621
  );
104574
104622
  }
104623
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
104575
104624
  const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
104576
104625
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout);
104577
104626
  while (Date.now() < videoDeadline) {
@@ -104582,6 +104631,7 @@ async function initializeSession(session) {
104582
104631
  await new Promise((r) => setTimeout(r, 100));
104583
104632
  }
104584
104633
  await page.evaluate(`document.fonts?.ready`);
104634
+ await waitForOptionalTailwindReady(page, pageReadyTimeout);
104585
104635
  warmupRunning = false;
104586
104636
  session.beginFrameTimeTicks = (warmupTicks + 10) * session.beginFrameIntervalMs;
104587
104637
  if (session.options.format === "png") {
@@ -110929,6 +110979,24 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
110929
110979
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
110930
110980
  });
110931
110981
  }
110982
+ function collectVideoReadinessSkipIds(nativeHdrVideoIds, extractedVideos) {
110983
+ return Array.from(
110984
+ /* @__PURE__ */ new Set([
110985
+ ...nativeHdrVideoIds,
110986
+ ...extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => video.videoId)
110987
+ ])
110988
+ ).sort();
110989
+ }
110990
+ function hasUsableVideoDimensions(metadata) {
110991
+ return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
110992
+ }
110993
+ function collectVideoMetadataHints(extractedVideos) {
110994
+ return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
110995
+ id: video.videoId,
110996
+ width: video.metadata.width,
110997
+ height: video.metadata.height
110998
+ })).sort((a, b) => a.id.localeCompare(b.id));
110999
+ }
110932
111000
  function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log = defaultLogger, measuredCaptureCost) {
110933
111001
  const captureCost = combineCaptureCostEstimates(
110934
111002
  estimateCaptureCostMultiplier(compiled, composition),
@@ -111594,6 +111662,27 @@ function createRenderJob(config2) {
111594
111662
  function normalizeCompositionSrcPath(srcPath) {
111595
111663
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
111596
111664
  }
111665
+ function createStandaloneEntryRenderClone(root, host) {
111666
+ const hostClone = host.cloneNode(true);
111667
+ hostClone.setAttribute("data-start", "0");
111668
+ if (root === host) return hostClone;
111669
+ const rootClone = root.cloneNode(false);
111670
+ rootClone.appendChild(hostClone);
111671
+ return rootClone;
111672
+ }
111673
+ function replaceBodyWithRenderClone(body, renderClone) {
111674
+ while (body.firstChild) {
111675
+ body.removeChild(body.firstChild);
111676
+ }
111677
+ body.appendChild(renderClone);
111678
+ }
111679
+ function shouldUseStreamingEncode(cfg, outputFormat, workerCount, durationSeconds) {
111680
+ if (!cfg.enableStreamingEncode) return false;
111681
+ if (outputFormat === "png-sequence") return false;
111682
+ if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return false;
111683
+ if (durationSeconds > cfg.streamingEncodeMaxDurationSeconds) return false;
111684
+ return workerCount === 1;
111685
+ }
111597
111686
  function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
111598
111687
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
111599
111688
  const { document: document2 } = parseHTML(indexHtml);
@@ -111608,16 +111697,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
111608
111697
  (candidate) => candidate.hasAttribute("data-composition-id")
111609
111698
  ) ?? null;
111610
111699
  if (!root) return null;
111611
- const hostClone = host.cloneNode(true);
111612
- hostClone.setAttribute("data-start", "0");
111613
- body.innerHTML = "";
111614
- if (root === host) {
111615
- body.appendChild(hostClone);
111616
- return document2.toString();
111617
- }
111618
- const rootClone = root.cloneNode(false);
111619
- rootClone.appendChild(hostClone);
111620
- body.appendChild(rootClone);
111700
+ const renderClone = createStandaloneEntryRenderClone(root, host);
111701
+ replaceBodyWithRenderClone(body, renderClone);
111621
111702
  return document2.toString();
111622
111703
  }
111623
111704
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
@@ -111649,7 +111730,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111649
111730
  }
111650
111731
  const enableChunkedEncode = cfg.enableChunkedEncode;
111651
111732
  const chunkedEncodeSize = cfg.chunkSizeFrames;
111652
- const enableStreamingEncode = cfg.enableStreamingEncode && !isPngSequence;
111653
111733
  let peakRssBytes = 0;
111654
111734
  let peakHeapUsedBytes = 0;
111655
111735
  const sampleMemory = () => {
@@ -111943,6 +112023,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111943
112023
  let frameLookup = null;
111944
112024
  const compiledDir = join16(workDir, "compiled");
111945
112025
  let extractionResult = null;
112026
+ let videoReadinessSkipIds = [];
112027
+ let videoMetadataHints = [];
111946
112028
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
111947
112029
  const videoTransfers = /* @__PURE__ */ new Map();
111948
112030
  if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
@@ -111999,6 +112081,11 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111999
112081
  if (extractionResult.extracted.length > 0) {
112000
112082
  frameLookup = createFrameLookupTable(composition.videos, extractionResult.extracted);
112001
112083
  }
112084
+ videoReadinessSkipIds = collectVideoReadinessSkipIds(
112085
+ nativeHdrVideoIds,
112086
+ extractionResult.extracted
112087
+ );
112088
+ videoMetadataHints = collectVideoMetadataHints(extractionResult.extracted);
112002
112089
  perfStages.videoExtractMs = Date.now() - stage2Start;
112003
112090
  const existingAudioSrcs = new Set(composition.audios.map((a) => a.src));
112004
112091
  for (const ext of extractionResult.extracted) {
@@ -112112,9 +112199,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112112
112199
  format: needsAlpha ? "png" : "jpeg",
112113
112200
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
112114
112201
  };
112115
- const buildHdrCaptureOptions = () => ({
112202
+ const buildCaptureOptions = () => ({
112116
112203
  ...captureOptions,
112117
- skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
112204
+ videoMetadataHints,
112205
+ skipReadinessVideoIds: videoReadinessSkipIds
112118
112206
  });
112119
112207
  let captureCalibration;
112120
112208
  let switchedToScreenshotAfterCalibration = false;
@@ -112127,7 +112215,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112127
112215
  calibrationSession = await createCaptureSession(
112128
112216
  fileServer.url,
112129
112217
  calibrationDir,
112130
- buildHdrCaptureOptions(),
112218
+ buildCaptureOptions(),
112131
112219
  videoInjector,
112132
112220
  calibrationCfg
112133
112221
  );
@@ -112210,6 +112298,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112210
112298
  await closeCaptureSession(probeSession);
112211
112299
  probeSession = null;
112212
112300
  }
112301
+ let useStreamingEncode = shouldUseStreamingEncode(cfg, outputFormat, workerCount, job.duration);
112302
+ log.info("streaming-encode gate", {
112303
+ enabled: useStreamingEncode,
112304
+ configFlag: cfg.enableStreamingEncode,
112305
+ outputFormat,
112306
+ workerCount,
112307
+ durationSeconds: job.duration,
112308
+ maxDurationSeconds: cfg.streamingEncodeMaxDurationSeconds
112309
+ });
112213
112310
  const captureAttempts = [];
112214
112311
  const FORMAT_EXT = {
112215
112312
  mp4: ".mp4",
@@ -112251,7 +112348,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112251
112348
  const domSession = await createCaptureSession(
112252
112349
  fileServer.url,
112253
112350
  framesDir,
112254
- buildHdrCaptureOptions(),
112351
+ buildCaptureOptions(),
112255
112352
  createVideoFrameInjector(frameLookup),
112256
112353
  cfg
112257
112354
  );
@@ -112749,28 +112846,47 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112749
112846
  } else {
112750
112847
  let streamingEncoder = null;
112751
112848
  let streamingEncoderClosed = false;
112752
- if (enableStreamingEncode) {
112753
- streamingEncoder = await spawnStreamingEncoder(
112754
- videoOnlyPath,
112755
- {
112756
- fps: job.config.fps,
112757
- width,
112758
- height,
112759
- codec: preset.codec,
112760
- preset: preset.preset,
112761
- quality: effectiveQuality,
112762
- bitrate: effectiveBitrate,
112763
- pixelFormat: preset.pixelFormat,
112764
- useGpu: job.config.useGpu,
112765
- imageFormat: captureOptions.format || "jpeg",
112766
- hdr: preset.hdr
112767
- },
112768
- abortSignal
112769
- );
112770
- assertNotAborted();
112849
+ if (useStreamingEncode) {
112850
+ try {
112851
+ streamingEncoder = await spawnStreamingEncoder(
112852
+ videoOnlyPath,
112853
+ {
112854
+ fps: job.config.fps,
112855
+ width,
112856
+ height,
112857
+ codec: preset.codec,
112858
+ preset: preset.preset,
112859
+ quality: effectiveQuality,
112860
+ bitrate: effectiveBitrate,
112861
+ pixelFormat: preset.pixelFormat,
112862
+ useGpu: job.config.useGpu,
112863
+ imageFormat: captureOptions.format || "jpeg",
112864
+ hdr: preset.hdr
112865
+ },
112866
+ abortSignal
112867
+ );
112868
+ assertNotAborted();
112869
+ } catch (err) {
112870
+ if (abortSignal?.aborted) {
112871
+ if (streamingEncoder && !streamingEncoderClosed) {
112872
+ await streamingEncoder.close().catch(() => {
112873
+ });
112874
+ streamingEncoderClosed = true;
112875
+ }
112876
+ throw err;
112877
+ }
112878
+ useStreamingEncode = false;
112879
+ streamingEncoder = null;
112880
+ log.warn("[Render] Streaming encoder spawn failed; falling back to disk-frame encode.", {
112881
+ error: err instanceof Error ? err.message : String(err),
112882
+ outputFormat,
112883
+ workerCount,
112884
+ durationSeconds: job.duration
112885
+ });
112886
+ }
112771
112887
  }
112772
112888
  try {
112773
- if (enableStreamingEncode && streamingEncoder) {
112889
+ if (useStreamingEncode && streamingEncoder) {
112774
112890
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
112775
112891
  const currentEncoder = streamingEncoder;
112776
112892
  if (workerCount > 1) {
@@ -112784,7 +112900,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112784
112900
  fileServer.url,
112785
112901
  workDir,
112786
112902
  tasks,
112787
- buildHdrCaptureOptions(),
112903
+ buildCaptureOptions(),
112788
112904
  () => createVideoFrameInjector(frameLookup),
112789
112905
  abortSignal,
112790
112906
  (progress) => {
@@ -112814,7 +112930,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112814
112930
  const session = probeSession ?? await createCaptureSession(
112815
112931
  fileServer.url,
112816
112932
  framesDir,
112817
- buildHdrCaptureOptions(),
112933
+ buildCaptureOptions(),
112818
112934
  videoInjector,
112819
112935
  cfg
112820
112936
  );
@@ -112869,7 +112985,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112869
112985
  initialWorkerCount: workerCount,
112870
112986
  allowRetry: job.config.workers === void 0,
112871
112987
  frameExt: needsAlpha ? "png" : "jpg",
112872
- captureOptions: buildHdrCaptureOptions(),
112988
+ captureOptions: buildCaptureOptions(),
112873
112989
  createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
112874
112990
  abortSignal,
112875
112991
  onProgress: (progress) => {
@@ -112904,7 +113020,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112904
113020
  const session = probeSession ?? await createCaptureSession(
112905
113021
  fileServer.url,
112906
113022
  framesDir,
112907
- buildHdrCaptureOptions(),
113023
+ buildCaptureOptions(),
112908
113024
  videoInjector,
112909
113025
  cfg
112910
113026
  );