@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.
package/dist/index.js CHANGED
@@ -98670,7 +98670,8 @@ var DEFAULT_CONFIG = {
98670
98670
  forceScreenshot: false,
98671
98671
  enableChunkedEncode: false,
98672
98672
  chunkSizeFrames: 360,
98673
- enableStreamingEncode: false,
98673
+ enableStreamingEncode: true,
98674
+ streamingEncodeMaxDurationSeconds: 240,
98674
98675
  ffmpegEncodeTimeout: 6e5,
98675
98676
  ffmpegProcessTimeout: 3e5,
98676
98677
  ffmpegStreamingTimeout: 6e5,
@@ -98732,6 +98733,13 @@ function resolveConfig(overrides) {
98732
98733
  "PRODUCER_ENABLE_STREAMING_ENCODE",
98733
98734
  DEFAULT_CONFIG.enableStreamingEncode
98734
98735
  ),
98736
+ streamingEncodeMaxDurationSeconds: Math.max(
98737
+ 0,
98738
+ envNum(
98739
+ "PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS",
98740
+ DEFAULT_CONFIG.streamingEncodeMaxDurationSeconds
98741
+ )
98742
+ ),
98735
98743
  ffmpegEncodeTimeout: envNum("FFMPEG_ENCODE_TIMEOUT_MS", DEFAULT_CONFIG.ffmpegEncodeTimeout),
98736
98744
  ffmpegProcessTimeout: envNum("FFMPEG_PROCESS_TIMEOUT_MS", DEFAULT_CONFIG.ffmpegProcessTimeout),
98737
98745
  ffmpegStreamingTimeout: envNum(
@@ -101674,6 +101682,44 @@ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100)
101674
101682
  }
101675
101683
  return Boolean(await page.evaluate(expression));
101676
101684
  }
101685
+ async function applyVideoMetadataHints(page, hints) {
101686
+ if (!hints || hints.length === 0) return;
101687
+ await page.evaluate(
101688
+ (metadataHints) => {
101689
+ for (const hint of metadataHints) {
101690
+ if (!hint.id || !Number.isFinite(hint.width) || !Number.isFinite(hint.height) || hint.width <= 0 || hint.height <= 0) {
101691
+ continue;
101692
+ }
101693
+ const video = document.getElementById(hint.id);
101694
+ if (!video) continue;
101695
+ if (!video.hasAttribute("width")) video.setAttribute("width", String(hint.width));
101696
+ if (!video.hasAttribute("height")) video.setAttribute("height", String(hint.height));
101697
+ const computed = window.getComputedStyle(video);
101698
+ if (!video.style.aspectRatio && (!computed.aspectRatio || computed.aspectRatio === "auto")) {
101699
+ video.style.aspectRatio = `${hint.width} / ${hint.height}`;
101700
+ }
101701
+ }
101702
+ },
101703
+ [...hints]
101704
+ );
101705
+ }
101706
+ async function waitForOptionalTailwindReady(page, timeoutMs) {
101707
+ const hasTailwindReady = await page.evaluate(
101708
+ `(() => { const ready = window.__tailwindReady; return !!ready && typeof ready.then === "function"; })()`
101709
+ );
101710
+ if (!hasTailwindReady) return;
101711
+ const ready = await Promise.race([
101712
+ page.evaluate(
101713
+ `Promise.resolve(window.__tailwindReady).then(() => true, () => false)`
101714
+ ),
101715
+ new Promise((resolve14) => setTimeout(() => resolve14(false), timeoutMs))
101716
+ ]);
101717
+ if (!ready) {
101718
+ throw new Error(
101719
+ `[FrameCapture] window.__tailwindReady not resolved after ${timeoutMs}ms. Tailwind browser runtime must finish before frame capture starts.`
101720
+ );
101721
+ }
101722
+ }
101677
101723
  async function initializeSession(session) {
101678
101724
  const { page, serverUrl } = session;
101679
101725
  page.on("console", (msg) => {
@@ -101717,6 +101763,7 @@ async function initializeSession(session) {
101717
101763
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
101718
101764
  );
101719
101765
  }
101766
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
101720
101767
  const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
101721
101768
  const videosReady = await pollPageExpression(
101722
101769
  page,
@@ -101729,6 +101776,7 @@ async function initializeSession(session) {
101729
101776
  );
101730
101777
  }
101731
101778
  await page.evaluate(`document.fonts?.ready`);
101779
+ await waitForOptionalTailwindReady(page, pageReadyTimeout2);
101732
101780
  if (session.options.format === "png") {
101733
101781
  await initTransparentBackground(session.page);
101734
101782
  }
@@ -101783,6 +101831,7 @@ async function initializeSession(session) {
101783
101831
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
101784
101832
  );
101785
101833
  }
101834
+ await applyVideoMetadataHints(page, session.options.videoMetadataHints);
101786
101835
  const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
101787
101836
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout);
101788
101837
  while (Date.now() < videoDeadline) {
@@ -101793,6 +101842,7 @@ async function initializeSession(session) {
101793
101842
  await new Promise((r) => setTimeout(r, 100));
101794
101843
  }
101795
101844
  await page.evaluate(`document.fonts?.ready`);
101845
+ await waitForOptionalTailwindReady(page, pageReadyTimeout);
101796
101846
  warmupRunning = false;
101797
101847
  session.beginFrameTimeTicks = (warmupTicks + 10) * session.beginFrameIntervalMs;
101798
101848
  if (session.options.format === "png") {
@@ -110764,6 +110814,24 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
110764
110814
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
110765
110815
  });
110766
110816
  }
110817
+ function collectVideoReadinessSkipIds(nativeHdrVideoIds, extractedVideos) {
110818
+ return Array.from(
110819
+ /* @__PURE__ */ new Set([
110820
+ ...nativeHdrVideoIds,
110821
+ ...extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => video.videoId)
110822
+ ])
110823
+ ).sort();
110824
+ }
110825
+ function hasUsableVideoDimensions(metadata) {
110826
+ return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
110827
+ }
110828
+ function collectVideoMetadataHints(extractedVideos) {
110829
+ return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
110830
+ id: video.videoId,
110831
+ width: video.metadata.width,
110832
+ height: video.metadata.height
110833
+ })).sort((a, b) => a.id.localeCompare(b.id));
110834
+ }
110767
110835
  function resolveRenderWorkerCount(totalFrames, requestedWorkers, cfg, compiled, composition, log = defaultLogger, measuredCaptureCost) {
110768
110836
  const captureCost = combineCaptureCostEstimates(
110769
110837
  estimateCaptureCostMultiplier(compiled, composition),
@@ -111429,6 +111497,27 @@ function createRenderJob(config2) {
111429
111497
  function normalizeCompositionSrcPath(srcPath) {
111430
111498
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
111431
111499
  }
111500
+ function createStandaloneEntryRenderClone(root, host) {
111501
+ const hostClone = host.cloneNode(true);
111502
+ hostClone.setAttribute("data-start", "0");
111503
+ if (root === host) return hostClone;
111504
+ const rootClone = root.cloneNode(false);
111505
+ rootClone.appendChild(hostClone);
111506
+ return rootClone;
111507
+ }
111508
+ function replaceBodyWithRenderClone(body, renderClone) {
111509
+ while (body.firstChild) {
111510
+ body.removeChild(body.firstChild);
111511
+ }
111512
+ body.appendChild(renderClone);
111513
+ }
111514
+ function shouldUseStreamingEncode(cfg, outputFormat, workerCount, durationSeconds) {
111515
+ if (!cfg.enableStreamingEncode) return false;
111516
+ if (outputFormat === "png-sequence") return false;
111517
+ if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) return false;
111518
+ if (durationSeconds > cfg.streamingEncodeMaxDurationSeconds) return false;
111519
+ return workerCount === 1;
111520
+ }
111432
111521
  function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
111433
111522
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
111434
111523
  const { document: document2 } = parseHTML(indexHtml);
@@ -111443,16 +111532,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
111443
111532
  (candidate) => candidate.hasAttribute("data-composition-id")
111444
111533
  ) ?? null;
111445
111534
  if (!root) return null;
111446
- const hostClone = host.cloneNode(true);
111447
- hostClone.setAttribute("data-start", "0");
111448
- body.innerHTML = "";
111449
- if (root === host) {
111450
- body.appendChild(hostClone);
111451
- return document2.toString();
111452
- }
111453
- const rootClone = root.cloneNode(false);
111454
- rootClone.appendChild(hostClone);
111455
- body.appendChild(rootClone);
111535
+ const renderClone = createStandaloneEntryRenderClone(root, host);
111536
+ replaceBodyWithRenderClone(body, renderClone);
111456
111537
  return document2.toString();
111457
111538
  }
111458
111539
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
@@ -111484,7 +111565,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111484
111565
  }
111485
111566
  const enableChunkedEncode = cfg.enableChunkedEncode;
111486
111567
  const chunkedEncodeSize = cfg.chunkSizeFrames;
111487
- const enableStreamingEncode = cfg.enableStreamingEncode && !isPngSequence;
111488
111568
  let peakRssBytes = 0;
111489
111569
  let peakHeapUsedBytes = 0;
111490
111570
  const sampleMemory = () => {
@@ -111778,6 +111858,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111778
111858
  let frameLookup = null;
111779
111859
  const compiledDir = join16(workDir, "compiled");
111780
111860
  let extractionResult = null;
111861
+ let videoReadinessSkipIds = [];
111862
+ let videoMetadataHints = [];
111781
111863
  const nativeHdrVideoIds = /* @__PURE__ */ new Set();
111782
111864
  const videoTransfers = /* @__PURE__ */ new Map();
111783
111865
  if (job.config.hdrMode !== "force-sdr" && composition.videos.length > 0) {
@@ -111834,6 +111916,11 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111834
111916
  if (extractionResult.extracted.length > 0) {
111835
111917
  frameLookup = createFrameLookupTable(composition.videos, extractionResult.extracted);
111836
111918
  }
111919
+ videoReadinessSkipIds = collectVideoReadinessSkipIds(
111920
+ nativeHdrVideoIds,
111921
+ extractionResult.extracted
111922
+ );
111923
+ videoMetadataHints = collectVideoMetadataHints(extractionResult.extracted);
111837
111924
  perfStages.videoExtractMs = Date.now() - stage2Start;
111838
111925
  const existingAudioSrcs = new Set(composition.audios.map((a) => a.src));
111839
111926
  for (const ext of extractionResult.extracted) {
@@ -111947,9 +112034,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111947
112034
  format: needsAlpha ? "png" : "jpeg",
111948
112035
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
111949
112036
  };
111950
- const buildHdrCaptureOptions = () => ({
112037
+ const buildCaptureOptions = () => ({
111951
112038
  ...captureOptions,
111952
- skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
112039
+ videoMetadataHints,
112040
+ skipReadinessVideoIds: videoReadinessSkipIds
111953
112041
  });
111954
112042
  let captureCalibration;
111955
112043
  let switchedToScreenshotAfterCalibration = false;
@@ -111962,7 +112050,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111962
112050
  calibrationSession = await createCaptureSession(
111963
112051
  fileServer.url,
111964
112052
  calibrationDir,
111965
- buildHdrCaptureOptions(),
112053
+ buildCaptureOptions(),
111966
112054
  videoInjector,
111967
112055
  calibrationCfg
111968
112056
  );
@@ -112045,6 +112133,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112045
112133
  await closeCaptureSession(probeSession);
112046
112134
  probeSession = null;
112047
112135
  }
112136
+ let useStreamingEncode = shouldUseStreamingEncode(cfg, outputFormat, workerCount, job.duration);
112137
+ log.info("streaming-encode gate", {
112138
+ enabled: useStreamingEncode,
112139
+ configFlag: cfg.enableStreamingEncode,
112140
+ outputFormat,
112141
+ workerCount,
112142
+ durationSeconds: job.duration,
112143
+ maxDurationSeconds: cfg.streamingEncodeMaxDurationSeconds
112144
+ });
112048
112145
  const captureAttempts = [];
112049
112146
  const FORMAT_EXT = {
112050
112147
  mp4: ".mp4",
@@ -112086,7 +112183,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112086
112183
  const domSession = await createCaptureSession(
112087
112184
  fileServer.url,
112088
112185
  framesDir,
112089
- buildHdrCaptureOptions(),
112186
+ buildCaptureOptions(),
112090
112187
  createVideoFrameInjector(frameLookup),
112091
112188
  cfg
112092
112189
  );
@@ -112584,28 +112681,47 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112584
112681
  } else {
112585
112682
  let streamingEncoder = null;
112586
112683
  let streamingEncoderClosed = false;
112587
- if (enableStreamingEncode) {
112588
- streamingEncoder = await spawnStreamingEncoder(
112589
- videoOnlyPath,
112590
- {
112591
- fps: job.config.fps,
112592
- width,
112593
- height,
112594
- codec: preset.codec,
112595
- preset: preset.preset,
112596
- quality: effectiveQuality,
112597
- bitrate: effectiveBitrate,
112598
- pixelFormat: preset.pixelFormat,
112599
- useGpu: job.config.useGpu,
112600
- imageFormat: captureOptions.format || "jpeg",
112601
- hdr: preset.hdr
112602
- },
112603
- abortSignal
112604
- );
112605
- assertNotAborted();
112684
+ if (useStreamingEncode) {
112685
+ try {
112686
+ streamingEncoder = await spawnStreamingEncoder(
112687
+ videoOnlyPath,
112688
+ {
112689
+ fps: job.config.fps,
112690
+ width,
112691
+ height,
112692
+ codec: preset.codec,
112693
+ preset: preset.preset,
112694
+ quality: effectiveQuality,
112695
+ bitrate: effectiveBitrate,
112696
+ pixelFormat: preset.pixelFormat,
112697
+ useGpu: job.config.useGpu,
112698
+ imageFormat: captureOptions.format || "jpeg",
112699
+ hdr: preset.hdr
112700
+ },
112701
+ abortSignal
112702
+ );
112703
+ assertNotAborted();
112704
+ } catch (err) {
112705
+ if (abortSignal?.aborted) {
112706
+ if (streamingEncoder && !streamingEncoderClosed) {
112707
+ await streamingEncoder.close().catch(() => {
112708
+ });
112709
+ streamingEncoderClosed = true;
112710
+ }
112711
+ throw err;
112712
+ }
112713
+ useStreamingEncode = false;
112714
+ streamingEncoder = null;
112715
+ log.warn("[Render] Streaming encoder spawn failed; falling back to disk-frame encode.", {
112716
+ error: err instanceof Error ? err.message : String(err),
112717
+ outputFormat,
112718
+ workerCount,
112719
+ durationSeconds: job.duration
112720
+ });
112721
+ }
112606
112722
  }
112607
112723
  try {
112608
- if (enableStreamingEncode && streamingEncoder) {
112724
+ if (useStreamingEncode && streamingEncoder) {
112609
112725
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
112610
112726
  const currentEncoder = streamingEncoder;
112611
112727
  if (workerCount > 1) {
@@ -112619,7 +112735,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112619
112735
  fileServer.url,
112620
112736
  workDir,
112621
112737
  tasks,
112622
- buildHdrCaptureOptions(),
112738
+ buildCaptureOptions(),
112623
112739
  () => createVideoFrameInjector(frameLookup),
112624
112740
  abortSignal,
112625
112741
  (progress) => {
@@ -112649,7 +112765,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112649
112765
  const session = probeSession ?? await createCaptureSession(
112650
112766
  fileServer.url,
112651
112767
  framesDir,
112652
- buildHdrCaptureOptions(),
112768
+ buildCaptureOptions(),
112653
112769
  videoInjector,
112654
112770
  cfg
112655
112771
  );
@@ -112704,7 +112820,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112704
112820
  initialWorkerCount: workerCount,
112705
112821
  allowRetry: job.config.workers === void 0,
112706
112822
  frameExt: needsAlpha ? "png" : "jpg",
112707
- captureOptions: buildHdrCaptureOptions(),
112823
+ captureOptions: buildCaptureOptions(),
112708
112824
  createBeforeCaptureHook: () => createVideoFrameInjector(frameLookup),
112709
112825
  abortSignal,
112710
112826
  onProgress: (progress) => {
@@ -112739,7 +112855,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112739
112855
  const session = probeSession ?? await createCaptureSession(
112740
112856
  fileServer.url,
112741
112857
  framesDir,
112742
- buildHdrCaptureOptions(),
112858
+ buildCaptureOptions(),
112743
112859
  videoInjector,
112744
112860
  cfg
112745
112861
  );