@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.
package/dist/index.js CHANGED
@@ -4304,7 +4304,8 @@ function computeSegmentPlanHash(input) {
4304
4304
  input.imageFormat,
4305
4305
  input.useGpu ? "gpu" : "cpu",
4306
4306
  `${input.outputWidth}x${input.outputHeight}`,
4307
- input.motionBlur
4307
+ input.motionBlur,
4308
+ input.captureQuality === void 0 ? "" : String(input.captureQuality)
4308
4309
  ].join(" ");
4309
4310
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4310
4311
  }
@@ -5220,9 +5221,18 @@ import {
5220
5221
 
5221
5222
  // src/services/renderMediaCollector.ts
5222
5223
  import { parseHTML as parseHTML2 } from "linkedom";
5223
- import { MEDIA_RENDER_ID_ATTR, resolveAuthoredTimingWindow } from "@hyperframes/core";
5224
+ import {
5225
+ MEDIA_RENDER_ID_ATTR,
5226
+ normalizeRateSpec as normalizeRateSpec2,
5227
+ resolveAuthoredTimingWindow,
5228
+ shiftRateLane,
5229
+ sourceTimeAt as sourceTimeAt2
5230
+ } from "@hyperframes/core";
5224
5231
  import {
5225
5232
  MEDIA_START_BASIS_ATTR,
5233
+ compositionOriginSeconds,
5234
+ cutToHostSlots,
5235
+ hostInPointSeconds,
5226
5236
  readMediaStartBasis,
5227
5237
  resolveAbsoluteMediaStartSeconds
5228
5238
  } from "@hyperframes/core/media-timing";
@@ -5232,7 +5242,6 @@ import {
5232
5242
  parseAudioElements,
5233
5243
  resolveReferencedStart
5234
5244
  } from "@hyperframes/engine";
5235
- var COMPOSITION_HOST_ATTR = "data-composition-file";
5236
5245
  function resolveHostEnd(host, hostStart) {
5237
5246
  return resolveAuthoredTimingWindow({
5238
5247
  start: hostStart,
@@ -5240,39 +5249,52 @@ function resolveHostEnd(host, hostStart) {
5240
5249
  end: host.getAttribute("data-end")
5241
5250
  })?.end ?? null;
5242
5251
  }
5243
- var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local" };
5244
- function resolveHostWindow(element, document2, startCache, visiting) {
5252
+ var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local", slots: [] };
5253
+ var COMPOSITION_HOST_SELECTOR = "[data-composition-id], [data-composition-src], [data-composition-file]";
5254
+ function enclosingHosts(element) {
5245
5255
  const hosts = [];
5246
5256
  for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
5247
- if (ancestor.hasAttribute(COMPOSITION_HOST_ATTR)) hosts.push(ancestor);
5257
+ if (ancestor.matches(COMPOSITION_HOST_SELECTOR)) {
5258
+ hosts.unshift(ancestor);
5259
+ }
5248
5260
  }
5261
+ return hosts;
5262
+ }
5263
+ function resolveHostWindow(element, document2, startCache, visiting) {
5264
+ const hosts = enclosingHosts(element);
5249
5265
  if (hosts.length === 0) return ROOT_WINDOW;
5250
5266
  let offset = 0;
5251
5267
  let limit = Infinity;
5252
- for (const host of hosts.reverse()) {
5268
+ const slots = [];
5269
+ for (const [index, host] of hosts.entries()) {
5253
5270
  const hostStart = resolveReferencedStart(document2, host, startCache, visiting);
5254
5271
  const hostEnd = resolveHostEnd(host, hostStart);
5255
- if (hostEnd != null) limit = Math.min(limit, offset + hostEnd);
5256
- offset += hostStart;
5272
+ if (hostEnd != null && !isRootComposition(host, index)) {
5273
+ limit = Math.min(limit, offset + hostEnd);
5274
+ }
5275
+ if (hostInPointSeconds(host) > 0) {
5276
+ slots.push({ start: offset + hostStart, end: offset + (hostEnd ?? Infinity) });
5277
+ }
5278
+ offset += compositionOriginSeconds(hostStart, host);
5257
5279
  }
5280
+ return { offset, limit, basis: mediaStartBasis(element), slots };
5281
+ }
5282
+ var isRootComposition = (host, depth) => depth === 0 && !host.matches("[data-composition-src], [data-composition-file]");
5283
+ function mediaStartBasis(element) {
5258
5284
  const tag = element.tagName.toLowerCase();
5259
- const basis = tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
5260
- return {
5261
- offset,
5262
- limit,
5263
- basis
5264
- };
5285
+ return tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
5265
5286
  }
5266
5287
  function collectHostWindows(html) {
5267
5288
  const { document: document2 } = parseHTML2(html);
5268
5289
  const windows = /* @__PURE__ */ new Map();
5269
5290
  const startCache = /* @__PURE__ */ new Map();
5270
5291
  const visiting = /* @__PURE__ */ new Set();
5271
- for (const element of document2.querySelectorAll(`[${MEDIA_RENDER_ID_ATTR}]`)) {
5292
+ for (const element of document2.querySelectorAll("video, audio, img[src]")) {
5272
5293
  const renderId = element.getAttribute(MEDIA_RENDER_ID_ATTR);
5273
- if (!renderId) continue;
5294
+ const id = renderId || element.getAttribute("id");
5295
+ if (!id || !renderId && windows.has(id)) continue;
5274
5296
  windows.set(
5275
- renderId,
5297
+ id,
5276
5298
  resolveHostWindow(element, document2, startCache, visiting)
5277
5299
  );
5278
5300
  }
@@ -5290,34 +5312,59 @@ function toAbsoluteWindow(start, end, window2) {
5290
5312
  hostStart: window2.offset,
5291
5313
  basis: window2.basis
5292
5314
  });
5293
- return { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5315
+ const absolute = { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5316
+ const played = cutToHostSlots(absolute, window2.slots);
5317
+ if (played.end === played.start && absolute.end > absolute.start) return null;
5318
+ return { start: absolute.start, end: played.end, played };
5319
+ }
5320
+ function startAtPlayed(clip, played) {
5321
+ const head = played.start - clip.start;
5322
+ if (head <= 0) return clip;
5323
+ if ("loop" in clip && clip.loop) return { ...clip, start: played.start };
5324
+ const rate = normalizeRateSpec2(clip.playbackRate);
5325
+ return {
5326
+ ...clip,
5327
+ start: played.start,
5328
+ mediaStart: clip.mediaStart + sourceTimeAt2(rate, head),
5329
+ playbackRate: shiftRateLane(rate, head)
5330
+ };
5294
5331
  }
5295
5332
  function collectRenderMedia(html) {
5296
5333
  const windows = collectHostWindows(html);
5297
5334
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5335
+ const droppedMediaIds = /* @__PURE__ */ new Set();
5298
5336
  const videos = [];
5299
5337
  for (const video of parseVideoElements(html)) {
5300
5338
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5301
- if (absolute) videos.push({ ...video, ...absolute });
5339
+ if (!absolute) {
5340
+ droppedMediaIds.add(video.id);
5341
+ continue;
5342
+ }
5343
+ videos.push(
5344
+ startAtPlayed({ ...video, start: absolute.start, end: absolute.end }, absolute.played)
5345
+ );
5302
5346
  }
5303
5347
  const images = [];
5304
5348
  for (const image of parseImageElements(html)) {
5305
5349
  const absolute = toAbsoluteWindow(image.start, image.end, windowFor(image.id));
5306
- if (absolute) images.push({ ...image, ...absolute });
5350
+ if (absolute) images.push({ ...image, start: absolute.start, end: absolute.end });
5351
+ else droppedMediaIds.add(image.id);
5307
5352
  }
5308
5353
  const audios = [];
5309
5354
  for (const audio of parseAudioElements(html)) {
5310
5355
  const elementId = audio.type === "video" ? audio.id.replace(/-audio$/, "") : audio.id;
5311
- const authoredEnd = audio.end > 0 ? audio.end : Infinity;
5312
- const absolute = toAbsoluteWindow(audio.start, authoredEnd, windowFor(elementId));
5313
- if (!absolute) continue;
5314
- audios.push({
5315
- ...audio,
5316
- start: absolute.start,
5317
- end: Number.isFinite(absolute.end) ? absolute.end : 0
5318
- });
5356
+ const placed = placeAudio(audio, windowFor(elementId));
5357
+ if (placed) audios.push(placed);
5358
+ else droppedMediaIds.add(elementId);
5319
5359
  }
5320
- return { videos, audios, images };
5360
+ return { videos, audios, images, droppedMediaIds };
5361
+ }
5362
+ function placeAudio(audio, window2) {
5363
+ const authoredEnd = audio.end > 0 ? audio.end : Infinity;
5364
+ const absolute = toAbsoluteWindow(audio.start, authoredEnd, window2);
5365
+ if (!absolute) return null;
5366
+ const end = Number.isFinite(absolute.end) ? absolute.end : 0;
5367
+ return startAtPlayed({ ...audio, start: absolute.start, end }, absolute.played);
5321
5368
  }
5322
5369
 
5323
5370
  // src/services/htmlCompiler.ts
@@ -5325,6 +5372,7 @@ import {
5325
5372
  analyzeKeyframeIntervals,
5326
5373
  assertMediaPayload,
5327
5374
  NotMediaPayloadError as NotMediaPayloadError2,
5375
+ parseMasterFxChain,
5328
5376
  probeMediaProfile as probeMediaProfile2
5329
5377
  } from "@hyperframes/engine";
5330
5378
 
@@ -7024,7 +7072,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7024
7072
  for (const [relPath, absPath] of preparedGifAssets) {
7025
7073
  externalAssets.set(relPath, absPath);
7026
7074
  }
7027
- const { videos, audios, images } = collectRenderMedia(html);
7075
+ const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7076
+ const masterFxChain = parseMasterFxChain(html);
7028
7077
  for (const video of videos) {
7029
7078
  if (isHttpUrl(video.src)) continue;
7030
7079
  const videoPath = resolve6(projectDir, video.src);
@@ -7058,6 +7107,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7058
7107
  videos,
7059
7108
  audios,
7060
7109
  images,
7110
+ droppedMediaIds,
7111
+ ...masterFxChain ? { masterFxChain } : {},
7061
7112
  unresolvedCompositions,
7062
7113
  externalAssets,
7063
7114
  width,
@@ -7490,6 +7541,7 @@ async function runCompileStage(input) {
7490
7541
  videos: compiled.videos,
7491
7542
  audios: compiled.audios,
7492
7543
  images: compiled.images,
7544
+ masterFxChain: compiled.masterFxChain,
7493
7545
  width: compiled.width,
7494
7546
  height: compiled.height
7495
7547
  };
@@ -7552,7 +7604,7 @@ import {
7552
7604
  isTransientBrowserError,
7553
7605
  probeBeginFrameLiveness
7554
7606
  } from "@hyperframes/engine";
7555
- import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
7607
+ import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7556
7608
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7557
7609
 
7558
7610
  // src/services/render/stages/probeFailures.ts
@@ -7566,6 +7618,9 @@ function isActionableProbeFailure(line) {
7566
7618
  function resolveCaptureImageFormat(input) {
7567
7619
  return input.needsAlpha || input.motionBlur ? "png" : "jpeg";
7568
7620
  }
7621
+ function resolveCaptureJpegQuality(quality) {
7622
+ return quality === "draft" ? 80 : 100;
7623
+ }
7569
7624
 
7570
7625
  // src/services/render/stages/probeStage.ts
7571
7626
  var FRAME_BOUNDARY_EPSILON = 1e-3;
@@ -7599,6 +7654,26 @@ function hasVariableBoundMedia(html, variables) {
7599
7654
  return Boolean(variableId && Object.hasOwn(variables, variableId));
7600
7655
  });
7601
7656
  }
7657
+ function authoredMediaStarts(html) {
7658
+ const { document: document2 } = parseHTML5(html);
7659
+ const starts = /* @__PURE__ */ new Map();
7660
+ for (const media of document2.querySelectorAll("video, audio")) {
7661
+ const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
7662
+ if (id && !starts.has(id)) starts.set(id, readMediaStart2(media));
7663
+ }
7664
+ return starts;
7665
+ }
7666
+ function adoptRuntimeMediaStart(composition, el, authored) {
7667
+ const shift = el.mediaStart - authored;
7668
+ if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7669
+ const entries = el.tagName === "video" ? [
7670
+ ...composition.videos.filter((video) => video.id === el.id),
7671
+ ...composition.audios.filter(
7672
+ (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7673
+ )
7674
+ ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7675
+ for (const entry of entries) entry.mediaStart += shift;
7676
+ }
7602
7677
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7603
7678
  if (projectedEnd <= 0) return existingEnd;
7604
7679
  if (sourceChanged && durationInferred) return projectedEnd;
@@ -7701,7 +7776,7 @@ async function runProbeStage(input) {
7701
7776
  height,
7702
7777
  fps: job.config.fps,
7703
7778
  format: captureFormat,
7704
- quality: captureFormat === "png" ? void 0 : 80,
7779
+ quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
7705
7780
  variables: job.config.variables,
7706
7781
  deviceScaleFactor,
7707
7782
  motionBlur,
@@ -7852,6 +7927,7 @@ async function runProbeStage(input) {
7852
7927
  const existingVideoIds = new Set(composition.videos.map((v) => v.id));
7853
7928
  const existingAudioIds = new Set(composition.audios.map((a) => a.id));
7854
7929
  const existingImageIds = new Set(composition.images.map((image) => image.id));
7930
+ const authoredStarts = authoredMediaStarts(compiled.html);
7855
7931
  pruneMutedBrowserMedia(composition, browserMedia, existingAudioIds);
7856
7932
  for (const el of browserMedia) {
7857
7933
  if (!el.src || el.src === "about:blank") continue;
@@ -7879,9 +7955,11 @@ async function runProbeStage(input) {
7879
7955
  sourceChanged,
7880
7956
  el.durationInferred
7881
7957
  );
7882
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7883
- existing.mediaStart = el.mediaStart;
7884
- }
7958
+ adoptRuntimeMediaStart(
7959
+ composition,
7960
+ el,
7961
+ authoredStarts.get(el.id) ?? existing.mediaStart
7962
+ );
7885
7963
  if (el.hasAudio && !el.muted && !existing.hasAudio) {
7886
7964
  existing.hasAudio = true;
7887
7965
  }
@@ -7889,7 +7967,7 @@ async function runProbeStage(input) {
7889
7967
  existing.loop = true;
7890
7968
  }
7891
7969
  }
7892
- } else {
7970
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7893
7971
  composition.videos.push({
7894
7972
  id: el.id,
7895
7973
  src,
@@ -7920,14 +7998,16 @@ async function runProbeStage(input) {
7920
7998
  sourceChanged,
7921
7999
  el.durationInferred
7922
8000
  );
7923
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7924
- existing.mediaStart = el.mediaStart;
7925
- }
8001
+ adoptRuntimeMediaStart(
8002
+ composition,
8003
+ el,
8004
+ authoredStarts.get(el.id) ?? existing.mediaStart
8005
+ );
7926
8006
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
7927
8007
  existing.volume = el.volume;
7928
8008
  }
7929
8009
  }
7930
- } else {
8010
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7931
8011
  composition.audios.push({
7932
8012
  id: el.id,
7933
8013
  src,
@@ -7955,7 +8035,7 @@ async function runProbeStage(input) {
7955
8035
  existing.end = projectedEnd;
7956
8036
  }
7957
8037
  }
7958
- } else {
8038
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7959
8039
  composition.images.push({
7960
8040
  id: el.id,
7961
8041
  src,
@@ -8574,6 +8654,7 @@ async function runAudioStage(input) {
8574
8654
  ffmpegProcessTimeout,
8575
8655
  audioGain,
8576
8656
  audios,
8657
+ masterFxChain,
8577
8658
  abortSignal,
8578
8659
  assertNotAborted,
8579
8660
  log
@@ -8593,7 +8674,7 @@ async function runAudioStage(input) {
8593
8674
  audioOutputPath,
8594
8675
  duration,
8595
8676
  abortSignal,
8596
- { ffmpegProcessTimeout, audioGain },
8677
+ { ffmpegProcessTimeout, audioGain, masterFxChain },
8597
8678
  compiledDir
8598
8679
  );
8599
8680
  } catch (err) {
@@ -8679,13 +8760,16 @@ function shouldAllowAdaptiveCaptureRetry(workerCount, _explicitlyConfigured) {
8679
8760
  return workerCount > 1;
8680
8761
  }
8681
8762
  var JPEG_RAW_RGBA_DIVISOR = 8;
8763
+ var HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR = 4;
8682
8764
  var DISK_PROJECTION_SAMPLE_FRAMES = 10;
8683
8765
  function estimateDiskCaptureBytes(totalFrames, captureOptions) {
8684
8766
  const scale = captureOptions.deviceScaleFactor ?? 1;
8685
8767
  const outputWidth = Math.ceil(captureOptions.width * scale);
8686
8768
  const outputHeight = Math.ceil(captureOptions.height * scale);
8687
8769
  const rawBytes = Math.ceil(totalFrames) * outputWidth * outputHeight * 4;
8688
- return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(rawBytes / JPEG_RAW_RGBA_DIVISOR) : rawBytes;
8770
+ return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
8771
+ rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
8772
+ ) : rawBytes;
8689
8773
  }
8690
8774
  function freeDiskBytesAt(path) {
8691
8775
  try {
@@ -12062,6 +12146,7 @@ async function executeRenderPipeline(input) {
12062
12146
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
12063
12147
  audioGain: cfg.audioGain,
12064
12148
  audios: composition.audios,
12149
+ masterFxChain: composition.masterFxChain,
12065
12150
  abortSignal: executionSignal,
12066
12151
  assertNotAborted,
12067
12152
  log
@@ -12146,7 +12231,7 @@ async function executeRenderPipeline(input) {
12146
12231
  height,
12147
12232
  fps: job.config.fps,
12148
12233
  format: captureImageFormat,
12149
- quality: captureImageFormat === "png" ? void 0 : job.config.quality === "draft" ? 80 : 95,
12234
+ quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
12150
12235
  variables: job.config.variables,
12151
12236
  deviceScaleFactor,
12152
12237
  motionBlur: job.config.motionBlur,
@@ -12774,7 +12859,8 @@ async function executeRenderPipeline(input) {
12774
12859
  // Device-scaled: the capture buffer, not the CSS composition size.
12775
12860
  outputWidth: captureCompositionWidth ?? width,
12776
12861
  outputHeight: captureCompositionHeight ?? height,
12777
- motionBlur: job.config.motionBlur ? JSON.stringify(job.config.motionBlur) : ""
12862
+ motionBlur: job.config.motionBlur ? JSON.stringify(job.config.motionBlur) : "",
12863
+ captureQuality: captureOptions.quality
12778
12864
  });
12779
12865
  const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
12780
12866
  let completedSegments = /* @__PURE__ */ new Set();
@@ -16012,6 +16098,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16012
16098
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
16013
16099
  audioGain: cfg.audioGain,
16014
16100
  audios: composition.audios,
16101
+ masterFxChain: composition.masterFxChain,
16015
16102
  abortSignal,
16016
16103
  assertNotAborted
16017
16104
  });
@@ -17645,7 +17732,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
17645
17732
  height: plan2.dimensions.height,
17646
17733
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
17647
17734
  format: plan2.dimensions.format === "mp4" ? "jpeg" : "png",
17648
- quality: plan2.dimensions.format === "mp4" ? 80 : void 0,
17735
+ quality: plan2.dimensions.format === "mp4" ? resolveCaptureJpegQuality(job.config.quality) : void 0,
17649
17736
  deviceScaleFactor: encoder.deviceScaleFactor,
17650
17737
  // Re-inject the controller's snapshotted variables so the chunk's
17651
17738
  // first capture sees the same `window.__hfVariables` the in-process