@hyperframes/producer 0.8.149 → 0.8.150

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
@@ -5220,9 +5220,18 @@ import {
5220
5220
 
5221
5221
  // src/services/renderMediaCollector.ts
5222
5222
  import { parseHTML as parseHTML2 } from "linkedom";
5223
- import { MEDIA_RENDER_ID_ATTR, resolveAuthoredTimingWindow } from "@hyperframes/core";
5223
+ import {
5224
+ MEDIA_RENDER_ID_ATTR,
5225
+ normalizeRateSpec as normalizeRateSpec2,
5226
+ resolveAuthoredTimingWindow,
5227
+ shiftRateLane,
5228
+ sourceTimeAt as sourceTimeAt2
5229
+ } from "@hyperframes/core";
5224
5230
  import {
5225
5231
  MEDIA_START_BASIS_ATTR,
5232
+ compositionOriginSeconds,
5233
+ cutToHostSlots,
5234
+ hostInPointSeconds,
5226
5235
  readMediaStartBasis,
5227
5236
  resolveAbsoluteMediaStartSeconds
5228
5237
  } from "@hyperframes/core/media-timing";
@@ -5232,7 +5241,6 @@ import {
5232
5241
  parseAudioElements,
5233
5242
  resolveReferencedStart
5234
5243
  } from "@hyperframes/engine";
5235
- var COMPOSITION_HOST_ATTR = "data-composition-file";
5236
5244
  function resolveHostEnd(host, hostStart) {
5237
5245
  return resolveAuthoredTimingWindow({
5238
5246
  start: hostStart,
@@ -5240,39 +5248,52 @@ function resolveHostEnd(host, hostStart) {
5240
5248
  end: host.getAttribute("data-end")
5241
5249
  })?.end ?? null;
5242
5250
  }
5243
- var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local" };
5244
- function resolveHostWindow(element, document2, startCache, visiting) {
5251
+ var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local", slots: [] };
5252
+ var COMPOSITION_HOST_SELECTOR = "[data-composition-id], [data-composition-src], [data-composition-file]";
5253
+ function enclosingHosts(element) {
5245
5254
  const hosts = [];
5246
5255
  for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
5247
- if (ancestor.hasAttribute(COMPOSITION_HOST_ATTR)) hosts.push(ancestor);
5256
+ if (ancestor.matches(COMPOSITION_HOST_SELECTOR)) {
5257
+ hosts.unshift(ancestor);
5258
+ }
5248
5259
  }
5260
+ return hosts;
5261
+ }
5262
+ function resolveHostWindow(element, document2, startCache, visiting) {
5263
+ const hosts = enclosingHosts(element);
5249
5264
  if (hosts.length === 0) return ROOT_WINDOW;
5250
5265
  let offset = 0;
5251
5266
  let limit = Infinity;
5252
- for (const host of hosts.reverse()) {
5267
+ const slots = [];
5268
+ for (const [index, host] of hosts.entries()) {
5253
5269
  const hostStart = resolveReferencedStart(document2, host, startCache, visiting);
5254
5270
  const hostEnd = resolveHostEnd(host, hostStart);
5255
- if (hostEnd != null) limit = Math.min(limit, offset + hostEnd);
5256
- offset += hostStart;
5271
+ if (hostEnd != null && !isRootComposition(host, index)) {
5272
+ limit = Math.min(limit, offset + hostEnd);
5273
+ }
5274
+ if (hostInPointSeconds(host) > 0) {
5275
+ slots.push({ start: offset + hostStart, end: offset + (hostEnd ?? Infinity) });
5276
+ }
5277
+ offset += compositionOriginSeconds(hostStart, host);
5257
5278
  }
5279
+ return { offset, limit, basis: mediaStartBasis(element), slots };
5280
+ }
5281
+ var isRootComposition = (host, depth) => depth === 0 && !host.matches("[data-composition-src], [data-composition-file]");
5282
+ function mediaStartBasis(element) {
5258
5283
  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
- };
5284
+ return tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
5265
5285
  }
5266
5286
  function collectHostWindows(html) {
5267
5287
  const { document: document2 } = parseHTML2(html);
5268
5288
  const windows = /* @__PURE__ */ new Map();
5269
5289
  const startCache = /* @__PURE__ */ new Map();
5270
5290
  const visiting = /* @__PURE__ */ new Set();
5271
- for (const element of document2.querySelectorAll(`[${MEDIA_RENDER_ID_ATTR}]`)) {
5291
+ for (const element of document2.querySelectorAll("video, audio, img[src]")) {
5272
5292
  const renderId = element.getAttribute(MEDIA_RENDER_ID_ATTR);
5273
- if (!renderId) continue;
5293
+ const id = renderId || element.getAttribute("id");
5294
+ if (!id || !renderId && windows.has(id)) continue;
5274
5295
  windows.set(
5275
- renderId,
5296
+ id,
5276
5297
  resolveHostWindow(element, document2, startCache, visiting)
5277
5298
  );
5278
5299
  }
@@ -5290,34 +5311,59 @@ function toAbsoluteWindow(start, end, window2) {
5290
5311
  hostStart: window2.offset,
5291
5312
  basis: window2.basis
5292
5313
  });
5293
- return { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5314
+ const absolute = { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5315
+ const played = cutToHostSlots(absolute, window2.slots);
5316
+ if (played.end === played.start && absolute.end > absolute.start) return null;
5317
+ return { start: absolute.start, end: played.end, played };
5318
+ }
5319
+ function startAtPlayed(clip, played) {
5320
+ const head = played.start - clip.start;
5321
+ if (head <= 0) return clip;
5322
+ if ("loop" in clip && clip.loop) return { ...clip, start: played.start };
5323
+ const rate = normalizeRateSpec2(clip.playbackRate);
5324
+ return {
5325
+ ...clip,
5326
+ start: played.start,
5327
+ mediaStart: clip.mediaStart + sourceTimeAt2(rate, head),
5328
+ playbackRate: shiftRateLane(rate, head)
5329
+ };
5294
5330
  }
5295
5331
  function collectRenderMedia(html) {
5296
5332
  const windows = collectHostWindows(html);
5297
5333
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5334
+ const droppedMediaIds = /* @__PURE__ */ new Set();
5298
5335
  const videos = [];
5299
5336
  for (const video of parseVideoElements(html)) {
5300
5337
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5301
- if (absolute) videos.push({ ...video, ...absolute });
5338
+ if (!absolute) {
5339
+ droppedMediaIds.add(video.id);
5340
+ continue;
5341
+ }
5342
+ videos.push(
5343
+ startAtPlayed({ ...video, start: absolute.start, end: absolute.end }, absolute.played)
5344
+ );
5302
5345
  }
5303
5346
  const images = [];
5304
5347
  for (const image of parseImageElements(html)) {
5305
5348
  const absolute = toAbsoluteWindow(image.start, image.end, windowFor(image.id));
5306
- if (absolute) images.push({ ...image, ...absolute });
5349
+ if (absolute) images.push({ ...image, start: absolute.start, end: absolute.end });
5350
+ else droppedMediaIds.add(image.id);
5307
5351
  }
5308
5352
  const audios = [];
5309
5353
  for (const audio of parseAudioElements(html)) {
5310
5354
  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
- });
5355
+ const placed = placeAudio(audio, windowFor(elementId));
5356
+ if (placed) audios.push(placed);
5357
+ else droppedMediaIds.add(elementId);
5319
5358
  }
5320
- return { videos, audios, images };
5359
+ return { videos, audios, images, droppedMediaIds };
5360
+ }
5361
+ function placeAudio(audio, window2) {
5362
+ const authoredEnd = audio.end > 0 ? audio.end : Infinity;
5363
+ const absolute = toAbsoluteWindow(audio.start, authoredEnd, window2);
5364
+ if (!absolute) return null;
5365
+ const end = Number.isFinite(absolute.end) ? absolute.end : 0;
5366
+ return startAtPlayed({ ...audio, start: absolute.start, end }, absolute.played);
5321
5367
  }
5322
5368
 
5323
5369
  // src/services/htmlCompiler.ts
@@ -5325,6 +5371,7 @@ import {
5325
5371
  analyzeKeyframeIntervals,
5326
5372
  assertMediaPayload,
5327
5373
  NotMediaPayloadError as NotMediaPayloadError2,
5374
+ parseMasterFxChain,
5328
5375
  probeMediaProfile as probeMediaProfile2
5329
5376
  } from "@hyperframes/engine";
5330
5377
 
@@ -7024,7 +7071,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7024
7071
  for (const [relPath, absPath] of preparedGifAssets) {
7025
7072
  externalAssets.set(relPath, absPath);
7026
7073
  }
7027
- const { videos, audios, images } = collectRenderMedia(html);
7074
+ const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7075
+ const masterFxChain = parseMasterFxChain(html);
7028
7076
  for (const video of videos) {
7029
7077
  if (isHttpUrl(video.src)) continue;
7030
7078
  const videoPath = resolve6(projectDir, video.src);
@@ -7058,6 +7106,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7058
7106
  videos,
7059
7107
  audios,
7060
7108
  images,
7109
+ droppedMediaIds,
7110
+ ...masterFxChain ? { masterFxChain } : {},
7061
7111
  unresolvedCompositions,
7062
7112
  externalAssets,
7063
7113
  width,
@@ -7490,6 +7540,7 @@ async function runCompileStage(input) {
7490
7540
  videos: compiled.videos,
7491
7541
  audios: compiled.audios,
7492
7542
  images: compiled.images,
7543
+ masterFxChain: compiled.masterFxChain,
7493
7544
  width: compiled.width,
7494
7545
  height: compiled.height
7495
7546
  };
@@ -7552,7 +7603,7 @@ import {
7552
7603
  isTransientBrowserError,
7553
7604
  probeBeginFrameLiveness
7554
7605
  } from "@hyperframes/engine";
7555
- import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
7606
+ import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7556
7607
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7557
7608
 
7558
7609
  // src/services/render/stages/probeFailures.ts
@@ -7599,6 +7650,26 @@ function hasVariableBoundMedia(html, variables) {
7599
7650
  return Boolean(variableId && Object.hasOwn(variables, variableId));
7600
7651
  });
7601
7652
  }
7653
+ function authoredMediaStarts(html) {
7654
+ const { document: document2 } = parseHTML5(html);
7655
+ const starts = /* @__PURE__ */ new Map();
7656
+ for (const media of document2.querySelectorAll("video, audio")) {
7657
+ const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
7658
+ if (id && !starts.has(id)) starts.set(id, readMediaStart2(media));
7659
+ }
7660
+ return starts;
7661
+ }
7662
+ function adoptRuntimeMediaStart(composition, el, authored) {
7663
+ const shift = el.mediaStart - authored;
7664
+ if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7665
+ const entries = el.tagName === "video" ? [
7666
+ ...composition.videos.filter((video) => video.id === el.id),
7667
+ ...composition.audios.filter(
7668
+ (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7669
+ )
7670
+ ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7671
+ for (const entry of entries) entry.mediaStart += shift;
7672
+ }
7602
7673
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7603
7674
  if (projectedEnd <= 0) return existingEnd;
7604
7675
  if (sourceChanged && durationInferred) return projectedEnd;
@@ -7852,6 +7923,7 @@ async function runProbeStage(input) {
7852
7923
  const existingVideoIds = new Set(composition.videos.map((v) => v.id));
7853
7924
  const existingAudioIds = new Set(composition.audios.map((a) => a.id));
7854
7925
  const existingImageIds = new Set(composition.images.map((image) => image.id));
7926
+ const authoredStarts = authoredMediaStarts(compiled.html);
7855
7927
  pruneMutedBrowserMedia(composition, browserMedia, existingAudioIds);
7856
7928
  for (const el of browserMedia) {
7857
7929
  if (!el.src || el.src === "about:blank") continue;
@@ -7879,9 +7951,11 @@ async function runProbeStage(input) {
7879
7951
  sourceChanged,
7880
7952
  el.durationInferred
7881
7953
  );
7882
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7883
- existing.mediaStart = el.mediaStart;
7884
- }
7954
+ adoptRuntimeMediaStart(
7955
+ composition,
7956
+ el,
7957
+ authoredStarts.get(el.id) ?? existing.mediaStart
7958
+ );
7885
7959
  if (el.hasAudio && !el.muted && !existing.hasAudio) {
7886
7960
  existing.hasAudio = true;
7887
7961
  }
@@ -7889,7 +7963,7 @@ async function runProbeStage(input) {
7889
7963
  existing.loop = true;
7890
7964
  }
7891
7965
  }
7892
- } else {
7966
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7893
7967
  composition.videos.push({
7894
7968
  id: el.id,
7895
7969
  src,
@@ -7920,14 +7994,16 @@ async function runProbeStage(input) {
7920
7994
  sourceChanged,
7921
7995
  el.durationInferred
7922
7996
  );
7923
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7924
- existing.mediaStart = el.mediaStart;
7925
- }
7997
+ adoptRuntimeMediaStart(
7998
+ composition,
7999
+ el,
8000
+ authoredStarts.get(el.id) ?? existing.mediaStart
8001
+ );
7926
8002
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
7927
8003
  existing.volume = el.volume;
7928
8004
  }
7929
8005
  }
7930
- } else {
8006
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7931
8007
  composition.audios.push({
7932
8008
  id: el.id,
7933
8009
  src,
@@ -7955,7 +8031,7 @@ async function runProbeStage(input) {
7955
8031
  existing.end = projectedEnd;
7956
8032
  }
7957
8033
  }
7958
- } else {
8034
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7959
8035
  composition.images.push({
7960
8036
  id: el.id,
7961
8037
  src,
@@ -8574,6 +8650,7 @@ async function runAudioStage(input) {
8574
8650
  ffmpegProcessTimeout,
8575
8651
  audioGain,
8576
8652
  audios,
8653
+ masterFxChain,
8577
8654
  abortSignal,
8578
8655
  assertNotAborted,
8579
8656
  log
@@ -8593,7 +8670,7 @@ async function runAudioStage(input) {
8593
8670
  audioOutputPath,
8594
8671
  duration,
8595
8672
  abortSignal,
8596
- { ffmpegProcessTimeout, audioGain },
8673
+ { ffmpegProcessTimeout, audioGain, masterFxChain },
8597
8674
  compiledDir
8598
8675
  );
8599
8676
  } catch (err) {
@@ -12062,6 +12139,7 @@ async function executeRenderPipeline(input) {
12062
12139
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
12063
12140
  audioGain: cfg.audioGain,
12064
12141
  audios: composition.audios,
12142
+ masterFxChain: composition.masterFxChain,
12065
12143
  abortSignal: executionSignal,
12066
12144
  assertNotAborted,
12067
12145
  log
@@ -16012,6 +16090,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16012
16090
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
16013
16091
  audioGain: cfg.audioGain,
16014
16092
  audios: composition.audios,
16093
+ masterFxChain: composition.masterFxChain,
16015
16094
  abortSignal,
16016
16095
  assertNotAborted
16017
16096
  });
@@ -5239,9 +5239,18 @@ import {
5239
5239
 
5240
5240
  // src/services/renderMediaCollector.ts
5241
5241
  import { parseHTML as parseHTML2 } from "linkedom";
5242
- import { MEDIA_RENDER_ID_ATTR, resolveAuthoredTimingWindow } from "@hyperframes/core";
5242
+ import {
5243
+ MEDIA_RENDER_ID_ATTR,
5244
+ normalizeRateSpec as normalizeRateSpec2,
5245
+ resolveAuthoredTimingWindow,
5246
+ shiftRateLane,
5247
+ sourceTimeAt as sourceTimeAt2
5248
+ } from "@hyperframes/core";
5243
5249
  import {
5244
5250
  MEDIA_START_BASIS_ATTR,
5251
+ compositionOriginSeconds,
5252
+ cutToHostSlots,
5253
+ hostInPointSeconds,
5245
5254
  readMediaStartBasis,
5246
5255
  resolveAbsoluteMediaStartSeconds
5247
5256
  } from "@hyperframes/core/media-timing";
@@ -5251,7 +5260,6 @@ import {
5251
5260
  parseAudioElements,
5252
5261
  resolveReferencedStart
5253
5262
  } from "@hyperframes/engine";
5254
- var COMPOSITION_HOST_ATTR = "data-composition-file";
5255
5263
  function resolveHostEnd(host, hostStart) {
5256
5264
  return resolveAuthoredTimingWindow({
5257
5265
  start: hostStart,
@@ -5259,39 +5267,52 @@ function resolveHostEnd(host, hostStart) {
5259
5267
  end: host.getAttribute("data-end")
5260
5268
  })?.end ?? null;
5261
5269
  }
5262
- var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local" };
5263
- function resolveHostWindow(element, document2, startCache, visiting) {
5270
+ var ROOT_WINDOW = { offset: 0, limit: Infinity, basis: "local", slots: [] };
5271
+ var COMPOSITION_HOST_SELECTOR = "[data-composition-id], [data-composition-src], [data-composition-file]";
5272
+ function enclosingHosts(element) {
5264
5273
  const hosts = [];
5265
5274
  for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
5266
- if (ancestor.hasAttribute(COMPOSITION_HOST_ATTR)) hosts.push(ancestor);
5275
+ if (ancestor.matches(COMPOSITION_HOST_SELECTOR)) {
5276
+ hosts.unshift(ancestor);
5277
+ }
5267
5278
  }
5279
+ return hosts;
5280
+ }
5281
+ function resolveHostWindow(element, document2, startCache, visiting) {
5282
+ const hosts = enclosingHosts(element);
5268
5283
  if (hosts.length === 0) return ROOT_WINDOW;
5269
5284
  let offset = 0;
5270
5285
  let limit = Infinity;
5271
- for (const host of hosts.reverse()) {
5286
+ const slots = [];
5287
+ for (const [index, host] of hosts.entries()) {
5272
5288
  const hostStart = resolveReferencedStart(document2, host, startCache, visiting);
5273
5289
  const hostEnd = resolveHostEnd(host, hostStart);
5274
- if (hostEnd != null) limit = Math.min(limit, offset + hostEnd);
5275
- offset += hostStart;
5290
+ if (hostEnd != null && !isRootComposition(host, index)) {
5291
+ limit = Math.min(limit, offset + hostEnd);
5292
+ }
5293
+ if (hostInPointSeconds(host) > 0) {
5294
+ slots.push({ start: offset + hostStart, end: offset + (hostEnd ?? Infinity) });
5295
+ }
5296
+ offset += compositionOriginSeconds(hostStart, host);
5276
5297
  }
5298
+ return { offset, limit, basis: mediaStartBasis(element), slots };
5299
+ }
5300
+ var isRootComposition = (host, depth) => depth === 0 && !host.matches("[data-composition-src], [data-composition-file]");
5301
+ function mediaStartBasis(element) {
5277
5302
  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
- };
5303
+ return tag === "video" || tag === "audio" ? readMediaStartBasis(element.getAttribute(MEDIA_START_BASIS_ATTR)) : "local";
5284
5304
  }
5285
5305
  function collectHostWindows(html) {
5286
5306
  const { document: document2 } = parseHTML2(html);
5287
5307
  const windows = /* @__PURE__ */ new Map();
5288
5308
  const startCache = /* @__PURE__ */ new Map();
5289
5309
  const visiting = /* @__PURE__ */ new Set();
5290
- for (const element of document2.querySelectorAll(`[${MEDIA_RENDER_ID_ATTR}]`)) {
5310
+ for (const element of document2.querySelectorAll("video, audio, img[src]")) {
5291
5311
  const renderId = element.getAttribute(MEDIA_RENDER_ID_ATTR);
5292
- if (!renderId) continue;
5312
+ const id = renderId || element.getAttribute("id");
5313
+ if (!id || !renderId && windows.has(id)) continue;
5293
5314
  windows.set(
5294
- renderId,
5315
+ id,
5295
5316
  resolveHostWindow(element, document2, startCache, visiting)
5296
5317
  );
5297
5318
  }
@@ -5309,34 +5330,59 @@ function toAbsoluteWindow(start, end, window2) {
5309
5330
  hostStart: window2.offset,
5310
5331
  basis: window2.basis
5311
5332
  });
5312
- return { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5333
+ const absolute = { start: absoluteStart, end: Math.min(absoluteEnd, window2.limit) };
5334
+ const played = cutToHostSlots(absolute, window2.slots);
5335
+ if (played.end === played.start && absolute.end > absolute.start) return null;
5336
+ return { start: absolute.start, end: played.end, played };
5337
+ }
5338
+ function startAtPlayed(clip, played) {
5339
+ const head = played.start - clip.start;
5340
+ if (head <= 0) return clip;
5341
+ if ("loop" in clip && clip.loop) return { ...clip, start: played.start };
5342
+ const rate = normalizeRateSpec2(clip.playbackRate);
5343
+ return {
5344
+ ...clip,
5345
+ start: played.start,
5346
+ mediaStart: clip.mediaStart + sourceTimeAt2(rate, head),
5347
+ playbackRate: shiftRateLane(rate, head)
5348
+ };
5313
5349
  }
5314
5350
  function collectRenderMedia(html) {
5315
5351
  const windows = collectHostWindows(html);
5316
5352
  const windowFor = (id) => windows.get(id) ?? ROOT_WINDOW;
5353
+ const droppedMediaIds = /* @__PURE__ */ new Set();
5317
5354
  const videos = [];
5318
5355
  for (const video of parseVideoElements(html)) {
5319
5356
  const absolute = toAbsoluteWindow(video.start, video.end, windowFor(video.id));
5320
- if (absolute) videos.push({ ...video, ...absolute });
5357
+ if (!absolute) {
5358
+ droppedMediaIds.add(video.id);
5359
+ continue;
5360
+ }
5361
+ videos.push(
5362
+ startAtPlayed({ ...video, start: absolute.start, end: absolute.end }, absolute.played)
5363
+ );
5321
5364
  }
5322
5365
  const images = [];
5323
5366
  for (const image of parseImageElements(html)) {
5324
5367
  const absolute = toAbsoluteWindow(image.start, image.end, windowFor(image.id));
5325
- if (absolute) images.push({ ...image, ...absolute });
5368
+ if (absolute) images.push({ ...image, start: absolute.start, end: absolute.end });
5369
+ else droppedMediaIds.add(image.id);
5326
5370
  }
5327
5371
  const audios = [];
5328
5372
  for (const audio of parseAudioElements(html)) {
5329
5373
  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
- });
5374
+ const placed = placeAudio(audio, windowFor(elementId));
5375
+ if (placed) audios.push(placed);
5376
+ else droppedMediaIds.add(elementId);
5338
5377
  }
5339
- return { videos, audios, images };
5378
+ return { videos, audios, images, droppedMediaIds };
5379
+ }
5380
+ function placeAudio(audio, window2) {
5381
+ const authoredEnd = audio.end > 0 ? audio.end : Infinity;
5382
+ const absolute = toAbsoluteWindow(audio.start, authoredEnd, window2);
5383
+ if (!absolute) return null;
5384
+ const end = Number.isFinite(absolute.end) ? absolute.end : 0;
5385
+ return startAtPlayed({ ...audio, start: absolute.start, end }, absolute.played);
5340
5386
  }
5341
5387
 
5342
5388
  // src/services/htmlCompiler.ts
@@ -5344,6 +5390,7 @@ import {
5344
5390
  analyzeKeyframeIntervals,
5345
5391
  assertMediaPayload,
5346
5392
  NotMediaPayloadError as NotMediaPayloadError2,
5393
+ parseMasterFxChain,
5347
5394
  probeMediaProfile as probeMediaProfile2
5348
5395
  } from "@hyperframes/engine";
5349
5396
 
@@ -7043,7 +7090,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7043
7090
  for (const [relPath, absPath] of preparedGifAssets) {
7044
7091
  externalAssets.set(relPath, absPath);
7045
7092
  }
7046
- const { videos, audios, images } = collectRenderMedia(html);
7093
+ const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7094
+ const masterFxChain = parseMasterFxChain(html);
7047
7095
  for (const video of videos) {
7048
7096
  if (isHttpUrl(video.src)) continue;
7049
7097
  const videoPath = resolve6(projectDir, video.src);
@@ -7077,6 +7125,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7077
7125
  videos,
7078
7126
  audios,
7079
7127
  images,
7128
+ droppedMediaIds,
7129
+ ...masterFxChain ? { masterFxChain } : {},
7080
7130
  unresolvedCompositions,
7081
7131
  externalAssets,
7082
7132
  width,
@@ -7509,6 +7559,7 @@ async function runCompileStage(input) {
7509
7559
  videos: compiled.videos,
7510
7560
  audios: compiled.audios,
7511
7561
  images: compiled.images,
7562
+ masterFxChain: compiled.masterFxChain,
7512
7563
  width: compiled.width,
7513
7564
  height: compiled.height
7514
7565
  };
@@ -7571,7 +7622,7 @@ import {
7571
7622
  isTransientBrowserError,
7572
7623
  probeBeginFrameLiveness
7573
7624
  } from "@hyperframes/engine";
7574
- import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
7625
+ import { MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2, fpsToNumber as fpsToNumber7, readMediaStart as readMediaStart2 } from "@hyperframes/core";
7575
7626
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
7576
7627
 
7577
7628
  // src/services/render/stages/probeFailures.ts
@@ -7618,6 +7669,26 @@ function hasVariableBoundMedia(html, variables) {
7618
7669
  return Boolean(variableId && Object.hasOwn(variables, variableId));
7619
7670
  });
7620
7671
  }
7672
+ function authoredMediaStarts(html) {
7673
+ const { document: document2 } = parseHTML5(html);
7674
+ const starts = /* @__PURE__ */ new Map();
7675
+ for (const media of document2.querySelectorAll("video, audio")) {
7676
+ const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
7677
+ if (id && !starts.has(id)) starts.set(id, readMediaStart2(media));
7678
+ }
7679
+ return starts;
7680
+ }
7681
+ function adoptRuntimeMediaStart(composition, el, authored) {
7682
+ const shift = el.mediaStart - authored;
7683
+ if (el.mediaStart <= 0 || Math.abs(shift) <= BROWSER_MEDIA_EPSILON) return;
7684
+ const entries = el.tagName === "video" ? [
7685
+ ...composition.videos.filter((video) => video.id === el.id),
7686
+ ...composition.audios.filter(
7687
+ (audio) => audio.type === "video" && audio.id === `${el.id}-audio`
7688
+ )
7689
+ ] : composition.audios.filter((audio) => audio.type !== "video" && audio.id === el.id);
7690
+ for (const entry of entries) entry.mediaStart += shift;
7691
+ }
7621
7692
  function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, durationInferred) {
7622
7693
  if (projectedEnd <= 0) return existingEnd;
7623
7694
  if (sourceChanged && durationInferred) return projectedEnd;
@@ -7871,6 +7942,7 @@ async function runProbeStage(input) {
7871
7942
  const existingVideoIds = new Set(composition.videos.map((v) => v.id));
7872
7943
  const existingAudioIds = new Set(composition.audios.map((a) => a.id));
7873
7944
  const existingImageIds = new Set(composition.images.map((image) => image.id));
7945
+ const authoredStarts = authoredMediaStarts(compiled.html);
7874
7946
  pruneMutedBrowserMedia(composition, browserMedia, existingAudioIds);
7875
7947
  for (const el of browserMedia) {
7876
7948
  if (!el.src || el.src === "about:blank") continue;
@@ -7898,9 +7970,11 @@ async function runProbeStage(input) {
7898
7970
  sourceChanged,
7899
7971
  el.durationInferred
7900
7972
  );
7901
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7902
- existing.mediaStart = el.mediaStart;
7903
- }
7973
+ adoptRuntimeMediaStart(
7974
+ composition,
7975
+ el,
7976
+ authoredStarts.get(el.id) ?? existing.mediaStart
7977
+ );
7904
7978
  if (el.hasAudio && !el.muted && !existing.hasAudio) {
7905
7979
  existing.hasAudio = true;
7906
7980
  }
@@ -7908,7 +7982,7 @@ async function runProbeStage(input) {
7908
7982
  existing.loop = true;
7909
7983
  }
7910
7984
  }
7911
- } else {
7985
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7912
7986
  composition.videos.push({
7913
7987
  id: el.id,
7914
7988
  src,
@@ -7939,14 +8013,16 @@ async function runProbeStage(input) {
7939
8013
  sourceChanged,
7940
8014
  el.durationInferred
7941
8015
  );
7942
- if (el.mediaStart > 0 && (existing.mediaStart <= 0 || Math.abs(existing.mediaStart - el.mediaStart) > BROWSER_MEDIA_EPSILON)) {
7943
- existing.mediaStart = el.mediaStart;
7944
- }
8016
+ adoptRuntimeMediaStart(
8017
+ composition,
8018
+ el,
8019
+ authoredStarts.get(el.id) ?? existing.mediaStart
8020
+ );
7945
8021
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
7946
8022
  existing.volume = el.volume;
7947
8023
  }
7948
8024
  }
7949
- } else {
8025
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7950
8026
  composition.audios.push({
7951
8027
  id: el.id,
7952
8028
  src,
@@ -7974,7 +8050,7 @@ async function runProbeStage(input) {
7974
8050
  existing.end = projectedEnd;
7975
8051
  }
7976
8052
  }
7977
- } else {
8053
+ } else if (!compiled.droppedMediaIds.has(el.id)) {
7978
8054
  composition.images.push({
7979
8055
  id: el.id,
7980
8056
  src,
@@ -8557,6 +8633,7 @@ async function runAudioStage(input) {
8557
8633
  ffmpegProcessTimeout,
8558
8634
  audioGain,
8559
8635
  audios,
8636
+ masterFxChain,
8560
8637
  abortSignal,
8561
8638
  assertNotAborted,
8562
8639
  log
@@ -8576,7 +8653,7 @@ async function runAudioStage(input) {
8576
8653
  audioOutputPath,
8577
8654
  duration,
8578
8655
  abortSignal,
8579
- { ffmpegProcessTimeout, audioGain },
8656
+ { ffmpegProcessTimeout, audioGain, masterFxChain },
8580
8657
  compiledDir
8581
8658
  );
8582
8659
  } catch (err) {
@@ -12045,6 +12122,7 @@ async function executeRenderPipeline(input) {
12045
12122
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
12046
12123
  audioGain: cfg.audioGain,
12047
12124
  audios: composition.audios,
12125
+ masterFxChain: composition.masterFxChain,
12048
12126
  abortSignal: executionSignal,
12049
12127
  assertNotAborted,
12050
12128
  log
@@ -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>;
@@ -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. */