@hyperframes/producer 0.8.149 → 0.8.151

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.
@@ -1417,9 +1417,18 @@ import {
1417
1417
 
1418
1418
  // src/services/renderMediaCollector.ts
1419
1419
  import { parseHTML as parseHTML2 } from "linkedom";
1420
- import { MEDIA_RENDER_ID_ATTR, resolveAuthoredTimingWindow } from "@hyperframes/core";
1420
+ import {
1421
+ MEDIA_RENDER_ID_ATTR,
1422
+ normalizeRateSpec as normalizeRateSpec2,
1423
+ resolveAuthoredTimingWindow,
1424
+ shiftRateLane,
1425
+ sourceTimeAt as sourceTimeAt2
1426
+ } from "@hyperframes/core";
1421
1427
  import {
1422
1428
  MEDIA_START_BASIS_ATTR,
1429
+ compositionOriginSeconds,
1430
+ cutToHostSlots,
1431
+ hostInPointSeconds,
1423
1432
  readMediaStartBasis,
1424
1433
  resolveAbsoluteMediaStartSeconds
1425
1434
  } from "@hyperframes/core/media-timing";
@@ -1429,7 +1438,6 @@ import {
1429
1438
  parseAudioElements,
1430
1439
  resolveReferencedStart
1431
1440
  } from "@hyperframes/engine";
1432
- var COMPOSITION_HOST_ATTR = "data-composition-file";
1433
1441
  function resolveHostEnd(host, hostStart) {
1434
1442
  return resolveAuthoredTimingWindow({
1435
1443
  start: hostStart,
@@ -1437,39 +1445,52 @@ function resolveHostEnd(host, hostStart) {
1437
1445
  end: host.getAttribute("data-end")
1438
1446
  })?.end ?? null;
1439
1447
  }
1440
- var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local" };
1441
- function resolveHostWindow(element, document2, startCache, visiting) {
1448
+ var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local", slots: [] };
1449
+ var COMPOSITION_HOST_SELECTOR = "[data-composition-id], [data-composition-src], [data-composition-file]";
1450
+ function enclosingHosts(element) {
1442
1451
  const hosts = [];
1443
1452
  for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
1444
- if (ancestor.hasAttribute(COMPOSITION_HOST_ATTR)) hosts.push(ancestor);
1453
+ if (ancestor.matches(COMPOSITION_HOST_SELECTOR)) {
1454
+ hosts.unshift(ancestor);
1455
+ }
1445
1456
  }
1457
+ return hosts;
1458
+ }
1459
+ function resolveHostWindow(element, document2, startCache, visiting) {
1460
+ const hosts = enclosingHosts(element);
1446
1461
  if (hosts.length === 0) return ROOT_WINDOW;
1447
1462
  let offset = 0;
1448
1463
  let limit = Infinity;
1449
- for (const host of hosts.reverse()) {
1464
+ const slots = [];
1465
+ for (const [index, host] of hosts.entries()) {
1450
1466
  const hostStart = resolveReferencedStart(document2, host, startCache, visiting);
1451
1467
  const hostEnd = resolveHostEnd(host, hostStart);
1452
- if (hostEnd != null) limit = Math.min(limit, offset + hostEnd);
1453
- offset += hostStart;
1468
+ if (hostEnd != null && !isRootComposition(host, index)) {
1469
+ limit = Math.min(limit, offset + hostEnd);
1470
+ }
1471
+ if (hostInPointSeconds(host) > 0) {
1472
+ slots.push({ start: offset + hostStart, end: offset + (hostEnd ?? Infinity) });
1473
+ }
1474
+ offset += compositionOriginSeconds(hostStart, host);
1454
1475
  }
1476
+ return { offset, limit, basis: mediaStartBasis(element), slots };
1477
+ }
1478
+ var isRootComposition = (host, depth) => depth === 0 && !host.matches("[data-composition-src], [data-composition-file]");
1479
+ function mediaStartBasis(element) {
1455
1480
  const tag = element.tagName.toLowerCase();
1456
- const basis = tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
1457
- return {
1458
- offset,
1459
- limit,
1460
- basis
1461
- };
1481
+ return tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
1462
1482
  }
1463
1483
  function collectHostWindows(html) {
1464
1484
  const { document: document2 } = parseHTML2(html);
1465
1485
  const windows = /* @__PURE__ */ new Map();
1466
1486
  const startCache = /* @__PURE__ */ new Map();
1467
1487
  const visiting = /* @__PURE__ */ new Set();
1468
- for (const element of document2.querySelectorAll(`[${MEDIA_RENDER_ID_ATTR}]`)) {
1488
+ for (const element of document2.querySelectorAll("video, audio, img[src]")) {
1469
1489
  const renderId = element.getAttribute(MEDIA_RENDER_ID_ATTR);
1470
- if (!renderId) continue;
1490
+ const id = renderId || element.getAttribute("id");
1491
+ if (!id || !renderId && windows.has(id)) continue;
1471
1492
  windows.set(
1472
- renderId,
1493
+ id,
1473
1494
  resolveHostWindow(element, document2, startCache, visiting)
1474
1495
  );
1475
1496
  }
@@ -1487,34 +1508,59 @@ function toAbsoluteWindow(start, end, window2) {
1487
1508
  hostStart: window2.offset,
1488
1509
  basis: window2.basis
1489
1510
  });
1490
- return { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
1511
+ const absolute = { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
1512
+ const played = cutToHostSlots(absolute, window2.slots);
1513
+ if (played.end === played.start && absolute.end > absolute.start) return null;
1514
+ return { start: absolute.start, end: played.end, played };
1515
+ }
1516
+ function startAtPlayed(clip, played) {
1517
+ const head = played.start - clip.start;
1518
+ if (head <= 0) return clip;
1519
+ if ("loop" in clip && clip.loop) return { ...clip, start: played.start };
1520
+ const rate = normalizeRateSpec2(clip.playbackRate);
1521
+ return {
1522
+ ...clip,
1523
+ start: played.start,
1524
+ mediaStart: clip.mediaStart + sourceTimeAt2(rate, head),
1525
+ playbackRate: shiftRateLane(rate, head)
1526
+ };
1491
1527
  }
1492
1528
  function collectRenderMedia(html) {
1493
1529
  const windows = collectHostWindows(html);
1494
1530
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
1531
+ const droppedMediaIds = /* @__PURE__ */ new Set();
1495
1532
  const videos = [];
1496
1533
  for (const video of parseVideoElements(html)) {
1497
1534
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
1498
- if (absolute) videos.push({ ...video, ...absolute });
1535
+ if (!absolute) {
1536
+ droppedMediaIds.add(video.id);
1537
+ continue;
1538
+ }
1539
+ videos.push(
1540
+ startAtPlayed({ ...video, start: absolute.start, end: absolute.end }, absolute.played)
1541
+ );
1499
1542
  }
1500
1543
  const images = [];
1501
1544
  for (const image of parseImageElements(html)) {
1502
1545
  const absolute = toAbsoluteWindow(image.start, image.end, windowFor(image.id));
1503
- if (absolute) images.push({ ...image, ...absolute });
1546
+ if (absolute) images.push({ ...image, start: absolute.start, end: absolute.end });
1547
+ else droppedMediaIds.add(image.id);
1504
1548
  }
1505
1549
  const audios = [];
1506
1550
  for (const audio of parseAudioElements(html)) {
1507
1551
  const elementId = audio.type === "video" ? audio.id.replace(/-audio$/, "") : audio.id;
1508
- const authoredEnd = audio.end > 0 ? audio.end : Infinity;
1509
- const absolute = toAbsoluteWindow(audio.start, authoredEnd, windowFor(elementId));
1510
- if (!absolute) continue;
1511
- audios.push({
1512
- ...audio,
1513
- start: absolute.start,
1514
- end: Number.isFinite(absolute.end) ? absolute.end : 0
1515
- });
1552
+ const placed = placeAudio(audio, windowFor(elementId));
1553
+ if (placed) audios.push(placed);
1554
+ else droppedMediaIds.add(elementId);
1516
1555
  }
1517
- return { videos, audios, images };
1556
+ return { videos, audios, images, droppedMediaIds };
1557
+ }
1558
+ function placeAudio(audio, window2) {
1559
+ const authoredEnd = audio.end > 0 ? audio.end : Infinity;
1560
+ const absolute = toAbsoluteWindow(audio.start, authoredEnd, window2);
1561
+ if (!absolute) return null;
1562
+ const end = Number.isFinite(absolute.end) ? absolute.end : 0;
1563
+ return startAtPlayed({ ...audio, start: absolute.start, end }, absolute.played);
1518
1564
  }
1519
1565
 
1520
1566
  // src/services/htmlCompiler.ts
@@ -1522,6 +1568,7 @@ import {
1522
1568
  analyzeKeyframeIntervals,
1523
1569
  assertMediaPayload,
1524
1570
  NotMediaPayloadError as NotMediaPayloadError2,
1571
+ parseMasterFxChain,
1525
1572
  probeMediaProfile as probeMediaProfile2
1526
1573
  } from "@hyperframes/engine";
1527
1574
 
@@ -3221,7 +3268,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3221
3268
  for (const [relPath, absPath] of preparedGifAssets) {
3222
3269
  externalAssets.set(relPath, absPath);
3223
3270
  }
3224
- const { videos, audios, images } = collectRenderMedia(html);
3271
+ const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
3272
+ const masterFxChain = parseMasterFxChain(html);
3225
3273
  for (const video of videos) {
3226
3274
  if (isHttpUrl(video.src)) continue;
3227
3275
  const videoPath = resolve5(projectDir, video.src);
@@ -3255,6 +3303,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3255
3303
  videos,
3256
3304
  audios,
3257
3305
  images,
3306
+ droppedMediaIds,
3307
+ ...masterFxChain ? { masterFxChain } : {},
3258
3308
  unresolvedCompositions,
3259
3309
  externalAssets,
3260
3310
  width,
@@ -3687,6 +3737,7 @@ async function runCompileStage(input) {
3687
3737
  videos: compiled.videos,
3688
3738
  audios: compiled.audios,
3689
3739
  images: compiled.images,
3740
+ masterFxChain: compiled.masterFxChain,
3690
3741
  width: compiled.width,
3691
3742
  height: compiled.height
3692
3743
  };
@@ -3749,7 +3800,7 @@ import {
3749
3800
  isTransientBrowserError,
3750
3801
  probeBeginFrameLiveness
3751
3802
  } from "@hyperframes/engine";
3752
- import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
3803
+ import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
3753
3804
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
3754
3805
 
3755
3806
  // src/services/render/stages/probeFailures.ts
@@ -3763,6 +3814,9 @@ function isActionableProbeFailure(line) {
3763
3814
  function resolveCaptureImageFormat(input) {
3764
3815
  return input.needsAlpha || input.motionBlur ? "png" : "jpeg";
3765
3816
  }
3817
+ function resolveCaptureJpegQuality(quality) {
3818
+ return quality === "draft" ? 80 : 100;
3819
+ }
3766
3820
 
3767
3821
  // src/services/render/stages/probeStage.ts
3768
3822
  var FRAME_BOUNDARY_EPSILON = 1e-3;
@@ -3796,6 +3850,26 @@ function hasVariableBoundMedia(html, variables) {
3796
3850
  return Boolean(variableId && Object.hasOwn(variables, variableId));
3797
3851
  });
3798
3852
  }
3853
+ function authoredMediaStarts(html) {
3854
+ const { document: document2 } = parseHTML5(html);
3855
+ const starts = /* @__PURE__ */ new Map();
3856
+ for (const media of document2.querySelectorAll("video, audio")) {
3857
+ const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
3858
+ if (id && !starts.has(id)) starts.set(id, readMediaStart2(media));
3859
+ }
3860
+ return starts;
3861
+ }
3862
+ function adoptRuntimeMediaStart(composition, el, authored) {
3863
+ const shift = el.mediaStart - authored;
3864
+ if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
3865
+ const entries = el.tagName === "video" ? [
3866
+ ...composition.videos.filter((video) => video.id === el.id),
3867
+ ...composition.audios.filter(
3868
+ (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
3869
+ )
3870
+ ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
3871
+ for (const entry of entries) entry.mediaStart += shift;
3872
+ }
3799
3873
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
3800
3874
  if (projectedEnd <= 0) return existingEnd;
3801
3875
  if (sourceChanged && durationInferred) return projectedEnd;
@@ -3898,7 +3972,7 @@ async function runProbeStage(input) {
3898
3972
  height,
3899
3973
  fps: job.config.fps,
3900
3974
  format: captureFormat,
3901
- quality: captureFormat === "png" ? void 0 : 80,
3975
+ quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
3902
3976
  variables: job.config.variables,
3903
3977
  deviceScaleFactor,
3904
3978
  motionBlur,
@@ -4049,6 +4123,7 @@ async function runProbeStage(input) {
4049
4123
  const existingVideoIds = new Set(composition.videos.map((v) => v.id));
4050
4124
  const existingAudioIds = new Set(composition.audios.map((a) => a.id));
4051
4125
  const existingImageIds = new Set(composition.images.map((image) => image.id));
4126
+ const authoredStarts = authoredMediaStarts(compiled.html);
4052
4127
  pruneMutedBrowserMedia(composition, browserMedia, existingAudioIds);
4053
4128
  for (const el of browserMedia) {
4054
4129
  if (!el.src || el.src === "about:blank") continue;
@@ -4076,9 +4151,11 @@ async function runProbeStage(input) {
4076
4151
  sourceChanged,
4077
4152
  el.durationInferred
4078
4153
  );
4079
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
4080
- existing.mediaStart = el.mediaStart;
4081
- }
4154
+ adoptRuntimeMediaStart(
4155
+ composition,
4156
+ el,
4157
+ authoredStarts.get(el.id) ?? existing.mediaStart
4158
+ );
4082
4159
  if (el.hasAudio && !el.muted && !existing.hasAudio) {
4083
4160
  existing.hasAudio = true;
4084
4161
  }
@@ -4086,7 +4163,7 @@ async function runProbeStage(input) {
4086
4163
  existing.loop = true;
4087
4164
  }
4088
4165
  }
4089
- } else {
4166
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
4090
4167
  composition.videos.push({
4091
4168
  id: el.id,
4092
4169
  src,
@@ -4117,14 +4194,16 @@ async function runProbeStage(input) {
4117
4194
  sourceChanged,
4118
4195
  el.durationInferred
4119
4196
  );
4120
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
4121
- existing.mediaStart = el.mediaStart;
4122
- }
4197
+ adoptRuntimeMediaStart(
4198
+ composition,
4199
+ el,
4200
+ authoredStarts.get(el.id) ?? existing.mediaStart
4201
+ );
4123
4202
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
4124
4203
  existing.volume = el.volume;
4125
4204
  }
4126
4205
  }
4127
- } else {
4206
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
4128
4207
  composition.audios.push({
4129
4208
  id: el.id,
4130
4209
  src,
@@ -4152,7 +4231,7 @@ async function runProbeStage(input) {
4152
4231
  existing.end = projectedEnd;
4153
4232
  }
4154
4233
  }
4155
- } else {
4234
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
4156
4235
  composition.images.push({
4157
4236
  id: el.id,
4158
4237
  src,
@@ -4771,6 +4850,7 @@ async function runAudioStage(input) {
4771
4850
  ffmpegProcessTimeout,
4772
4851
  audioGain,
4773
4852
  audios,
4853
+ masterFxChain,
4774
4854
  abortSignal,
4775
4855
  assertNotAborted,
4776
4856
  log
@@ -4790,7 +4870,7 @@ async function runAudioStage(input) {
4790
4870
  audioOutputPath,
4791
4871
  duration,
4792
4872
  abortSignal,
4793
- { ffmpegProcessTimeout, audioGain },
4873
+ { ffmpegProcessTimeout, audioGain, masterFxChain },
4794
4874
  compiledDir
4795
4875
  );
4796
4876
  } catch (err) {
@@ -4876,13 +4956,16 @@ function shouldAllowAdaptiveCaptureRetry(workerCount, _explicitlyConfigured) {
4876
4956
  return workerCount > 1;
4877
4957
  }
4878
4958
  var JPEG_RAW_RGBA_DIVISOR = 8;
4959
+ var HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR = 4;
4879
4960
  var DISK_PROJECTION_SAMPLE_FRAMES = 10;
4880
4961
  function estimateDiskCaptureBytes(totalFrames, captureOptions) {
4881
4962
  const scale = captureOptions.deviceScaleFactor ?? 1;
4882
4963
  const outputWidth = Math.ceil(captureOptions.width * scale);
4883
4964
  const outputHeight = Math.ceil(captureOptions.height * scale);
4884
4965
  const rawBytes = Math.ceil(totalFrames) * outputWidth * outputHeight * 4;
4885
- return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(rawBytes / JPEG_RAW_RGBA_DIVISOR) : rawBytes;
4966
+ return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
4967
+ rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
4968
+ ) : rawBytes;
4886
4969
  }
4887
4970
  function freeDiskBytesAt(path) {
4888
4971
  try {
@@ -7449,6 +7532,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7449
7532
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
7450
7533
  audioGain: cfg.audioGain,
7451
7534
  audios: composition.audios,
7535
+ masterFxChain: composition.masterFxChain,
7452
7536
  abortSignal,
7453
7537
  assertNotAborted
7454
7538
  });
@@ -9082,7 +9166,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9082
9166
  height: plan2.dimensions.height,
9083
9167
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
9084
9168
  format: plan2.dimensions.format === "mp4" ? "jpeg" : "png",
9085
- quality: plan2.dimensions.format === "mp4" ? 80 : void 0,
9169
+ quality: plan2.dimensions.format === "mp4" ? resolveCaptureJpegQuality(job.config.quality) : void 0,
9086
9170
  deviceScaleFactor: encoder.deviceScaleFactor,
9087
9171
  // Re-inject the controller's snapshotted variables so the chunk's
9088
9172
  // first capture sees the same `window.__hfVariables` the in-process
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "675e02ee21f31540e3a6abaec061f86cea92a0178ae67a390700acb4f801dbbc",
4
+ "sha256": "06d59f28d4f4b00706a3efda6cffaeb85060b7595694fd753a5240d18337d11f",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"