@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.
@@ -4323,7 +4323,8 @@ function computeSegmentPlanHash(input) {
4323
4323
  input.imageFormat,
4324
4324
  input.useGpu ? "gpu" : "cpu",
4325
4325
  `${input.outputWidth}x${input.outputHeight}`,
4326
- input.motionBlur
4326
+ input.motionBlur,
4327
+ input.captureQuality === void 0 ? "" : String(input.captureQuality)
4327
4328
  ].join(" ");
4328
4329
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4329
4330
  }
@@ -5239,9 +5240,18 @@ import {
5239
5240
 
5240
5241
  // src/services/renderMediaCollector.ts
5241
5242
  import { parseHTML as parseHTML2 } from "linkedom";
5242
- import { MEDIA_RENDER_ID_ATTR, resolveAuthoredTimingWindow } from "@hyperframes/core";
5243
+ import {
5244
+ MEDIA_RENDER_ID_ATTR,
5245
+ normalizeRateSpec as normalizeRateSpec2,
5246
+ resolveAuthoredTimingWindow,
5247
+ shiftRateLane,
5248
+ sourceTimeAt as sourceTimeAt2
5249
+ } from "@hyperframes/core";
5243
5250
  import {
5244
5251
  MEDIA_START_BASIS_ATTR,
5252
+ compositionOriginSeconds,
5253
+ cutToHostSlots,
5254
+ hostInPointSeconds,
5245
5255
  readMediaStartBasis,
5246
5256
  resolveAbsoluteMediaStartSeconds
5247
5257
  } from "@hyperframes/core/media-timing";
@@ -5251,7 +5261,6 @@ import {
5251
5261
  parseAudioElements,
5252
5262
  resolveReferencedStart
5253
5263
  } from "@hyperframes/engine";
5254
- var COMPOSITION_HOST_ATTR = "data-composition-file";
5255
5264
  function resolveHostEnd(host, hostStart) {
5256
5265
  return resolveAuthoredTimingWindow({
5257
5266
  start: hostStart,
@@ -5259,39 +5268,52 @@ function resolveHostEnd(host, hostStart) {
5259
5268
  end: host.getAttribute("data-end")
5260
5269
  })?.end ?? null;
5261
5270
  }
5262
- var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local" };
5263
- function resolveHostWindow(element, document2, startCache, visiting) {
5271
+ var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local", slots: [] };
5272
+ var COMPOSITION_HOST_SELECTOR = "[data-composition-id], [data-composition-src], [data-composition-file]";
5273
+ function enclosingHosts(element) {
5264
5274
  const hosts = [];
5265
5275
  for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
5266
- if (ancestor.hasAttribute(COMPOSITION_HOST_ATTR)) hosts.push(ancestor);
5276
+ if (ancestor.matches(COMPOSITION_HOST_SELECTOR)) {
5277
+ hosts.unshift(ancestor);
5278
+ }
5267
5279
  }
5280
+ return hosts;
5281
+ }
5282
+ function resolveHostWindow(element, document2, startCache, visiting) {
5283
+ const hosts = enclosingHosts(element);
5268
5284
  if (hosts.length === 0) return ROOT_WINDOW;
5269
5285
  let offset = 0;
5270
5286
  let limit = Infinity;
5271
- for (const host of hosts.reverse()) {
5287
+ const slots = [];
5288
+ for (const [index, host] of hosts.entries()) {
5272
5289
  const hostStart = resolveReferencedStart(document2, host, startCache, visiting);
5273
5290
  const hostEnd = resolveHostEnd(host, hostStart);
5274
- if (hostEnd != null) limit = Math.min(limit, offset + hostEnd);
5275
- offset += hostStart;
5291
+ if (hostEnd != null && !isRootComposition(host, index)) {
5292
+ limit = Math.min(limit, offset + hostEnd);
5293
+ }
5294
+ if (hostInPointSeconds(host) > 0) {
5295
+ slots.push({ start: offset + hostStart, end: offset + (hostEnd ?? Infinity) });
5296
+ }
5297
+ offset += compositionOriginSeconds(hostStart, host);
5276
5298
  }
5299
+ return { offset, limit, basis: mediaStartBasis(element), slots };
5300
+ }
5301
+ var isRootComposition = (host, depth) => depth === 0 && !host.matches("[data-composition-src], [data-composition-file]");
5302
+ function mediaStartBasis(element) {
5277
5303
  const tag = element.tagName.toLowerCase();
5278
- const basis = tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
5279
- return {
5280
- offset,
5281
- limit,
5282
- basis
5283
- };
5304
+ return tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
5284
5305
  }
5285
5306
  function collectHostWindows(html) {
5286
5307
  const { document: document2 } = parseHTML2(html);
5287
5308
  const windows = /* @__PURE__ */ new Map();
5288
5309
  const startCache = /* @__PURE__ */ new Map();
5289
5310
  const visiting = /* @__PURE__ */ new Set();
5290
- for (const element of document2.querySelectorAll(`[${MEDIA_RENDER_ID_ATTR}]`)) {
5311
+ for (const element of document2.querySelectorAll("video, audio, img[src]")) {
5291
5312
  const renderId = element.getAttribute(MEDIA_RENDER_ID_ATTR);
5292
- if (!renderId) continue;
5313
+ const id = renderId || element.getAttribute("id");
5314
+ if (!id || !renderId && windows.has(id)) continue;
5293
5315
  windows.set(
5294
- renderId,
5316
+ id,
5295
5317
  resolveHostWindow(element, document2, startCache, visiting)
5296
5318
  );
5297
5319
  }
@@ -5309,34 +5331,59 @@ function toAbsoluteWindow(start, end, window2) {
5309
5331
  hostStart: window2.offset,
5310
5332
  basis: window2.basis
5311
5333
  });
5312
- return { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5334
+ const absolute = { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5335
+ const played = cutToHostSlots(absolute, window2.slots);
5336
+ if (played.end === played.start && absolute.end > absolute.start) return null;
5337
+ return { start: absolute.start, end: played.end, played };
5338
+ }
5339
+ function startAtPlayed(clip, played) {
5340
+ const head = played.start - clip.start;
5341
+ if (head <= 0) return clip;
5342
+ if ("loop" in clip && clip.loop) return { ...clip, start: played.start };
5343
+ const rate = normalizeRateSpec2(clip.playbackRate);
5344
+ return {
5345
+ ...clip,
5346
+ start: played.start,
5347
+ mediaStart: clip.mediaStart + sourceTimeAt2(rate, head),
5348
+ playbackRate: shiftRateLane(rate, head)
5349
+ };
5313
5350
  }
5314
5351
  function collectRenderMedia(html) {
5315
5352
  const windows = collectHostWindows(html);
5316
5353
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5354
+ const droppedMediaIds = /* @__PURE__ */ new Set();
5317
5355
  const videos = [];
5318
5356
  for (const video of parseVideoElements(html)) {
5319
5357
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5320
- if (absolute) videos.push({ ...video, ...absolute });
5358
+ if (!absolute) {
5359
+ droppedMediaIds.add(video.id);
5360
+ continue;
5361
+ }
5362
+ videos.push(
5363
+ startAtPlayed({ ...video, start: absolute.start, end: absolute.end }, absolute.played)
5364
+ );
5321
5365
  }
5322
5366
  const images = [];
5323
5367
  for (const image of parseImageElements(html)) {
5324
5368
  const absolute = toAbsoluteWindow(image.start, image.end, windowFor(image.id));
5325
- if (absolute) images.push({ ...image, ...absolute });
5369
+ if (absolute) images.push({ ...image, start: absolute.start, end: absolute.end });
5370
+ else droppedMediaIds.add(image.id);
5326
5371
  }
5327
5372
  const audios = [];
5328
5373
  for (const audio of parseAudioElements(html)) {
5329
5374
  const elementId = audio.type === "video" ? audio.id.replace(/-audio$/, "") : audio.id;
5330
- const authoredEnd = audio.end > 0 ? audio.end : Infinity;
5331
- const absolute = toAbsoluteWindow(audio.start, authoredEnd, windowFor(elementId));
5332
- if (!absolute) continue;
5333
- audios.push({
5334
- ...audio,
5335
- start: absolute.start,
5336
- end: Number.isFinite(absolute.end) ? absolute.end : 0
5337
- });
5375
+ const placed = placeAudio(audio, windowFor(elementId));
5376
+ if (placed) audios.push(placed);
5377
+ else droppedMediaIds.add(elementId);
5338
5378
  }
5339
- return { videos, audios, images };
5379
+ return { videos, audios, images, droppedMediaIds };
5380
+ }
5381
+ function placeAudio(audio, window2) {
5382
+ const authoredEnd = audio.end > 0 ? audio.end : Infinity;
5383
+ const absolute = toAbsoluteWindow(audio.start, authoredEnd, window2);
5384
+ if (!absolute) return null;
5385
+ const end = Number.isFinite(absolute.end) ? absolute.end : 0;
5386
+ return startAtPlayed({ ...audio, start: absolute.start, end }, absolute.played);
5340
5387
  }
5341
5388
 
5342
5389
  // src/services/htmlCompiler.ts
@@ -5344,6 +5391,7 @@ import {
5344
5391
  analyzeKeyframeIntervals,
5345
5392
  assertMediaPayload,
5346
5393
  NotMediaPayloadError as NotMediaPayloadError2,
5394
+ parseMasterFxChain,
5347
5395
  probeMediaProfile as probeMediaProfile2
5348
5396
  } from "@hyperframes/engine";
5349
5397
 
@@ -7043,7 +7091,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7043
7091
  for (const [relPath, absPath] of preparedGifAssets) {
7044
7092
  externalAssets.set(relPath, absPath);
7045
7093
  }
7046
- const { videos, audios, images } = collectRenderMedia(html);
7094
+ const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7095
+ const masterFxChain = parseMasterFxChain(html);
7047
7096
  for (const video of videos) {
7048
7097
  if (isHttpUrl(video.src)) continue;
7049
7098
  const videoPath = resolve6(projectDir, video.src);
@@ -7077,6 +7126,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7077
7126
  videos,
7078
7127
  audios,
7079
7128
  images,
7129
+ droppedMediaIds,
7130
+ ...masterFxChain ? { masterFxChain } : {},
7080
7131
  unresolvedCompositions,
7081
7132
  externalAssets,
7082
7133
  width,
@@ -7509,6 +7560,7 @@ async function runCompileStage(input) {
7509
7560
  videos: compiled.videos,
7510
7561
  audios: compiled.audios,
7511
7562
  images: compiled.images,
7563
+ masterFxChain: compiled.masterFxChain,
7512
7564
  width: compiled.width,
7513
7565
  height: compiled.height
7514
7566
  };
@@ -7571,7 +7623,7 @@ import {
7571
7623
  isTransientBrowserError,
7572
7624
  probeBeginFrameLiveness
7573
7625
  } from "@hyperframes/engine";
7574
- import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
7626
+ import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7575
7627
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7576
7628
 
7577
7629
  // src/services/render/stages/probeFailures.ts
@@ -7585,6 +7637,9 @@ function isActionableProbeFailure(line) {
7585
7637
  function resolveCaptureImageFormat(input) {
7586
7638
  return input.needsAlpha || input.motionBlur ? "png" : "jpeg";
7587
7639
  }
7640
+ function resolveCaptureJpegQuality(quality) {
7641
+ return quality === "draft" ? 80 : 100;
7642
+ }
7588
7643
 
7589
7644
  // src/services/render/stages/probeStage.ts
7590
7645
  var FRAME_BOUNDARY_EPSILON = 1e-3;
@@ -7618,6 +7673,26 @@ function hasVariableBoundMedia(html, variables) {
7618
7673
  return Boolean(variableId && Object.hasOwn(variables, variableId));
7619
7674
  });
7620
7675
  }
7676
+ function authoredMediaStarts(html) {
7677
+ const { document: document2 } = parseHTML5(html);
7678
+ const starts = /* @__PURE__ */ new Map();
7679
+ for (const media of document2.querySelectorAll("video, audio")) {
7680
+ const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
7681
+ if (id && !starts.has(id)) starts.set(id, readMediaStart2(media));
7682
+ }
7683
+ return starts;
7684
+ }
7685
+ function adoptRuntimeMediaStart(composition, el, authored) {
7686
+ const shift = el.mediaStart - authored;
7687
+ if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7688
+ const entries = el.tagName === "video" ? [
7689
+ ...composition.videos.filter((video) => video.id === el.id),
7690
+ ...composition.audios.filter(
7691
+ (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7692
+ )
7693
+ ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7694
+ for (const entry of entries) entry.mediaStart += shift;
7695
+ }
7621
7696
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7622
7697
  if (projectedEnd <= 0) return existingEnd;
7623
7698
  if (sourceChanged && durationInferred) return projectedEnd;
@@ -7720,7 +7795,7 @@ async function runProbeStage(input) {
7720
7795
  height,
7721
7796
  fps: job.config.fps,
7722
7797
  format: captureFormat,
7723
- quality: captureFormat === "png" ? void 0 : 80,
7798
+ quality: captureFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
7724
7799
  variables: job.config.variables,
7725
7800
  deviceScaleFactor,
7726
7801
  motionBlur,
@@ -7871,6 +7946,7 @@ async function runProbeStage(input) {
7871
7946
  const existingVideoIds = new Set(composition.videos.map((v) => v.id));
7872
7947
  const existingAudioIds = new Set(composition.audios.map((a) => a.id));
7873
7948
  const existingImageIds = new Set(composition.images.map((image) => image.id));
7949
+ const authoredStarts = authoredMediaStarts(compiled.html);
7874
7950
  pruneMutedBrowserMedia(composition, browserMedia, existingAudioIds);
7875
7951
  for (const el of browserMedia) {
7876
7952
  if (!el.src || el.src === "about:blank") continue;
@@ -7898,9 +7974,11 @@ async function runProbeStage(input) {
7898
7974
  sourceChanged,
7899
7975
  el.durationInferred
7900
7976
  );
7901
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7902
- existing.mediaStart = el.mediaStart;
7903
- }
7977
+ adoptRuntimeMediaStart(
7978
+ composition,
7979
+ el,
7980
+ authoredStarts.get(el.id) ?? existing.mediaStart
7981
+ );
7904
7982
  if (el.hasAudio && !el.muted && !existing.hasAudio) {
7905
7983
  existing.hasAudio = true;
7906
7984
  }
@@ -7908,7 +7986,7 @@ async function runProbeStage(input) {
7908
7986
  existing.loop = true;
7909
7987
  }
7910
7988
  }
7911
- } else {
7989
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7912
7990
  composition.videos.push({
7913
7991
  id: el.id,
7914
7992
  src,
@@ -7939,14 +8017,16 @@ async function runProbeStage(input) {
7939
8017
  sourceChanged,
7940
8018
  el.durationInferred
7941
8019
  );
7942
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7943
- existing.mediaStart = el.mediaStart;
7944
- }
8020
+ adoptRuntimeMediaStart(
8021
+ composition,
8022
+ el,
8023
+ authoredStarts.get(el.id) ?? existing.mediaStart
8024
+ );
7945
8025
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
7946
8026
  existing.volume = el.volume;
7947
8027
  }
7948
8028
  }
7949
- } else {
8029
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7950
8030
  composition.audios.push({
7951
8031
  id: el.id,
7952
8032
  src,
@@ -7974,7 +8054,7 @@ async function runProbeStage(input) {
7974
8054
  existing.end = projectedEnd;
7975
8055
  }
7976
8056
  }
7977
- } else {
8057
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7978
8058
  composition.images.push({
7979
8059
  id: el.id,
7980
8060
  src,
@@ -8557,6 +8637,7 @@ async function runAudioStage(input) {
8557
8637
  ffmpegProcessTimeout,
8558
8638
  audioGain,
8559
8639
  audios,
8640
+ masterFxChain,
8560
8641
  abortSignal,
8561
8642
  assertNotAborted,
8562
8643
  log
@@ -8576,7 +8657,7 @@ async function runAudioStage(input) {
8576
8657
  audioOutputPath,
8577
8658
  duration,
8578
8659
  abortSignal,
8579
- { ffmpegProcessTimeout, audioGain },
8660
+ { ffmpegProcessTimeout, audioGain, masterFxChain },
8580
8661
  compiledDir
8581
8662
  );
8582
8663
  } catch (err) {
@@ -8662,13 +8743,16 @@ function shouldAllowAdaptiveCaptureRetry(workerCount, _explicitlyConfigured) {
8662
8743
  return workerCount > 1;
8663
8744
  }
8664
8745
  var JPEG_RAW_RGBA_DIVISOR = 8;
8746
+ var HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR = 4;
8665
8747
  var DISK_PROJECTION_SAMPLE_FRAMES = 10;
8666
8748
  function estimateDiskCaptureBytes(totalFrames, captureOptions) {
8667
8749
  const scale = captureOptions.deviceScaleFactor ?? 1;
8668
8750
  const outputWidth = Math.ceil(captureOptions.width * scale);
8669
8751
  const outputHeight = Math.ceil(captureOptions.height * scale);
8670
8752
  const rawBytes = Math.ceil(totalFrames) * outputWidth * outputHeight * 4;
8671
- return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(rawBytes / JPEG_RAW_RGBA_DIVISOR) : rawBytes;
8753
+ return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
8754
+ rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
8755
+ ) : rawBytes;
8672
8756
  }
8673
8757
  function freeDiskBytesAt(path) {
8674
8758
  try {
@@ -12045,6 +12129,7 @@ async function executeRenderPipeline(input) {
12045
12129
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
12046
12130
  audioGain: cfg.audioGain,
12047
12131
  audios: composition.audios,
12132
+ masterFxChain: composition.masterFxChain,
12048
12133
  abortSignal: executionSignal,
12049
12134
  assertNotAborted,
12050
12135
  log
@@ -12129,7 +12214,7 @@ async function executeRenderPipeline(input) {
12129
12214
  height,
12130
12215
  fps: job.config.fps,
12131
12216
  format: captureImageFormat,
12132
- quality: captureImageFormat === "png" ? void 0 : job.config.quality === "draft" ? 80 : 95,
12217
+ quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
12133
12218
  variables: job.config.variables,
12134
12219
  deviceScaleFactor,
12135
12220
  motionBlur: job.config.motionBlur,
@@ -12757,7 +12842,8 @@ async function executeRenderPipeline(input) {
12757
12842
  // Device-scaled: the capture buffer, not the CSS composition size.
12758
12843
  outputWidth: captureCompositionWidth ?? width,
12759
12844
  outputHeight: captureCompositionHeight ?? height,
12760
- motionBlur: job.config.motionBlur ? JSON.stringify(job.config.motionBlur) : ""
12845
+ motionBlur: job.config.motionBlur ? JSON.stringify(job.config.motionBlur) : "",
12846
+ captureQuality: captureOptions.quality
12761
12847
  });
12762
12848
  const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
12763
12849
  let completedSegments = /* @__PURE__ */ new Set();
@@ -8,6 +8,8 @@ export interface CompiledComposition {
8
8
  videos: VideoElement[];
9
9
  audios: AudioElement[];
10
10
  images: ImageElement[];
11
+ droppedMediaIds: Set<string>;
12
+ masterFxChain?: string;
11
13
  unresolvedCompositions: UnresolvedElement[];
12
14
  /** Assets that resolve outside projectDir. Keys are the path used in HTML, values are absolute filesystem paths. */
13
15
  externalAssets: Map<string, string>;
@@ -1,4 +1,4 @@
1
- import type { MotionBlurOptions } from "@hyperframes/engine";
1
+ import type { EngineConfig, MotionBlurOptions } from "@hyperframes/engine";
2
2
  /**
3
3
  * The format a render captures its frames in.
4
4
  *
@@ -16,3 +16,8 @@ export declare function resolveCaptureImageFormat(input: {
16
16
  needsAlpha: boolean;
17
17
  motionBlur: MotionBlurOptions | undefined;
18
18
  }): "jpeg" | "png";
19
+ /**
20
+ * Below 100, Chrome's JPEG turns graded colour and grain into 8x8 chroma blotches that survive
21
+ * the final encode. Draft keeps the smaller, faster frames.
22
+ */
23
+ export declare function resolveCaptureJpegQuality(quality: EngineConfig["quality"]): number;
@@ -37,6 +37,7 @@ export interface SegmentPlanHashInput {
37
37
  outputHeight: number;
38
38
  /** Serialised motion-blur options, or "" when off; sample count and shutter change every pixel. */
39
39
  motionBlur: string;
40
+ captureQuality: number | undefined;
40
41
  }
41
42
  /** Everything that determines segment bytes. Key order is fixed by construction. */
42
43
  export declare function computeSegmentPlanHash(input: SegmentPlanHashInput): string;
@@ -19,6 +19,7 @@ export interface CompositionMetadata {
19
19
  videos: VideoElement[];
20
20
  audios: AudioElement[];
21
21
  images: ImageElement[];
22
+ masterFxChain?: string;
22
23
  width: number;
23
24
  height: number;
24
25
  }
@@ -32,6 +32,7 @@ export interface AudioStageInput {
32
32
  audioGain: number;
33
33
  /** Read-only view of `composition.audios`. */
34
34
  audios: CompositionMetadata["audios"];
35
+ masterFxChain?: string;
35
36
  abortSignal: AbortSignal | undefined;
36
37
  assertNotAborted: () => void;
37
38
  /** Where a per-track failure's detail goes. Optional so tests need not pass one. */
@@ -1,23 +1,17 @@
1
1
  /**
2
2
  * Collect the render pipeline's media list from the fully inlined document.
3
3
  *
4
- * Sub-composition media used to be gathered from each composition FILE before
5
- * inlining, then merged with the main document's media and deduplicated by
6
- * element id. That merge is unsound: ids are unique per file, not per render
7
- * document, so two scenes that both declare `<video id="clip">` — or that both
8
- * declare a bare `<video>` and get the per-file auto-id `hf-video-0` — collapse
9
- * into a single entry. See mediaRenderIds.ts for the full failure.
10
- *
11
- * Reading the inlined document instead makes the render document the single
12
- * source of truth for what media exists: every element is present exactly once,
13
- * `assignMediaRenderIds` has already given it a document-unique key, and the
14
- * timeline offsets are recoverable from the composition hosts it sits inside.
4
+ * Ids are unique per composition file, not per render document (see mediaRenderIds.ts), so media is
5
+ * read from the inlined document: each element once, keyed by its render id or else its author id,
6
+ * and placed by the composition hosts it sits inside.
15
7
  */
16
8
  import { type VideoElement, type ImageElement, type AudioElement } from "@hyperframes/engine";
17
9
  export interface RenderMedia {
18
10
  videos: VideoElement[];
19
11
  audios: AudioElement[];
20
12
  images: ImageElement[];
13
+ /** Ids of clips in the document that never play; the browser probe must not add them back. */
14
+ droppedMediaIds: Set<string>;
21
15
  }
22
16
  /**
23
17
  * Parse every media element in the inlined render document, with each clip's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.149",
3
+ "version": "0.8.151",
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.149",
39
- "@hyperframes/core": "0.8.149",
40
- "@hyperframes/lint": "0.8.149",
41
- "@hyperframes/studio-server": "0.8.149",
42
- "@hyperframes/parsers": "0.8.149"
38
+ "@hyperframes/lint": "0.8.151",
39
+ "@hyperframes/engine": "0.8.151",
40
+ "@hyperframes/studio-server": "0.8.151",
41
+ "@hyperframes/parsers": "0.8.151",
42
+ "@hyperframes/core": "0.8.151"
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.149"
51
+ "@hyperframes/aws-lambda": "0.8.151"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"