@hyperframes/producer 0.8.193 → 0.8.194

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
@@ -1564,8 +1564,11 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
1564
1564
  `Render artifact is truncated: expected ${expectedSeconds.toFixed(3)}s, probed ${probedSeconds.toFixed(3)}s (deficit ${deficit.toFixed(3)}s exceeds tolerance ${toleranceSeconds.toFixed(3)}s). Artifact: ${stagingPath}`
1565
1565
  );
1566
1566
  }
1567
+ function hasUsableFrameCount(frames) {
1568
+ return frames !== void 0 && Number.isFinite(frames) && frames > 0;
1569
+ }
1567
1570
  function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
1568
- if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
1571
+ if (!hasUsableFrameCount(probedFrames)) {
1569
1572
  return;
1570
1573
  }
1571
1574
  const shortfall = expectedFrames - probedFrames;
@@ -1585,9 +1588,10 @@ function assertSizeMatches(expected, probed, stagingPath) {
1585
1588
  );
1586
1589
  }
1587
1590
  var ArtifactTransaction = class {
1588
- constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
1591
+ constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe, logger = defaultLogger) {
1589
1592
  this.kind = kind;
1590
1593
  this.fileSystem = fileSystem;
1594
+ this.logger = logger;
1591
1595
  this.destinationPath = resolve4(destinationPath);
1592
1596
  this.transactionDirectory = createSiblingTransactionDirectory(this.destinationPath);
1593
1597
  this.stagingPath = join5(this.transactionDirectory, basename3(this.destinationPath));
@@ -1670,6 +1674,19 @@ var ArtifactTransaction = class {
1670
1674
  probePath
1671
1675
  );
1672
1676
  if (expected.expectedFrames !== void 0) {
1677
+ if (!hasUsableFrameCount(probed.frames)) {
1678
+ try {
1679
+ this.logger.debug(
1680
+ "[Render] Frame-count validation skipped: no usable frame count was reported.",
1681
+ {
1682
+ event: "artifact_frame_count_unavailable",
1683
+ artifact: this.destinationPath,
1684
+ expectedFrames: expected.expectedFrames
1685
+ }
1686
+ );
1687
+ } catch {
1688
+ }
1689
+ }
1673
1690
  assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
1674
1691
  }
1675
1692
  assertSizeMatches(expected, probed, probePath);
@@ -5257,6 +5274,8 @@ import {
5257
5274
  rewriteInlineStyleAssetUrls
5258
5275
  } from "@hyperframes/core";
5259
5276
  import { MAX_AUDIO_GAIN } from "@hyperframes/core/audio-gain";
5277
+ import { readPlaybackRate } from "@hyperframes/parsers/media-duration";
5278
+ import { resolveRateSpec } from "@hyperframes/core/speed-ramp";
5260
5279
  import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
5261
5280
  import {
5262
5281
  assignBundledRuntimeCompositionIds,
@@ -7232,6 +7251,9 @@ async function discoverMediaFromBrowser(page) {
7232
7251
  const intrinsicDuration = isImage ? 0 : htmlEl.duration;
7233
7252
  const playbackStartRaw = htmlEl.getAttribute("data-playback-start");
7234
7253
  const mediaStartRaw = htmlEl.getAttribute("data-media-start");
7254
+ const playbackRateRaw = htmlEl.getAttribute("data-playback-rate");
7255
+ const automationRaw = htmlEl.getAttribute("data-automation");
7256
+ const defaultPlaybackRate = isImage ? 1 : htmlEl.defaultPlaybackRate;
7235
7257
  const loop = htmlEl.hasAttribute("loop");
7236
7258
  const hasAudio = htmlEl.getAttribute("data-has-audio") === "true";
7237
7259
  const volume = parseFloat(htmlEl.getAttribute("data-volume") || "1");
@@ -7247,6 +7269,9 @@ async function discoverMediaFromBrowser(page) {
7247
7269
  durationInferred,
7248
7270
  playbackStartRaw,
7249
7271
  mediaStartRaw,
7272
+ playbackRateRaw,
7273
+ automationRaw,
7274
+ defaultPlaybackRate,
7250
7275
  loop,
7251
7276
  hasAudio,
7252
7277
  volume,
@@ -7256,7 +7281,17 @@ async function discoverMediaFromBrowser(page) {
7256
7281
  return results;
7257
7282
  });
7258
7283
  return elements.map(
7259
- ({ endRaw, durationRaw, intrinsicDuration, playbackStartRaw, mediaStartRaw, ...element }) => ({
7284
+ ({
7285
+ endRaw,
7286
+ durationRaw,
7287
+ intrinsicDuration,
7288
+ playbackStartRaw,
7289
+ mediaStartRaw,
7290
+ playbackRateRaw,
7291
+ automationRaw,
7292
+ defaultPlaybackRate,
7293
+ ...element
7294
+ }) => ({
7260
7295
  ...element,
7261
7296
  end: parseStrictFiniteTimingNumber(endRaw) ?? 0,
7262
7297
  duration: element.durationInferred && Number.isFinite(intrinsicDuration) && intrinsicDuration > 0 ? intrinsicDuration : parseStrictFiniteTimingNumber(durationRaw) ?? 0,
@@ -7266,7 +7301,14 @@ async function discoverMediaFromBrowser(page) {
7266
7301
  if (name === "data-media-start") return mediaStartRaw;
7267
7302
  return null;
7268
7303
  }
7269
- })
7304
+ }),
7305
+ playbackRate: resolveRateSpec(
7306
+ automationRaw,
7307
+ readPlaybackRate(
7308
+ (name) => name === "data-playback-rate" ? playbackRateRaw : null,
7309
+ defaultPlaybackRate
7310
+ )
7311
+ )
7270
7312
  })
7271
7313
  );
7272
7314
  }
@@ -7686,9 +7728,18 @@ import {
7686
7728
  getCompositionDuration,
7687
7729
  initializeSession as initializeSession4,
7688
7730
  isTransientBrowserError,
7689
- probeBeginFrameLiveness
7731
+ probeBeginFrameLiveness,
7732
+ videoAudioId
7690
7733
  } from "@hyperframes/engine";
7691
- import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7734
+ import {
7735
+ MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
7736
+ fpsToNumber as fpsToNumber7,
7737
+ normalizeRateSpec as normalizeRateSpec3,
7738
+ readMediaStart as readMediaStart2,
7739
+ shiftRateLane as shiftRateLane2,
7740
+ sourceTimeAt as sourceTimeAt3,
7741
+ timeAtSourceTime
7742
+ } from "@hyperframes/core";
7692
7743
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7693
7744
 
7694
7745
  // src/services/render/stages/probeFailures.ts
@@ -7747,16 +7798,28 @@ function authoredMediaStarts(html) {
7747
7798
  }
7748
7799
  return starts;
7749
7800
  }
7750
- function adoptRuntimeMediaStart(composition, el, authored) {
7801
+ function isVideoAudio(audio, videoId) {
7802
+ return audio.type === "video" && audio.id === videoAudioId(videoId);
7803
+ }
7804
+ function adoptRuntimeMediaTiming(composition, el, authored) {
7751
7805
  const shift = el.mediaStart - authored;
7752
- if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7753
7806
  const entries = el.tagName === "video" ? [
7754
7807
  ...composition.videos.filter((video) => video.id === el.id),
7755
- ...composition.audios.filter(
7756
- (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7757
- )
7808
+ ...composition.audios.filter((audio) => isVideoAudio(audio, el.id))
7758
7809
  ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7759
- for (const entry of entries) entry.mediaStart += shift;
7810
+ for (const entry of entries) {
7811
+ if (typeof entry.playbackRate !== "object") {
7812
+ if (typeof el.playbackRate === "object") {
7813
+ const consumed = Math.max(0, entry.mediaStart - authored);
7814
+ const head = timeAtSourceTime(normalizeRateSpec3(entry.playbackRate), consumed);
7815
+ entry.mediaStart += sourceTimeAt3(el.playbackRate, head) - consumed;
7816
+ entry.playbackRate = shiftRateLane2(el.playbackRate, head);
7817
+ } else {
7818
+ entry.playbackRate = el.playbackRate;
7819
+ }
7820
+ }
7821
+ if (el.mediaStart > 0 && Math.abs(shift) > BROWSER_MEDIA_EPSILON) entry.mediaStart += shift;
7822
+ }
7760
7823
  }
7761
7824
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7762
7825
  if (projectedEnd <= 0) return existingEnd;
@@ -8040,7 +8103,7 @@ async function runProbeStage(input) {
8040
8103
  sourceChanged,
8041
8104
  el.durationInferred
8042
8105
  );
8043
- adoptRuntimeMediaStart(
8106
+ adoptRuntimeMediaTiming(
8044
8107
  composition,
8045
8108
  el,
8046
8109
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8063,6 +8126,7 @@ async function runProbeStage(input) {
8063
8126
  start: el.start,
8064
8127
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8065
8128
  mediaStart: el.mediaStart,
8129
+ playbackRate: el.playbackRate,
8066
8130
  loop: el.loop,
8067
8131
  hasAudio: el.hasAudio && !el.muted
8068
8132
  });
@@ -8087,7 +8151,7 @@ async function runProbeStage(input) {
8087
8151
  sourceChanged,
8088
8152
  el.durationInferred
8089
8153
  );
8090
- adoptRuntimeMediaStart(
8154
+ adoptRuntimeMediaTiming(
8091
8155
  composition,
8092
8156
  el,
8093
8157
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8105,6 +8169,7 @@ async function runProbeStage(input) {
8105
8169
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8106
8170
  mediaStart: el.mediaStart,
8107
8171
  loop: el.loop,
8172
+ playbackRate: el.playbackRate,
8108
8173
  layer: 0,
8109
8174
  volume: el.volume,
8110
8175
  type: "audio"
@@ -8365,7 +8430,8 @@ import {
8365
8430
  isHttpUrl as isHttpUrl2,
8366
8431
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
8367
8432
  runVideoExtractionWithRetry,
8368
- safeVideoExtractionSourceIdentity
8433
+ safeVideoExtractionSourceIdentity,
8434
+ videoAudioId as videoAudioId2
8369
8435
  } from "@hyperframes/engine";
8370
8436
 
8371
8437
  // src/services/render/extractionFailureMetadata.ts
@@ -8723,7 +8789,7 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8723
8789
  const video = composition.videos.find((v) => v.id === ext.videoId);
8724
8790
  if (!video || !video.hasAudio || video.hidden || existingAudioSrcs.has(video.src)) continue;
8725
8791
  composition.audios.push({
8726
- id: `${video.id}-audio`,
8792
+ id: videoAudioId2(video.id),
8727
8793
  src: video.src,
8728
8794
  start: video.start,
8729
8795
  end: video.end,
@@ -11749,13 +11815,36 @@ function colorGradingValueHasLut(rawAttributeValue) {
11749
11815
  return false;
11750
11816
  }
11751
11817
  }
11752
- async function resolveCompositionElementCount(probeSession, html) {
11818
+ var PROBE_READ_TIMEOUT_MS = 5e3;
11819
+ async function evaluateWithinBound(page, fn, what, log) {
11820
+ let timer;
11821
+ const timedOut = new Promise((_, reject) => {
11822
+ timer = setTimeout(
11823
+ () => reject(new Error(`no answer within ${PROBE_READ_TIMEOUT_MS} ms (page busy)`)),
11824
+ PROBE_READ_TIMEOUT_MS
11825
+ );
11826
+ });
11827
+ try {
11828
+ return await Promise.race([page.evaluate(fn), timedOut]);
11829
+ } catch (err) {
11830
+ log.warn(`[Render] live ${what} read skipped; using the static answer`, {
11831
+ error: err instanceof Error ? err.message : String(err)
11832
+ });
11833
+ throw err;
11834
+ } finally {
11835
+ clearTimeout(timer);
11836
+ }
11837
+ }
11838
+ async function resolveCompositionElementCount(probeSession, html, log = defaultLogger) {
11753
11839
  if (probeSession?.isInitialized) {
11754
11840
  try {
11755
- const liveCount = await probeSession.page.evaluate(
11841
+ const liveCount = await evaluateWithinBound(
11842
+ probeSession.page,
11756
11843
  // Live HTMLCollection length — avoids materializing a static NodeList
11757
11844
  // on the large-DOM comps this gate exists to catch (review nit).
11758
- () => document.getElementsByTagName("*").length
11845
+ () => document.getElementsByTagName("*").length,
11846
+ "element count",
11847
+ log
11759
11848
  );
11760
11849
  if (typeof liveCount === "number" && Number.isFinite(liveCount)) {
11761
11850
  return { count: liveCount, source: "live" };
@@ -11843,11 +11932,11 @@ function probeAdaptersUsed() {
11843
11932
  }
11844
11933
  return used;
11845
11934
  }
11846
- async function resolveAdaptersUsed(probeSession, html) {
11935
+ async function resolveAdaptersUsed(probeSession, html, log = defaultLogger) {
11847
11936
  const staticAdapters = detectAdaptersStatic(html);
11848
11937
  if (!probeSession?.isInitialized) return staticAdapters;
11849
11938
  try {
11850
- const live = await probeSession.page.evaluate(probeAdaptersUsed);
11939
+ const live = await evaluateWithinBound(probeSession.page, probeAdaptersUsed, "adapters", log);
11851
11940
  if (!Array.isArray(live)) return staticAdapters;
11852
11941
  const detected = /* @__PURE__ */ new Set([...staticAdapters, ...live]);
11853
11942
  return KNOWN_RUNTIME_ADAPTERS.filter((name) => detected.has(name));
@@ -12105,7 +12194,10 @@ async function executeRenderPipeline(input) {
12105
12194
  );
12106
12195
  const artifactTransaction = new ArtifactTransaction(
12107
12196
  outputPath,
12108
- isPngSequence || isHls ? "directory" : "file"
12197
+ isPngSequence || isHls ? "directory" : "file",
12198
+ void 0,
12199
+ void 0,
12200
+ log
12109
12201
  );
12110
12202
  const stagedOutputPath = artifactTransaction.stagingPath;
12111
12203
  const needsAlpha = outputNeedsAlpha(outputFormat);
@@ -12604,8 +12696,8 @@ async function executeRenderPipeline(input) {
12604
12696
  vfxHostCount,
12605
12697
  vfxCapture,
12606
12698
  vfxTypes
12607
- } = await resolveCompositionElementCount(probeSession, compiled.html);
12608
- const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html);
12699
+ } = await resolveCompositionElementCount(probeSession, compiled.html, log);
12700
+ const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html, log);
12609
12701
  const deShortBandEnabled = deShortBandMaxElements > 0;
12610
12702
  const deShortBandOpen = deShortBandEnabled && deShortBandMinFrames > 0 && compositionElementCountSource === "live" && compositionElementCount <= deShortBandMaxElements;
12611
12703
  const deShortBandRoute = process.env.HF_DE_SHORT_BAND_ROUTE === "true";
@@ -1583,8 +1583,11 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
1583
1583
  `Render artifact is truncated: expected ${expectedSeconds.toFixed(3)}s, probed ${probedSeconds.toFixed(3)}s (deficit ${deficit.toFixed(3)}s exceeds tolerance ${toleranceSeconds.toFixed(3)}s). Artifact: ${stagingPath}`
1584
1584
  );
1585
1585
  }
1586
+ function hasUsableFrameCount(frames) {
1587
+ return frames !== void 0 && Number.isFinite(frames) && frames > 0;
1588
+ }
1586
1589
  function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
1587
- if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
1590
+ if (!hasUsableFrameCount(probedFrames)) {
1588
1591
  return;
1589
1592
  }
1590
1593
  const shortfall = expectedFrames - probedFrames;
@@ -1604,9 +1607,10 @@ function assertSizeMatches(expected, probed, stagingPath) {
1604
1607
  );
1605
1608
  }
1606
1609
  var ArtifactTransaction = class {
1607
- constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
1610
+ constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe, logger = defaultLogger) {
1608
1611
  this.kind = kind;
1609
1612
  this.fileSystem = fileSystem;
1613
+ this.logger = logger;
1610
1614
  this.destinationPath = resolve4(destinationPath);
1611
1615
  this.transactionDirectory = createSiblingTransactionDirectory(this.destinationPath);
1612
1616
  this.stagingPath = join5(this.transactionDirectory, basename3(this.destinationPath));
@@ -1689,6 +1693,19 @@ var ArtifactTransaction = class {
1689
1693
  probePath
1690
1694
  );
1691
1695
  if (expected.expectedFrames !== void 0) {
1696
+ if (!hasUsableFrameCount(probed.frames)) {
1697
+ try {
1698
+ this.logger.debug(
1699
+ "[Render] Frame-count validation skipped: no usable frame count was reported.",
1700
+ {
1701
+ event: "artifact_frame_count_unavailable",
1702
+ artifact: this.destinationPath,
1703
+ expectedFrames: expected.expectedFrames
1704
+ }
1705
+ );
1706
+ } catch {
1707
+ }
1708
+ }
1692
1709
  assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
1693
1710
  }
1694
1711
  assertSizeMatches(expected, probed, probePath);
@@ -5276,6 +5293,8 @@ import {
5276
5293
  rewriteInlineStyleAssetUrls
5277
5294
  } from "@hyperframes/core";
5278
5295
  import { MAX_AUDIO_GAIN } from "@hyperframes/core/audio-gain";
5296
+ import { readPlaybackRate } from "@hyperframes/parsers/media-duration";
5297
+ import { resolveRateSpec } from "@hyperframes/core/speed-ramp";
5279
5298
  import { gsapCdnDist } from "@hyperframes/core/gsap-cdn";
5280
5299
  import {
5281
5300
  assignBundledRuntimeCompositionIds,
@@ -7251,6 +7270,9 @@ async function discoverMediaFromBrowser(page) {
7251
7270
  const intrinsicDuration = isImage ? 0 : htmlEl.duration;
7252
7271
  const playbackStartRaw = htmlEl.getAttribute("data-playback-start");
7253
7272
  const mediaStartRaw = htmlEl.getAttribute("data-media-start");
7273
+ const playbackRateRaw = htmlEl.getAttribute("data-playback-rate");
7274
+ const automationRaw = htmlEl.getAttribute("data-automation");
7275
+ const defaultPlaybackRate = isImage ? 1 : htmlEl.defaultPlaybackRate;
7254
7276
  const loop = htmlEl.hasAttribute("loop");
7255
7277
  const hasAudio = htmlEl.getAttribute("data-has-audio") === "true";
7256
7278
  const volume = parseFloat(htmlEl.getAttribute("data-volume") || "1");
@@ -7266,6 +7288,9 @@ async function discoverMediaFromBrowser(page) {
7266
7288
  durationInferred,
7267
7289
  playbackStartRaw,
7268
7290
  mediaStartRaw,
7291
+ playbackRateRaw,
7292
+ automationRaw,
7293
+ defaultPlaybackRate,
7269
7294
  loop,
7270
7295
  hasAudio,
7271
7296
  volume,
@@ -7275,7 +7300,17 @@ async function discoverMediaFromBrowser(page) {
7275
7300
  return results;
7276
7301
  });
7277
7302
  return elements.map(
7278
- ({ endRaw, durationRaw, intrinsicDuration, playbackStartRaw, mediaStartRaw, ...element }) => ({
7303
+ ({
7304
+ endRaw,
7305
+ durationRaw,
7306
+ intrinsicDuration,
7307
+ playbackStartRaw,
7308
+ mediaStartRaw,
7309
+ playbackRateRaw,
7310
+ automationRaw,
7311
+ defaultPlaybackRate,
7312
+ ...element
7313
+ }) => ({
7279
7314
  ...element,
7280
7315
  end: parseStrictFiniteTimingNumber(endRaw) ?? 0,
7281
7316
  duration: element.durationInferred && Number.isFinite(intrinsicDuration) && intrinsicDuration > 0 ? intrinsicDuration : parseStrictFiniteTimingNumber(durationRaw) ?? 0,
@@ -7285,7 +7320,14 @@ async function discoverMediaFromBrowser(page) {
7285
7320
  if (name === "data-media-start") return mediaStartRaw;
7286
7321
  return null;
7287
7322
  }
7288
- })
7323
+ }),
7324
+ playbackRate: resolveRateSpec(
7325
+ automationRaw,
7326
+ readPlaybackRate(
7327
+ (name) => name === "data-playback-rate" ? playbackRateRaw : null,
7328
+ defaultPlaybackRate
7329
+ )
7330
+ )
7289
7331
  })
7290
7332
  );
7291
7333
  }
@@ -7705,9 +7747,18 @@ import {
7705
7747
  getCompositionDuration,
7706
7748
  initializeSession as initializeSession4,
7707
7749
  isTransientBrowserError,
7708
- probeBeginFrameLiveness
7750
+ probeBeginFrameLiveness,
7751
+ videoAudioId
7709
7752
  } from "@hyperframes/engine";
7710
- import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7753
+ import {
7754
+ MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
7755
+ fpsToNumber as fpsToNumber7,
7756
+ normalizeRateSpec as normalizeRateSpec3,
7757
+ readMediaStart as readMediaStart2,
7758
+ shiftRateLane as shiftRateLane2,
7759
+ sourceTimeAt as sourceTimeAt3,
7760
+ timeAtSourceTime
7761
+ } from "@hyperframes/core";
7711
7762
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7712
7763
 
7713
7764
  // src/services/render/stages/probeFailures.ts
@@ -7766,16 +7817,28 @@ function authoredMediaStarts(html) {
7766
7817
  }
7767
7818
  return starts;
7768
7819
  }
7769
- function adoptRuntimeMediaStart(composition, el, authored) {
7820
+ function isVideoAudio(audio, videoId) {
7821
+ return audio.type === "video" && audio.id === videoAudioId(videoId);
7822
+ }
7823
+ function adoptRuntimeMediaTiming(composition, el, authored) {
7770
7824
  const shift = el.mediaStart - authored;
7771
- if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7772
7825
  const entries = el.tagName === "video" ? [
7773
7826
  ...composition.videos.filter((video) => video.id === el.id),
7774
- ...composition.audios.filter(
7775
- (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7776
- )
7827
+ ...composition.audios.filter((audio) => isVideoAudio(audio, el.id))
7777
7828
  ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7778
- for (const entry of entries) entry.mediaStart += shift;
7829
+ for (const entry of entries) {
7830
+ if (typeof entry.playbackRate !== "object") {
7831
+ if (typeof el.playbackRate === "object") {
7832
+ const consumed = Math.max(0, entry.mediaStart - authored);
7833
+ const head = timeAtSourceTime(normalizeRateSpec3(entry.playbackRate), consumed);
7834
+ entry.mediaStart += sourceTimeAt3(el.playbackRate, head) - consumed;
7835
+ entry.playbackRate = shiftRateLane2(el.playbackRate, head);
7836
+ } else {
7837
+ entry.playbackRate = el.playbackRate;
7838
+ }
7839
+ }
7840
+ if (el.mediaStart > 0 && Math.abs(shift) > BROWSER_MEDIA_EPSILON) entry.mediaStart += shift;
7841
+ }
7779
7842
  }
7780
7843
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7781
7844
  if (projectedEnd <= 0) return existingEnd;
@@ -8059,7 +8122,7 @@ async function runProbeStage(input) {
8059
8122
  sourceChanged,
8060
8123
  el.durationInferred
8061
8124
  );
8062
- adoptRuntimeMediaStart(
8125
+ adoptRuntimeMediaTiming(
8063
8126
  composition,
8064
8127
  el,
8065
8128
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8082,6 +8145,7 @@ async function runProbeStage(input) {
8082
8145
  start: el.start,
8083
8146
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8084
8147
  mediaStart: el.mediaStart,
8148
+ playbackRate: el.playbackRate,
8085
8149
  loop: el.loop,
8086
8150
  hasAudio: el.hasAudio && !el.muted
8087
8151
  });
@@ -8106,7 +8170,7 @@ async function runProbeStage(input) {
8106
8170
  sourceChanged,
8107
8171
  el.durationInferred
8108
8172
  );
8109
- adoptRuntimeMediaStart(
8173
+ adoptRuntimeMediaTiming(
8110
8174
  composition,
8111
8175
  el,
8112
8176
  authoredStarts.get(el.id) ?? existing.mediaStart
@@ -8124,6 +8188,7 @@ async function runProbeStage(input) {
8124
8188
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
8125
8189
  mediaStart: el.mediaStart,
8126
8190
  loop: el.loop,
8191
+ playbackRate: el.playbackRate,
8127
8192
  layer: 0,
8128
8193
  volume: el.volume,
8129
8194
  type: "audio"
@@ -8353,7 +8418,8 @@ import {
8353
8418
  isHttpUrl as isHttpUrl2,
8354
8419
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
8355
8420
  runVideoExtractionWithRetry,
8356
- safeVideoExtractionSourceIdentity
8421
+ safeVideoExtractionSourceIdentity,
8422
+ videoAudioId as videoAudioId2
8357
8423
  } from "@hyperframes/engine";
8358
8424
 
8359
8425
  // src/services/render/extractionFailureMetadata.ts
@@ -8706,7 +8772,7 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8706
8772
  const video = composition.videos.find((v) => v.id === ext.videoId);
8707
8773
  if (!video || !video.hasAudio || video.hidden || existingAudioSrcs.has(video.src)) continue;
8708
8774
  composition.audios.push({
8709
- id: `${video.id}-audio`,
8775
+ id: videoAudioId2(video.id),
8710
8776
  src: video.src,
8711
8777
  start: video.start,
8712
8778
  end: video.end,
@@ -11732,13 +11798,36 @@ function colorGradingValueHasLut(rawAttributeValue) {
11732
11798
  return false;
11733
11799
  }
11734
11800
  }
11735
- async function resolveCompositionElementCount(probeSession, html) {
11801
+ var PROBE_READ_TIMEOUT_MS = 5e3;
11802
+ async function evaluateWithinBound(page, fn, what, log) {
11803
+ let timer;
11804
+ const timedOut = new Promise((_, reject) => {
11805
+ timer = setTimeout(
11806
+ () => reject(new Error(`no answer within ${PROBE_READ_TIMEOUT_MS} ms (page busy)`)),
11807
+ PROBE_READ_TIMEOUT_MS
11808
+ );
11809
+ });
11810
+ try {
11811
+ return await Promise.race([page.evaluate(fn), timedOut]);
11812
+ } catch (err) {
11813
+ log.warn(`[Render] live ${what} read skipped; using the static answer`, {
11814
+ error: err instanceof Error ? err.message : String(err)
11815
+ });
11816
+ throw err;
11817
+ } finally {
11818
+ clearTimeout(timer);
11819
+ }
11820
+ }
11821
+ async function resolveCompositionElementCount(probeSession, html, log = defaultLogger) {
11736
11822
  if (probeSession?.isInitialized) {
11737
11823
  try {
11738
- const liveCount = await probeSession.page.evaluate(
11824
+ const liveCount = await evaluateWithinBound(
11825
+ probeSession.page,
11739
11826
  // Live HTMLCollection length — avoids materializing a static NodeList
11740
11827
  // on the large-DOM comps this gate exists to catch (review nit).
11741
- () => document.getElementsByTagName("*").length
11828
+ () => document.getElementsByTagName("*").length,
11829
+ "element count",
11830
+ log
11742
11831
  );
11743
11832
  if (typeof liveCount === "number" && Number.isFinite(liveCount)) {
11744
11833
  return { count: liveCount, source: "live" };
@@ -11826,11 +11915,11 @@ function probeAdaptersUsed() {
11826
11915
  }
11827
11916
  return used;
11828
11917
  }
11829
- async function resolveAdaptersUsed(probeSession, html) {
11918
+ async function resolveAdaptersUsed(probeSession, html, log = defaultLogger) {
11830
11919
  const staticAdapters = detectAdaptersStatic(html);
11831
11920
  if (!probeSession?.isInitialized) return staticAdapters;
11832
11921
  try {
11833
- const live = await probeSession.page.evaluate(probeAdaptersUsed);
11922
+ const live = await evaluateWithinBound(probeSession.page, probeAdaptersUsed, "adapters", log);
11834
11923
  if (!Array.isArray(live)) return staticAdapters;
11835
11924
  const detected = /* @__PURE__ */ new Set([...staticAdapters, ...live]);
11836
11925
  return KNOWN_RUNTIME_ADAPTERS.filter((name) => detected.has(name));
@@ -12088,7 +12177,10 @@ async function executeRenderPipeline(input) {
12088
12177
  );
12089
12178
  const artifactTransaction = new ArtifactTransaction(
12090
12179
  outputPath,
12091
- isPngSequence || isHls ? "directory" : "file"
12180
+ isPngSequence || isHls ? "directory" : "file",
12181
+ void 0,
12182
+ void 0,
12183
+ log
12092
12184
  );
12093
12185
  const stagedOutputPath = artifactTransaction.stagingPath;
12094
12186
  const needsAlpha = outputNeedsAlpha(outputFormat);
@@ -12587,8 +12679,8 @@ async function executeRenderPipeline(input) {
12587
12679
  vfxHostCount,
12588
12680
  vfxCapture,
12589
12681
  vfxTypes
12590
- } = await resolveCompositionElementCount(probeSession, compiled.html);
12591
- const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html);
12682
+ } = await resolveCompositionElementCount(probeSession, compiled.html, log);
12683
+ const adaptersUsed = await resolveAdaptersUsed(probeSession, compiled.html, log);
12592
12684
  const deShortBandEnabled = deShortBandMaxElements > 0;
12593
12685
  const deShortBandOpen = deShortBandEnabled && deShortBandMinFrames > 0 && compositionElementCountSource === "live" && compositionElementCount <= deShortBandMaxElements;
12594
12686
  const deShortBandRoute = process.env.HF_DE_SHORT_BAND_ROUTE === "true";
@@ -1,4 +1,4 @@
1
- import { type ResolvedDuration, type UnresolvedElement } from "@hyperframes/core";
1
+ import { type RateSpec, type ResolvedDuration, type UnresolvedElement } from "@hyperframes/core";
2
2
  import { type VideoElement, type ImageElement, type AudioElement, type AudioVolumeKeyframe } from "@hyperframes/engine";
3
3
  import type { Page } from "puppeteer-core";
4
4
  import { type ProducerLogger } from "../logger.js";
@@ -216,6 +216,7 @@ export interface BrowserMediaElement {
216
216
  /** True when compilation inferred duration from the fallback source. */
217
217
  durationInferred: boolean;
218
218
  mediaStart: number;
219
+ playbackRate: RateSpec;
219
220
  loop: boolean;
220
221
  hasAudio: boolean;
221
222
  volume: number;
@@ -1,3 +1,4 @@
1
+ import { type ProducerLogger } from "../../logger.js";
1
2
  import type { RenderOutputFormat } from "./renderFormat.js";
2
3
  export type ArtifactKind = "file" | "directory";
3
4
  export interface ArtifactTransactionFileSystem {
@@ -113,13 +114,14 @@ export declare function buildArtifactExpectation(input: {
113
114
  export declare class ArtifactTransaction {
114
115
  private readonly kind;
115
116
  private readonly fileSystem;
117
+ private readonly logger;
116
118
  readonly destinationPath: string;
117
119
  readonly stagingPath: string;
118
120
  private readonly transactionDirectory;
119
121
  private readonly backupPath;
120
122
  private state;
121
123
  private readonly durationProbe;
122
- constructor(destinationPath: string, kind: ArtifactKind, fileSystem?: ArtifactTransactionFileSystem, durationProbe?: ArtifactDurationProbe);
124
+ constructor(destinationPath: string, kind: ArtifactKind, fileSystem?: ArtifactTransactionFileSystem, durationProbe?: ArtifactDurationProbe, logger?: Pick<ProducerLogger, "debug">);
123
125
  validate(expected?: ArtifactValidationExpectation): Promise<void>;
124
126
  private assertReadableNonEmptyDirectory;
125
127
  /**
@@ -706,6 +706,8 @@ export interface ElementTagScan {
706
706
  * live count and uses this only when no such session exists.
707
707
  */
708
708
  export declare function scanElementTags(html: string): ElementTagScan;
709
+ /** Longest a live probe-page read may hold the render; a busy page otherwise waits out the CDP timeout. */
710
+ export declare const PROBE_READ_TIMEOUT_MS = 5000;
709
711
  /**
710
712
  * Element count for the short-comp band's gate, WITH PROVENANCE.
711
713
  *
@@ -728,7 +730,7 @@ export declare function scanElementTags(html: string): ElementTagScan;
728
730
  * the fleet distribution recorded by the baseline release must be measured
729
731
  * by the same resolver that later gates routing, or the baseline is invalid.
730
732
  */
731
- export declare function resolveCompositionElementCount(probeSession: Pick<CaptureSession, "isInitialized" | "page"> | null, html: string): Promise<{
733
+ export declare function resolveCompositionElementCount(probeSession: Pick<CaptureSession, "isInitialized" | "page"> | null, html: string, log?: Pick<ProducerLogger, "warn">): Promise<{
732
734
  count: number;
733
735
  source: "live" | "static";
734
736
  byTag?: Readonly<Record<string, number>>;
@@ -761,7 +763,7 @@ export declare function detectAdaptersStatic(html: string): RuntimeAdapterName[]
761
763
  * Runtime adapters a composition exercises. Unlike `resolveCompositionElementCount`
762
764
  * (one source of truth, for gating), this is observational: unions live + static every render.
763
765
  */
764
- export declare function resolveAdaptersUsed(probeSession: Pick<CaptureSession, "isInitialized" | "page"> | null, html: string): Promise<readonly RuntimeAdapterName[]>;
766
+ export declare function resolveAdaptersUsed(probeSession: Pick<CaptureSession, "isInitialized" | "page"> | null, html: string, log?: Pick<ProducerLogger, "warn">): Promise<readonly RuntimeAdapterName[]>;
765
767
  /**
766
768
  * Max-merge init telemetry across per-worker capture perf summaries — the
767
769
  * success-path channel for PARALLEL renders, whose worker console buffers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.193",
3
+ "version": "0.8.194",
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/engine": "0.8.193",
39
- "@hyperframes/core": "0.8.193",
40
- "@hyperframes/parsers": "0.8.193",
41
- "@hyperframes/lint": "0.8.193",
42
- "@hyperframes/studio-server": "0.8.193"
38
+ "@hyperframes/core": "0.8.194",
39
+ "@hyperframes/engine": "0.8.194",
40
+ "@hyperframes/parsers": "0.8.194",
41
+ "@hyperframes/lint": "0.8.194",
42
+ "@hyperframes/studio-server": "0.8.194"
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.193"
51
+ "@hyperframes/aws-lambda": "0.8.194"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"