@hyperframes/producer 0.4.6 → 0.4.8

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
@@ -5116,12 +5116,12 @@ var require_common = __commonJS({
5116
5116
  createDebug.skips = [];
5117
5117
  createDebug.formatters = {};
5118
5118
  function selectColor(namespace) {
5119
- let hash = 0;
5119
+ let hash2 = 0;
5120
5120
  for (let i = 0; i < namespace.length; i++) {
5121
- hash = (hash << 5) - hash + namespace.charCodeAt(i);
5122
- hash |= 0;
5121
+ hash2 = (hash2 << 5) - hash2 + namespace.charCodeAt(i);
5122
+ hash2 |= 0;
5123
5123
  }
5124
- return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
5124
+ return createDebug.colors[Math.abs(hash2) % createDebug.colors.length];
5125
5125
  }
5126
5126
  createDebug.selectColor = selectColor;
5127
5127
  function createDebug(namespace) {
@@ -54349,25 +54349,25 @@ var require_data = __commonJS({
54349
54349
  var notmodified_1 = __importDefault2(require_notmodified());
54350
54350
  var debug6 = (0, debug_1.default)("get-uri:data");
54351
54351
  var DataReadable = class extends stream_1.Readable {
54352
- constructor(hash, buf) {
54352
+ constructor(hash2, buf) {
54353
54353
  super();
54354
54354
  this.push(buf);
54355
54355
  this.push(null);
54356
- this.hash = hash;
54356
+ this.hash = hash2;
54357
54357
  }
54358
54358
  };
54359
54359
  var data = async ({ href: uri }, { cache } = {}) => {
54360
54360
  const shasum = (0, crypto_1.createHash)("sha1");
54361
54361
  shasum.update(uri);
54362
- const hash = shasum.digest("hex");
54363
- debug6('generated SHA1 hash for "data:" URI: %o', hash);
54364
- if (cache?.hash === hash) {
54365
- debug6("got matching cache SHA1 hash: %o", hash);
54362
+ const hash2 = shasum.digest("hex");
54363
+ debug6('generated SHA1 hash for "data:" URI: %o', hash2);
54364
+ if (cache?.hash === hash2) {
54365
+ debug6("got matching cache SHA1 hash: %o", hash2);
54366
54366
  throw new notmodified_1.default();
54367
54367
  } else {
54368
54368
  debug6('creating Readable stream from "data:" URI buffer');
54369
54369
  const { buffer } = (0, data_uri_to_buffer_1.dataUriToBuffer)(uri);
54370
- return new DataReadable(hash, Buffer.from(buffer));
54370
+ return new DataReadable(hash2, Buffer.from(buffer));
54371
54371
  }
54372
54372
  };
54373
54373
  exports.data = data;
@@ -75362,14 +75362,14 @@ var require_dist10 = __commonJS({
75362
75362
  (0, quickjs_emscripten_1.getQuickJS)(),
75363
75363
  this.loadPacFile()
75364
75364
  ]);
75365
- const hash = crypto3.createHash("sha1").update(code).digest("hex");
75366
- if (this.resolver && this.resolverHash === hash) {
75365
+ const hash2 = crypto3.createHash("sha1").update(code).digest("hex");
75366
+ if (this.resolver && this.resolverHash === hash2) {
75367
75367
  debug6("Same sha1 hash for code - contents have not changed, reusing previous proxy resolver");
75368
75368
  return this.resolver;
75369
75369
  }
75370
75370
  debug6("Creating new proxy resolver instance");
75371
75371
  this.resolver = (0, pac_resolver_1.createPacResolver)(qjs, code, this.opts);
75372
- this.resolverHash = hash;
75372
+ this.resolverHash = hash2;
75373
75373
  return this.resolver;
75374
75374
  } catch (err) {
75375
75375
  if (this.resolver && err.code === "ENOTMODIFIED") {
@@ -89421,6 +89421,7 @@ import {
89421
89421
  mkdirSync as mkdirSync10,
89422
89422
  rmSync as rmSync3,
89423
89423
  readFileSync as readFileSync9,
89424
+ readdirSync as readdirSync6,
89424
89425
  writeFileSync as writeFileSync4,
89425
89426
  copyFileSync as copyFileSync2,
89426
89427
  appendFileSync
@@ -98668,6 +98669,8 @@ var DEFAULT_CONFIG = {
98668
98669
  ffmpegEncodeTimeout: 6e5,
98669
98670
  ffmpegProcessTimeout: 3e5,
98670
98671
  ffmpegStreamingTimeout: 6e5,
98672
+ hdr: false,
98673
+ hdrAutoDetect: true,
98671
98674
  audioGain: 1.35,
98672
98675
  frameDataUriCacheLimit: 256,
98673
98676
  playerReadyTimeout: 45e3,
@@ -98724,6 +98727,12 @@ function resolveConfig(overrides) {
98724
98727
  "FFMPEG_STREAMING_TIMEOUT_MS",
98725
98728
  DEFAULT_CONFIG.ffmpegStreamingTimeout
98726
98729
  ),
98730
+ hdr: (() => {
98731
+ const raw2 = env2("PRODUCER_HDR_TRANSFER");
98732
+ if (raw2 === "hlg" || raw2 === "pq") return { transfer: raw2 };
98733
+ return void 0;
98734
+ })(),
98735
+ hdrAutoDetect: envBool("PRODUCER_HDR_AUTO_DETECT", DEFAULT_CONFIG.hdrAutoDetect),
98727
98736
  audioGain: envNum("PRODUCER_AUDIO_GAIN", DEFAULT_CONFIG.audioGain),
98728
98737
  frameDataUriCacheLimit: Math.max(
98729
98738
  32,
@@ -98920,7 +98929,8 @@ function buildChromeArgs(options, config2) {
98920
98929
  "--font-render-hinting=none",
98921
98930
  "--force-color-profile=srgb",
98922
98931
  `--window-size=${options.width},${options.height}`,
98923
- // Remotion perf flags — prevent Chrome from throttling background tabs/timers
98932
+ // Prevent Chrome from throttling background tabs/timers — critical when the
98933
+ // page is offscreen during headless capture
98924
98934
  "--disable-background-timer-throttling",
98925
98935
  "--disable-backgrounding-occluded-windows",
98926
98936
  "--disable-renderer-backgrounding",
@@ -99634,6 +99644,7 @@ var mediaRules = [
99634
99644
  const timedTagPositions = [];
99635
99645
  for (const tag of tags) {
99636
99646
  if (tag.name === "video" || tag.name === "audio") continue;
99647
+ if (readAttr(tag.raw, "data-composition-id")) continue;
99637
99648
  if (readAttr(tag.raw, "data-start")) {
99638
99649
  timedTagPositions.push({
99639
99650
  name: tag.name,
@@ -100740,7 +100751,8 @@ function lintHyperframeHtml(html, options = {}) {
100740
100751
  }
100741
100752
 
100742
100753
  // ../core/src/compiler/rewriteSubCompPaths.ts
100743
- import { join as join4, resolve as resolve6, dirname as dirname4 } from "path";
100754
+ import { posix } from "path";
100755
+ var { join: join4, resolve: resolve6, dirname: dirname4 } = posix;
100744
100756
  var PATH_ATTRS = ["src", "href"];
100745
100757
  var CSS_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
100746
100758
  function isAbsoluteOrSpecial(val) {
@@ -100907,6 +100919,83 @@ async function pageScreenshotCapture(page, options) {
100907
100919
  });
100908
100920
  return Buffer.from(result.data, "base64");
100909
100921
  }
100922
+ var TRANSPARENT_BG_STYLE_ID = "__hf_transparent_bg__";
100923
+ async function initTransparentBackground(page) {
100924
+ const client = await getCdpSession(page);
100925
+ await client.send("Emulation.setDefaultBackgroundColorOverride", {
100926
+ color: { r: 0, g: 0, b: 0, a: 0 }
100927
+ });
100928
+ await page.evaluate((styleId) => {
100929
+ if (document.getElementById(styleId)) return;
100930
+ const style = document.createElement("style");
100931
+ style.id = styleId;
100932
+ style.textContent = "html,body,[data-composition-id]{background:transparent !important;background-color:transparent !important;background-image:none !important;}";
100933
+ document.head.appendChild(style);
100934
+ }, TRANSPARENT_BG_STYLE_ID);
100935
+ }
100936
+ async function captureAlphaPng(page, width, height) {
100937
+ const client = await getCdpSession(page);
100938
+ const result = await client.send("Page.captureScreenshot", {
100939
+ format: "png",
100940
+ fromSurface: true,
100941
+ captureBeyondViewport: false,
100942
+ optimizeForSpeed: false,
100943
+ // must be false to preserve alpha
100944
+ clip: { x: 0, y: 0, width, height, scale: 1 }
100945
+ });
100946
+ return Buffer.from(result.data, "base64");
100947
+ }
100948
+ var DOM_LAYER_MASK_STYLE_ID = "__hf_dom_layer_mask__";
100949
+ async function applyDomLayerMask(page, showIds, extraHideIds) {
100950
+ await page.evaluate(
100951
+ (args) => {
100952
+ const existing = document.getElementById(args.styleId);
100953
+ if (existing) existing.remove();
100954
+ const showSelectors = [];
100955
+ for (const id of args.show) {
100956
+ const escaped = CSS.escape(id);
100957
+ showSelectors.push(`#${escaped}`, `#${escaped} *`);
100958
+ const renderEscaped = CSS.escape(`__render_frame_${id}__`);
100959
+ showSelectors.push(`#${renderEscaped}`, `#${renderEscaped} *`);
100960
+ }
100961
+ const massHideRule = "body *{visibility:hidden !important;}";
100962
+ const showRule = showSelectors.length === 0 ? "" : `${showSelectors.join(",")}{visibility:visible !important;}`;
100963
+ const style = document.createElement("style");
100964
+ style.id = args.styleId;
100965
+ style.textContent = `${massHideRule}
100966
+ ${showRule}`;
100967
+ document.head.appendChild(style);
100968
+ for (const id of args.hide) {
100969
+ const el = document.getElementById(id);
100970
+ if (el) {
100971
+ el.style.setProperty("visibility", "hidden", "important");
100972
+ }
100973
+ const img = document.getElementById(`__render_frame_${id}__`);
100974
+ if (img) {
100975
+ img.style.setProperty("visibility", "hidden", "important");
100976
+ }
100977
+ }
100978
+ },
100979
+ { show: showIds, hide: extraHideIds, styleId: DOM_LAYER_MASK_STYLE_ID }
100980
+ );
100981
+ }
100982
+ async function removeDomLayerMask(page, extraHideIds) {
100983
+ await page.evaluate(
100984
+ (args) => {
100985
+ const style = document.getElementById(args.styleId);
100986
+ if (style) style.remove();
100987
+ for (const id of args.hide) {
100988
+ const el = document.getElementById(id);
100989
+ if (el) {
100990
+ el.style.removeProperty("visibility");
100991
+ }
100992
+ const img = document.getElementById(`__render_frame_${id}__`);
100993
+ if (img) img.style.removeProperty("visibility");
100994
+ }
100995
+ },
100996
+ { hide: extraHideIds, styleId: DOM_LAYER_MASK_STYLE_ID }
100997
+ );
100998
+ }
100910
100999
  async function injectVideoFramesBatch(page, updates) {
100911
101000
  if (updates.length === 0) return;
100912
101001
  await page.evaluate(
@@ -100928,16 +101017,7 @@ async function injectVideoFramesBatch(page, updates) {
100928
101017
  video.parentNode?.insertBefore(img, video.nextSibling);
100929
101018
  }
100930
101019
  if (!img) continue;
100931
- if (!sourceIsStatic) {
100932
- img.style.position = computedStyle.position;
100933
- img.style.width = computedStyle.width;
100934
- img.style.height = computedStyle.height;
100935
- img.style.top = computedStyle.top;
100936
- img.style.left = computedStyle.left;
100937
- img.style.right = computedStyle.right;
100938
- img.style.bottom = computedStyle.bottom;
100939
- img.style.inset = computedStyle.inset;
100940
- } else {
101020
+ {
100941
101021
  const videoRect = video.getBoundingClientRect();
100942
101022
  const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
100943
101023
  const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
@@ -100956,6 +101036,7 @@ async function injectVideoFramesBatch(page, updates) {
100956
101036
  img.style.objectPosition = computedStyle.objectPosition;
100957
101037
  img.style.zIndex = computedStyle.zIndex;
100958
101038
  for (const property of visualProperties) {
101039
+ if (property === "opacity") continue;
100959
101040
  if (sourceIsStatic && (property === "top" || property === "left" || property === "right" || property === "bottom" || property === "inset")) {
100960
101041
  continue;
100961
101042
  }
@@ -100988,14 +101069,22 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
100988
101069
  const active = new Set(ids);
100989
101070
  const videos = Array.from(document.querySelectorAll("video[data-start]"));
100990
101071
  for (const video of videos) {
100991
- if (active.has(video.id)) continue;
100992
- video.style.removeProperty("display");
100993
- video.style.setProperty("visibility", "hidden", "important");
100994
- video.style.setProperty("opacity", "0", "important");
100995
- video.style.setProperty("pointer-events", "none", "important");
100996
101072
  const img = video.nextElementSibling;
100997
- if (img && img.classList.contains("__render_frame__")) {
100998
- img.style.visibility = "hidden";
101073
+ const hasImg = img && img.classList.contains("__render_frame__");
101074
+ if (active.has(video.id)) {
101075
+ video.style.setProperty("visibility", "hidden", "important");
101076
+ video.style.setProperty("pointer-events", "none", "important");
101077
+ if (hasImg) {
101078
+ img.style.visibility = "visible";
101079
+ }
101080
+ } else {
101081
+ video.style.removeProperty("display");
101082
+ video.style.setProperty("visibility", "hidden", "important");
101083
+ video.style.setProperty("opacity", "0", "important");
101084
+ video.style.setProperty("pointer-events", "none", "important");
101085
+ if (hasImg) {
101086
+ img.style.visibility = "hidden";
101087
+ }
100999
101088
  }
101000
101089
  }
101001
101090
  }, activeVideoIds);
@@ -101452,7 +101541,7 @@ var ENCODER_PRESETS = {
101452
101541
  standard: { preset: "medium", quality: 18, codec: "h264" },
101453
101542
  high: { preset: "slow", quality: 15, codec: "h264" }
101454
101543
  };
101455
- function getEncoderPreset(quality, format3 = "mp4") {
101544
+ function getEncoderPreset(quality, format3 = "mp4", hdr) {
101456
101545
  const base = ENCODER_PRESETS[quality];
101457
101546
  if (format3 === "webm") {
101458
101547
  return {
@@ -101470,6 +101559,15 @@ function getEncoderPreset(quality, format3 = "mp4") {
101470
101559
  pixelFormat: "yuva444p10le"
101471
101560
  };
101472
101561
  }
101562
+ if (hdr) {
101563
+ return {
101564
+ preset: base.preset === "ultrafast" ? "fast" : base.preset,
101565
+ quality: base.quality,
101566
+ codec: "h265",
101567
+ pixelFormat: "yuv420p10le",
101568
+ hdr
101569
+ };
101570
+ }
101473
101571
  return { ...base, pixelFormat: "yuv420p" };
101474
101572
  }
101475
101573
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -101527,6 +101625,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
101527
101625
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
101528
101626
  }
101529
101627
  }
101628
+ if (codec === "h265") {
101629
+ args.push("-tag:v", "hvc1");
101630
+ }
101530
101631
  } else if (codec === "vp9") {
101531
101632
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
101532
101633
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -101833,31 +101934,83 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
101833
101934
  import { spawn as spawn6 } from "child_process";
101834
101935
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync4 } from "fs";
101835
101936
  import { dirname as dirname6 } from "path";
101937
+
101938
+ // ../engine/src/utils/hdr.ts
101939
+ function isHdrColorSpace(cs) {
101940
+ if (!cs) return false;
101941
+ return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
101942
+ }
101943
+ function detectTransfer(cs) {
101944
+ if (cs?.colorTransfer === "smpte2084") return "pq";
101945
+ return "hlg";
101946
+ }
101947
+ var DEFAULT_HDR10_MASTERING = {
101948
+ masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
101949
+ maxCll: "1000,400"
101950
+ };
101951
+ function getHdrEncoderColorParams(transfer, mastering = DEFAULT_HDR10_MASTERING) {
101952
+ const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
101953
+ const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
101954
+ const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
101955
+ return {
101956
+ colorPrimaries: "bt2020",
101957
+ colorTrc,
101958
+ colorspace: "bt2020nc",
101959
+ pixelFormat: "yuv420p10le",
101960
+ x265ColorParams: `${tagging}:${metadata}`,
101961
+ mastering
101962
+ };
101963
+ }
101964
+ function analyzeCompositionHdr(colorSpaces) {
101965
+ let hasPq = false;
101966
+ let hasHdr = false;
101967
+ for (const cs of colorSpaces) {
101968
+ if (!isHdrColorSpace(cs)) continue;
101969
+ hasHdr = true;
101970
+ if (cs?.colorTransfer === "smpte2084") hasPq = true;
101971
+ }
101972
+ if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
101973
+ const dominantTransfer = hasPq ? "pq" : "hlg";
101974
+ return { hasHdr: true, dominantTransfer };
101975
+ }
101976
+
101977
+ // ../engine/src/services/streamingEncoder.ts
101836
101978
  function createFrameReorderBuffer(startFrame, endFrame) {
101837
- let nextFrame = startFrame;
101838
- let waiters = [];
101839
- const resolveWaiters = () => {
101840
- for (const waiter of waiters.slice()) {
101841
- if (waiter.frame === nextFrame) {
101842
- waiter.resolve();
101843
- waiters = waiters.filter((w) => w !== waiter);
101844
- }
101979
+ let cursor = startFrame;
101980
+ const pending = /* @__PURE__ */ new Map();
101981
+ const enqueueAt = (frame, resolve13) => {
101982
+ const list = pending.get(frame);
101983
+ if (list === void 0) {
101984
+ pending.set(frame, [resolve13]);
101985
+ } else {
101986
+ list.push(resolve13);
101845
101987
  }
101846
101988
  };
101847
- return {
101848
- waitForFrame: (frame) => new Promise((resolve13) => {
101849
- waiters.push({ frame, resolve: resolve13 });
101850
- resolveWaiters();
101851
- }),
101852
- advanceTo: (frame) => {
101853
- nextFrame = frame;
101854
- resolveWaiters();
101855
- },
101856
- waitForAllDone: () => new Promise((resolve13) => {
101857
- waiters.push({ frame: endFrame, resolve: resolve13 });
101858
- resolveWaiters();
101859
- })
101989
+ const flushAt = (frame) => {
101990
+ const list = pending.get(frame);
101991
+ if (list === void 0) return;
101992
+ pending.delete(frame);
101993
+ for (const resolve13 of list) resolve13();
101994
+ };
101995
+ const waitForFrame = (frame) => new Promise((resolve13) => {
101996
+ if (frame === cursor) {
101997
+ resolve13();
101998
+ return;
101999
+ }
102000
+ enqueueAt(frame, resolve13);
102001
+ });
102002
+ const advanceTo = (frame) => {
102003
+ cursor = frame;
102004
+ flushAt(frame);
101860
102005
  };
102006
+ const waitForAllDone = () => new Promise((resolve13) => {
102007
+ if (cursor >= endFrame) {
102008
+ resolve13();
102009
+ return;
102010
+ }
102011
+ enqueueAt(endFrame, resolve13);
102012
+ });
102013
+ return { waitForFrame, advanceTo, waitForAllDone };
101861
102014
  }
101862
102015
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101863
102016
  const {
@@ -101870,19 +102023,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101870
102023
  useGpu = false,
101871
102024
  imageFormat = "jpeg"
101872
102025
  } = options;
101873
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
101874
- const args = [
101875
- "-f",
101876
- "image2pipe",
101877
- "-vcodec",
101878
- inputCodec,
101879
- "-framerate",
101880
- String(fps),
101881
- "-i",
101882
- "-",
101883
- "-r",
101884
- String(fps)
101885
- ];
102026
+ const args = [];
102027
+ if (options.rawInputFormat) {
102028
+ const hdrTransfer = options.hdr?.transfer;
102029
+ const inputColorTrc = hdrTransfer === "pq" ? "smpte2084" : hdrTransfer === "hlg" ? "arib-std-b67" : void 0;
102030
+ args.push(
102031
+ "-f",
102032
+ "rawvideo",
102033
+ "-pix_fmt",
102034
+ options.rawInputFormat,
102035
+ "-s",
102036
+ `${options.width}x${options.height}`,
102037
+ "-framerate",
102038
+ String(fps)
102039
+ );
102040
+ if (inputColorTrc) {
102041
+ args.push(
102042
+ "-color_primaries",
102043
+ "bt2020",
102044
+ "-color_trc",
102045
+ inputColorTrc,
102046
+ "-colorspace",
102047
+ "bt2020nc"
102048
+ );
102049
+ }
102050
+ args.push("-i", "-");
102051
+ } else {
102052
+ const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
102053
+ args.push("-f", "image2pipe", "-vcodec", inputCodec, "-framerate", String(fps), "-i", "-");
102054
+ }
102055
+ args.push("-r", String(fps));
101886
102056
  const shouldUseGpu = useGpu && gpuEncoder !== null;
101887
102057
  if (codec === "h264" || codec === "h265") {
101888
102058
  if (shouldUseGpu) {
@@ -101920,12 +102090,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101920
102090
  if (bitrate) args.push("-b:v", bitrate);
101921
102091
  else args.push("-crf", String(quality));
101922
102092
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
101923
- const colorParams = "colorprim=bt709:transfer=bt709:colormatrix=bt709";
102093
+ const colorParams = options.rawInputFormat && options.hdr ? getHdrEncoderColorParams(options.hdr.transfer).x265ColorParams : "colorprim=bt709:transfer=bt709:colormatrix=bt709";
101924
102094
  if (preset === "ultrafast") {
101925
102095
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
101926
102096
  } else {
101927
102097
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
101928
102098
  }
102099
+ if (codec === "h265") {
102100
+ args.push("-tag:v", "hvc1");
102101
+ }
101929
102102
  }
101930
102103
  } else if (codec === "vp9") {
101931
102104
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -101941,17 +102114,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101941
102114
  return [...args, "-y", outputPath];
101942
102115
  }
101943
102116
  if (codec === "h264" || codec === "h265") {
101944
- args.push(
101945
- "-colorspace:v",
101946
- "bt709",
101947
- "-color_primaries:v",
101948
- "bt709",
101949
- "-color_trc:v",
101950
- "bt709",
101951
- "-color_range",
101952
- "tv"
101953
- );
101954
- if (gpuEncoder === "vaapi") {
102117
+ if (options.rawInputFormat && options.hdr) {
102118
+ args.push(
102119
+ "-colorspace:v",
102120
+ "bt2020nc",
102121
+ "-color_primaries:v",
102122
+ "bt2020",
102123
+ "-color_trc:v",
102124
+ options.hdr.transfer === "pq" ? "smpte2084" : "arib-std-b67",
102125
+ "-color_range",
102126
+ "tv"
102127
+ );
102128
+ } else {
102129
+ args.push(
102130
+ "-colorspace:v",
102131
+ "bt709",
102132
+ "-color_primaries:v",
102133
+ "bt709",
102134
+ "-color_trc:v",
102135
+ "bt709",
102136
+ "-color_range",
102137
+ "tv"
102138
+ );
102139
+ }
102140
+ if (options.rawInputFormat) {
102141
+ } else if (gpuEncoder === "vaapi") {
101955
102142
  const vfIdx = args.indexOf("-vf");
101956
102143
  if (vfIdx !== -1) {
101957
102144
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -102021,14 +102208,16 @@ Process error: ${err.message}`;
102021
102208
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
102022
102209
  return false;
102023
102210
  }
102024
- return ffmpeg.stdin.write(buffer);
102211
+ const copy = Buffer.from(buffer);
102212
+ return ffmpeg.stdin.write(copy);
102025
102213
  },
102026
102214
  close: async () => {
102027
102215
  clearTimeout(timer2);
102028
102216
  if (signal) signal.removeEventListener("abort", onAbort);
102029
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
102217
+ const stdin = ffmpeg.stdin;
102218
+ if (stdin && !stdin.destroyed) {
102030
102219
  await new Promise((resolve13) => {
102031
- ffmpeg.stdin.end(() => resolve13());
102220
+ stdin.end(() => resolve13());
102032
102221
  });
102033
102222
  }
102034
102223
  await exitPromise;
@@ -102132,6 +102321,10 @@ async function extractVideoMetadata(filePath) {
102132
102321
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
102133
102322
  const fps = avgFps || rFps;
102134
102323
  const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
102324
+ const colorTransfer = videoStream.color_transfer || "";
102325
+ const colorPrimaries = videoStream.color_primaries || "";
102326
+ const colorSpaceVal = videoStream.color_space || "";
102327
+ const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
102135
102328
  return {
102136
102329
  durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
102137
102330
  width: videoStream.width || 0,
@@ -102139,7 +102332,8 @@ async function extractVideoMetadata(filePath) {
102139
102332
  fps,
102140
102333
  videoCodec: videoStream.codec_name || "unknown",
102141
102334
  hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
102142
- isVFR
102335
+ isVFR,
102336
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
102143
102337
  };
102144
102338
  })();
102145
102339
  videoMetadataCache.set(filePath, probePromise);
@@ -102244,10 +102438,10 @@ import { finished } from "stream/promises";
102244
102438
  var downloadPathCache = /* @__PURE__ */ new Map();
102245
102439
  var inFlightDownloads = /* @__PURE__ */ new Map();
102246
102440
  function getFilenameFromUrl(url) {
102247
- const hash = createHash("md5").update(url).digest("hex").slice(0, 12);
102441
+ const hash2 = createHash("md5").update(url).digest("hex").slice(0, 12);
102248
102442
  const urlObj = new URL(url);
102249
102443
  const ext = extname2(urlObj.pathname) || ".mp4";
102250
- return `download_${hash}${ext}`;
102444
+ return `download_${hash2}${ext}`;
102251
102445
  }
102252
102446
  async function downloadToTemp(url, destDir, timeoutMs = 3e5) {
102253
102447
  const cachedPath = downloadPathCache.get(url);
@@ -102347,18 +102541,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102347
102541
  const metadata = await extractVideoMetadata(videoPath);
102348
102542
  const framePattern = `frame_%05d.${format3}`;
102349
102543
  const outputPattern = join8(videoOutputDir, framePattern);
102350
- const args = [
102351
- "-ss",
102352
- String(startTime),
102353
- "-i",
102354
- videoPath,
102355
- "-t",
102356
- String(duration),
102357
- "-vf",
102358
- `fps=${fps}`,
102359
- "-q:v",
102360
- format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
102361
- ];
102544
+ const isHdr = isHdrColorSpace(metadata.colorSpace);
102545
+ const isMacOS = process.platform === "darwin";
102546
+ const args = [];
102547
+ if (isHdr && isMacOS) {
102548
+ args.push("-hwaccel", "videotoolbox");
102549
+ }
102550
+ args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
102551
+ const vfFilters = [];
102552
+ if (isHdr && isMacOS) {
102553
+ vfFilters.push("format=nv12");
102554
+ }
102555
+ vfFilters.push(`fps=${fps}`);
102556
+ args.push("-vf", vfFilters.join(","));
102557
+ args.push("-q:v", format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
102362
102558
  if (format3 === "png") args.push("-compression_level", "6");
102363
102559
  args.push("-y", outputPattern);
102364
102560
  return new Promise((resolve13, reject) => {
@@ -102418,30 +102614,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102418
102614
  });
102419
102615
  });
102420
102616
  }
102617
+ async function convertSdrToHdr(inputPath, outputPath, signal, config2) {
102618
+ const timeout2 = config2?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
102619
+ const args = [
102620
+ "-i",
102621
+ inputPath,
102622
+ "-vf",
102623
+ "colorspace=all=bt2020:iall=bt709:range=tv",
102624
+ "-color_primaries",
102625
+ "bt2020",
102626
+ "-color_trc",
102627
+ "arib-std-b67",
102628
+ "-colorspace",
102629
+ "bt2020nc",
102630
+ "-c:v",
102631
+ "libx264",
102632
+ "-preset",
102633
+ "fast",
102634
+ "-crf",
102635
+ "16",
102636
+ "-c:a",
102637
+ "copy",
102638
+ "-y",
102639
+ outputPath
102640
+ ];
102641
+ const result = await runFfmpeg(args, { signal, timeout: timeout2 });
102642
+ if (!result.success) {
102643
+ throw new Error(
102644
+ `SDR\u2192HDR conversion failed (exit ${result.exitCode}): ${result.stderr.slice(-300)}`
102645
+ );
102646
+ }
102647
+ }
102421
102648
  async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
102422
102649
  const startTime = Date.now();
102423
102650
  const extracted = [];
102424
102651
  const errors = [];
102425
102652
  let totalFramesExtracted = 0;
102653
+ const resolvedVideos = [];
102654
+ for (const video of videos) {
102655
+ if (signal?.aborted) break;
102656
+ try {
102657
+ let videoPath = video.src;
102658
+ if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
102659
+ const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
102660
+ videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
102661
+ }
102662
+ if (isHttpUrl(videoPath)) {
102663
+ const downloadDir = join8(options.outputDir, "_downloads");
102664
+ mkdirSync5(downloadDir, { recursive: true });
102665
+ videoPath = await downloadToTemp(videoPath, downloadDir);
102666
+ }
102667
+ if (!existsSync8(videoPath)) {
102668
+ errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
102669
+ continue;
102670
+ }
102671
+ resolvedVideos.push({ video, videoPath });
102672
+ } catch (err) {
102673
+ errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
102674
+ }
102675
+ }
102676
+ const videoColorSpaces = await Promise.all(
102677
+ resolvedVideos.map(async ({ videoPath }) => {
102678
+ const metadata = await extractVideoMetadata(videoPath);
102679
+ return metadata.colorSpace;
102680
+ })
102681
+ );
102682
+ const hasAnyHdr = videoColorSpaces.some(isHdrColorSpace);
102683
+ if (hasAnyHdr) {
102684
+ const convertDir = join8(options.outputDir, "_hdr_normalized");
102685
+ mkdirSync5(convertDir, { recursive: true });
102686
+ for (let i = 0; i < resolvedVideos.length; i++) {
102687
+ if (signal?.aborted) break;
102688
+ const cs = videoColorSpaces[i] ?? null;
102689
+ if (!isHdrColorSpace(cs)) {
102690
+ const entry = resolvedVideos[i];
102691
+ if (!entry) continue;
102692
+ const convertedPath = join8(convertDir, `${entry.video.id}_hdr.mp4`);
102693
+ try {
102694
+ await convertSdrToHdr(entry.videoPath, convertedPath, signal, config2);
102695
+ entry.videoPath = convertedPath;
102696
+ } catch (err) {
102697
+ errors.push({
102698
+ videoId: entry.video.id,
102699
+ error: `SDR\u2192HDR conversion failed: ${err instanceof Error ? err.message : String(err)}`
102700
+ });
102701
+ }
102702
+ }
102703
+ }
102704
+ }
102426
102705
  const results = await Promise.all(
102427
- videos.map(async (video) => {
102706
+ resolvedVideos.map(async ({ video, videoPath }) => {
102428
102707
  if (signal?.aborted) {
102429
102708
  throw new Error("Video frame extraction cancelled");
102430
102709
  }
102431
102710
  try {
102432
- let videoPath = video.src;
102433
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
102434
- const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
102435
- videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
102436
- }
102437
- if (isHttpUrl(videoPath)) {
102438
- const downloadDir = join8(options.outputDir, "_downloads");
102439
- mkdirSync5(downloadDir, { recursive: true });
102440
- videoPath = await downloadToTemp(videoPath, downloadDir);
102441
- }
102442
- if (!existsSync8(videoPath)) {
102443
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
102444
- }
102445
102711
  let videoDuration = video.end - video.start;
102446
102712
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
102447
102713
  const metadata = await extractVideoMetadata(videoPath);
@@ -102676,6 +102942,142 @@ function createVideoFrameInjector(frameLookup, config2) {
102676
102942
  }
102677
102943
  };
102678
102944
  }
102945
+ async function queryElementStacking(page, nativeHdrVideoIds) {
102946
+ const hdrIds = Array.from(nativeHdrVideoIds);
102947
+ return page.evaluate((hdrIdList) => {
102948
+ const hdrSet = new Set(hdrIdList);
102949
+ const elements = document.querySelectorAll("[data-start]");
102950
+ const results = [];
102951
+ function getEffectiveZIndex(node) {
102952
+ let current = node;
102953
+ while (current) {
102954
+ const cs = window.getComputedStyle(current);
102955
+ const pos = cs.position;
102956
+ const z = parseInt(cs.zIndex);
102957
+ if (!Number.isNaN(z) && pos !== "static") return z;
102958
+ current = current.parentElement;
102959
+ }
102960
+ return 0;
102961
+ }
102962
+ function getEffectiveBorderRadius(node) {
102963
+ function resolveRadius(value, el) {
102964
+ if (value.includes("%")) {
102965
+ const pct = parseFloat(value) / 100;
102966
+ const htmlEl = el;
102967
+ const w = htmlEl.offsetWidth || 0;
102968
+ const h = htmlEl.offsetHeight || 0;
102969
+ return pct * Math.min(w, h);
102970
+ }
102971
+ return parseFloat(value) || 0;
102972
+ }
102973
+ const selfCs = window.getComputedStyle(node);
102974
+ const selfRadii = [
102975
+ resolveRadius(selfCs.borderTopLeftRadius, node),
102976
+ resolveRadius(selfCs.borderTopRightRadius, node),
102977
+ resolveRadius(selfCs.borderBottomRightRadius, node),
102978
+ resolveRadius(selfCs.borderBottomLeftRadius, node)
102979
+ ];
102980
+ if (selfRadii[0] > 0 || selfRadii[1] > 0 || selfRadii[2] > 0 || selfRadii[3] > 0) {
102981
+ return selfRadii;
102982
+ }
102983
+ let current = node.parentElement;
102984
+ while (current) {
102985
+ const cs = window.getComputedStyle(current);
102986
+ if (cs.overflow !== "visible") {
102987
+ const tl = resolveRadius(cs.borderTopLeftRadius, current);
102988
+ const tr = resolveRadius(cs.borderTopRightRadius, current);
102989
+ const brr = resolveRadius(cs.borderBottomRightRadius, current);
102990
+ const bl = resolveRadius(cs.borderBottomLeftRadius, current);
102991
+ if (tl > 0 || tr > 0 || brr > 0 || bl > 0) {
102992
+ return [tl, tr, brr, bl];
102993
+ }
102994
+ }
102995
+ current = current.parentElement;
102996
+ }
102997
+ return [0, 0, 0, 0];
102998
+ }
102999
+ function getEffectiveOpacity(node) {
103000
+ let opacity = 1;
103001
+ let current = node;
103002
+ while (current) {
103003
+ const cs = window.getComputedStyle(current);
103004
+ const val = parseFloat(cs.opacity);
103005
+ opacity *= Number.isNaN(val) ? 1 : val;
103006
+ current = current.parentElement;
103007
+ }
103008
+ return opacity;
103009
+ }
103010
+ function getViewportMatrix(node) {
103011
+ const chain = [];
103012
+ let current = node;
103013
+ while (current instanceof HTMLElement) {
103014
+ chain.push(current);
103015
+ const next = current.offsetParent ?? current.parentElement;
103016
+ if (next === current) break;
103017
+ current = next;
103018
+ }
103019
+ let mat = new DOMMatrix();
103020
+ for (let i = chain.length - 1; i >= 0; i--) {
103021
+ const htmlEl = chain[i];
103022
+ if (!htmlEl) continue;
103023
+ mat = mat.translate(htmlEl.offsetLeft, htmlEl.offsetTop);
103024
+ const cs = window.getComputedStyle(htmlEl);
103025
+ if (cs.transform && cs.transform !== "none") {
103026
+ const origin = cs.transformOrigin.split(" ");
103027
+ const ox = resolveLength(origin[0] ?? "0", htmlEl.offsetWidth);
103028
+ const oy = resolveLength(origin[1] ?? "0", htmlEl.offsetHeight);
103029
+ try {
103030
+ const t = new DOMMatrix(cs.transform);
103031
+ if (Number.isFinite(t.a) && Number.isFinite(t.b) && Number.isFinite(t.c) && Number.isFinite(t.d) && Number.isFinite(t.e) && Number.isFinite(t.f)) {
103032
+ mat = mat.translate(ox, oy).multiply(t).translate(-ox, -oy);
103033
+ }
103034
+ } catch {
103035
+ }
103036
+ }
103037
+ }
103038
+ return mat.toString();
103039
+ }
103040
+ function resolveLength(value, basis) {
103041
+ if (value.endsWith("%")) {
103042
+ const pct = parseFloat(value) / 100;
103043
+ return Number.isFinite(pct) ? pct * basis : 0;
103044
+ }
103045
+ const n = parseFloat(value);
103046
+ return Number.isFinite(n) ? n : 0;
103047
+ }
103048
+ for (const el of elements) {
103049
+ const id = el.id;
103050
+ if (!id) continue;
103051
+ const rect = el.getBoundingClientRect();
103052
+ const style = window.getComputedStyle(el);
103053
+ const zIndex = getEffectiveZIndex(el);
103054
+ const isHdrEl = hdrSet.has(id);
103055
+ const opacityStartNode = isHdrEl ? el.parentElement : el;
103056
+ const opacity = opacityStartNode ? getEffectiveOpacity(opacityStartNode) : 1;
103057
+ const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
103058
+ const htmlEl = el instanceof HTMLElement ? el : null;
103059
+ results.push({
103060
+ id,
103061
+ zIndex,
103062
+ x: Math.round(rect.x),
103063
+ y: Math.round(rect.y),
103064
+ width: Math.round(rect.width),
103065
+ height: Math.round(rect.height),
103066
+ layoutWidth: htmlEl?.offsetWidth || Math.round(rect.width),
103067
+ layoutHeight: htmlEl?.offsetHeight || Math.round(rect.height),
103068
+ opacity,
103069
+ visible,
103070
+ isHdr: hdrSet.has(id),
103071
+ // For HDR elements, use the full accumulated viewport matrix so the
103072
+ // affine blit can apply rotation/scale/translate properly. For DOM
103073
+ // elements, the element-level transform is sufficient for reference.
103074
+ transform: isHdrEl ? getViewportMatrix(el) : style.transform || "none",
103075
+ borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0]
103076
+ });
103077
+ }
103078
+ return results;
103079
+ }, hdrIds);
103080
+ }
102679
103081
 
102680
103082
  // ../engine/src/services/audioMixer.ts
102681
103083
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
@@ -105786,6 +106188,1016 @@ var serve = (options, listeningListener) => {
105786
106188
  return server;
105787
106189
  };
105788
106190
 
106191
+ // ../engine/src/utils/alphaBlit.ts
106192
+ import { inflateSync } from "zlib";
106193
+ function paeth(a, b, c) {
106194
+ const p = a + b - c;
106195
+ const pa = Math.abs(p - a);
106196
+ const pb = Math.abs(p - b);
106197
+ const pc = Math.abs(p - c);
106198
+ if (pa <= pb && pa <= pc) return a;
106199
+ if (pb <= pc) return b;
106200
+ return c;
106201
+ }
106202
+ function decodePngRaw(buf, caller) {
106203
+ if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71 || buf[4] !== 13 || buf[5] !== 10 || buf[6] !== 26 || buf[7] !== 10) {
106204
+ throw new Error(`${caller}: not a PNG file`);
106205
+ }
106206
+ let pos = 8;
106207
+ let width = 0;
106208
+ let height = 0;
106209
+ let bitDepth = 0;
106210
+ let colorType = 0;
106211
+ let interlace = 0;
106212
+ let sawIhdr = false;
106213
+ const idatChunks = [];
106214
+ while (pos + 12 <= buf.length) {
106215
+ const chunkLen = buf.readUInt32BE(pos);
106216
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
106217
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
106218
+ if (chunkType === "IHDR") {
106219
+ width = chunkData.readUInt32BE(0);
106220
+ height = chunkData.readUInt32BE(4);
106221
+ bitDepth = chunkData[8] ?? 0;
106222
+ colorType = chunkData[9] ?? 0;
106223
+ interlace = chunkData[12] ?? 0;
106224
+ sawIhdr = true;
106225
+ } else if (chunkType === "IDAT") {
106226
+ idatChunks.push(Buffer.from(chunkData));
106227
+ } else if (chunkType === "IEND") {
106228
+ break;
106229
+ }
106230
+ pos += 12 + chunkLen;
106231
+ }
106232
+ if (!sawIhdr) {
106233
+ throw new Error(`${caller}: PNG missing IHDR chunk`);
106234
+ }
106235
+ if (colorType !== 2 && colorType !== 6) {
106236
+ throw new Error(`${caller}: unsupported color type ${colorType} (expected 2=RGB or 6=RGBA)`);
106237
+ }
106238
+ if (interlace !== 0) {
106239
+ throw new Error(
106240
+ `${caller}: Adam7-interlaced PNGs are not supported (interlace method ${interlace})`
106241
+ );
106242
+ }
106243
+ const channels = colorType === 6 ? 4 : 3;
106244
+ const bpp = channels * (bitDepth / 8);
106245
+ const stride = width * bpp;
106246
+ const compressed = Buffer.concat(idatChunks);
106247
+ const decompressed = inflateSync(compressed);
106248
+ const rawPixels = Buffer.allocUnsafe(height * stride);
106249
+ const prevRow = new Uint8Array(stride);
106250
+ const currRow = new Uint8Array(stride);
106251
+ let srcPos = 0;
106252
+ for (let y = 0; y < height; y++) {
106253
+ const filterType = decompressed[srcPos++] ?? 0;
106254
+ const rawRow = decompressed.subarray(srcPos, srcPos + stride);
106255
+ srcPos += stride;
106256
+ switch (filterType) {
106257
+ case 0:
106258
+ currRow.set(rawRow);
106259
+ break;
106260
+ case 1:
106261
+ for (let x = 0; x < stride; x++) {
106262
+ currRow[x] = (rawRow[x] ?? 0) + (x >= bpp ? currRow[x - bpp] ?? 0 : 0) & 255;
106263
+ }
106264
+ break;
106265
+ case 2:
106266
+ for (let x = 0; x < stride; x++) {
106267
+ currRow[x] = (rawRow[x] ?? 0) + (prevRow[x] ?? 0) & 255;
106268
+ }
106269
+ break;
106270
+ case 3:
106271
+ for (let x = 0; x < stride; x++) {
106272
+ const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
106273
+ const up = prevRow[x] ?? 0;
106274
+ currRow[x] = (rawRow[x] ?? 0) + Math.floor((left2 + up) / 2) & 255;
106275
+ }
106276
+ break;
106277
+ case 4:
106278
+ for (let x = 0; x < stride; x++) {
106279
+ const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
106280
+ const up = prevRow[x] ?? 0;
106281
+ const upLeft = x >= bpp ? prevRow[x - bpp] ?? 0 : 0;
106282
+ currRow[x] = (rawRow[x] ?? 0) + paeth(left2, up, upLeft) & 255;
106283
+ }
106284
+ break;
106285
+ default:
106286
+ throw new Error(`${caller}: unknown filter type ${filterType} at row ${y}`);
106287
+ }
106288
+ rawPixels.set(currRow, y * stride);
106289
+ prevRow.set(currRow);
106290
+ }
106291
+ return { width, height, bitDepth, colorType, rawPixels };
106292
+ }
106293
+ function decodePng(buf) {
106294
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePng");
106295
+ if (bitDepth !== 8) {
106296
+ throw new Error(`decodePng: unsupported bit depth ${bitDepth} (expected 8)`);
106297
+ }
106298
+ const output2 = new Uint8Array(width * height * 4);
106299
+ if (colorType === 6) {
106300
+ output2.set(rawPixels);
106301
+ } else {
106302
+ for (let i = 0; i < width * height; i++) {
106303
+ output2[i * 4 + 0] = rawPixels[i * 3 + 0] ?? 0;
106304
+ output2[i * 4 + 1] = rawPixels[i * 3 + 1] ?? 0;
106305
+ output2[i * 4 + 2] = rawPixels[i * 3 + 2] ?? 0;
106306
+ output2[i * 4 + 3] = 255;
106307
+ }
106308
+ }
106309
+ return { width, height, data: output2 };
106310
+ }
106311
+ function decodePngToRgb48le(buf) {
106312
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePngToRgb48le");
106313
+ if (bitDepth !== 16) {
106314
+ throw new Error(`decodePngToRgb48le: unsupported bit depth ${bitDepth} (expected 16)`);
106315
+ }
106316
+ const bpp = colorType === 6 ? 8 : 6;
106317
+ const output2 = Buffer.allocUnsafe(width * height * 6);
106318
+ for (let y = 0; y < height; y++) {
106319
+ const dstBase = y * width * 6;
106320
+ const srcRowBase = y * width * bpp;
106321
+ for (let x = 0; x < width; x++) {
106322
+ const srcBase = srcRowBase + x * bpp;
106323
+ output2[dstBase + x * 6 + 0] = rawPixels[srcBase + 1] ?? 0;
106324
+ output2[dstBase + x * 6 + 1] = rawPixels[srcBase + 0] ?? 0;
106325
+ output2[dstBase + x * 6 + 2] = rawPixels[srcBase + 3] ?? 0;
106326
+ output2[dstBase + x * 6 + 3] = rawPixels[srcBase + 2] ?? 0;
106327
+ output2[dstBase + x * 6 + 4] = rawPixels[srcBase + 5] ?? 0;
106328
+ output2[dstBase + x * 6 + 5] = rawPixels[srcBase + 4] ?? 0;
106329
+ }
106330
+ }
106331
+ return { width, height, data: output2 };
106332
+ }
106333
+ function buildSrgbToHdrLut(transfer) {
106334
+ const lut = new Uint16Array(256);
106335
+ const hlgA = 0.17883277;
106336
+ const hlgB = 1 - 4 * hlgA;
106337
+ const hlgC = 0.5 - hlgA * Math.log(4 * hlgA);
106338
+ const pqM1 = 0.1593017578125;
106339
+ const pqM2 = 78.84375;
106340
+ const pqC1 = 0.8359375;
106341
+ const pqC2 = 18.8515625;
106342
+ const pqC3 = 18.6875;
106343
+ const pqMaxNits = 1e4;
106344
+ const sdrNits = 203;
106345
+ for (let i = 0; i < 256; i++) {
106346
+ const v = i / 255;
106347
+ const linear = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
106348
+ let signal;
106349
+ if (transfer === "hlg") {
106350
+ signal = linear <= 1 / 12 ? Math.sqrt(3 * linear) : hlgA * Math.log(12 * linear - hlgB) + hlgC;
106351
+ } else {
106352
+ const Lp = Math.max(0, linear * sdrNits / pqMaxNits);
106353
+ const Lm1 = Math.pow(Lp, pqM1);
106354
+ signal = Math.pow((pqC1 + pqC2 * Lm1) / (1 + pqC3 * Lm1), pqM2);
106355
+ }
106356
+ lut[i] = Math.min(65535, Math.round(signal * 65535));
106357
+ }
106358
+ return lut;
106359
+ }
106360
+ var SRGB_TO_HLG = buildSrgbToHdrLut("hlg");
106361
+ var SRGB_TO_PQ = buildSrgbToHdrLut("pq");
106362
+ function getSrgbToHdrLut(transfer) {
106363
+ return transfer === "pq" ? SRGB_TO_PQ : SRGB_TO_HLG;
106364
+ }
106365
+ function blitRgba8OverRgb48le(domRgba, canvas, width, height, transfer = "hlg") {
106366
+ const pixelCount = width * height;
106367
+ const lut = getSrgbToHdrLut(transfer);
106368
+ for (let i = 0; i < pixelCount; i++) {
106369
+ const da = domRgba[i * 4 + 3] ?? 0;
106370
+ if (da === 0) {
106371
+ continue;
106372
+ } else if (da === 255) {
106373
+ const r16 = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
106374
+ const g16 = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
106375
+ const b16 = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
106376
+ canvas.writeUInt16LE(r16, i * 6);
106377
+ canvas.writeUInt16LE(g16, i * 6 + 2);
106378
+ canvas.writeUInt16LE(b16, i * 6 + 4);
106379
+ } else {
106380
+ const alpha = da / 255;
106381
+ const invAlpha = 1 - alpha;
106382
+ const hdrR = (canvas[i * 6 + 0] ?? 0) | (canvas[i * 6 + 1] ?? 0) << 8;
106383
+ const hdrG = (canvas[i * 6 + 2] ?? 0) | (canvas[i * 6 + 3] ?? 0) << 8;
106384
+ const hdrB = (canvas[i * 6 + 4] ?? 0) | (canvas[i * 6 + 5] ?? 0) << 8;
106385
+ const domR = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
106386
+ const domG = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
106387
+ const domB = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
106388
+ canvas.writeUInt16LE(Math.round(domR * alpha + hdrR * invAlpha), i * 6);
106389
+ canvas.writeUInt16LE(Math.round(domG * alpha + hdrG * invAlpha), i * 6 + 2);
106390
+ canvas.writeUInt16LE(Math.round(domB * alpha + hdrB * invAlpha), i * 6 + 4);
106391
+ }
106392
+ }
106393
+ }
106394
+ function cornerAlpha(px, py, cx, cy, r) {
106395
+ const dx = px - cx;
106396
+ const dy = py - cy;
106397
+ const dist = Math.sqrt(dx * dx + dy * dy);
106398
+ if (dist > r + 0.5) return 0;
106399
+ if (dist > r - 0.5) return r + 0.5 - dist;
106400
+ return 1;
106401
+ }
106402
+ function roundedRectAlpha(px, py, w, h, radii) {
106403
+ const [tl, tr, br, bl] = radii;
106404
+ if (px < tl && py < tl) return cornerAlpha(px, py, tl, tl, tl);
106405
+ if (px >= w - tr && py < tr) return cornerAlpha(px, py, w - tr, tr, tr);
106406
+ if (px >= w - br && py >= h - br) return cornerAlpha(px, py, w - br, h - br, br);
106407
+ if (px < bl && py >= h - bl) return cornerAlpha(px, py, bl, h - bl, bl);
106408
+ return 1;
106409
+ }
106410
+ function blitRgb48leRegion(canvas, source2, dx, dy, sw, sh, canvasWidth, canvasHeight, opacity, borderRadius) {
106411
+ if (sw <= 0 || sh <= 0) return;
106412
+ const op = opacity ?? 1;
106413
+ const x0 = Math.max(0, dx);
106414
+ const y0 = Math.max(0, dy);
106415
+ const x1 = Math.min(canvasWidth, dx + sw);
106416
+ const y1 = Math.min(canvasHeight, dy + sh);
106417
+ if (x0 >= x1 || y0 >= y1) return;
106418
+ const clippedW = x1 - x0;
106419
+ const srcOffsetX = x0 - dx;
106420
+ const srcOffsetY = y0 - dy;
106421
+ const hasMask = borderRadius !== void 0;
106422
+ if (op >= 0.999 && !hasMask) {
106423
+ for (let y = 0; y < y1 - y0; y++) {
106424
+ const srcRowOff = ((srcOffsetY + y) * sw + srcOffsetX) * 6;
106425
+ const dstRowOff = ((y0 + y) * canvasWidth + x0) * 6;
106426
+ source2.copy(canvas, dstRowOff, srcRowOff, srcRowOff + clippedW * 6);
106427
+ }
106428
+ } else {
106429
+ for (let y = 0; y < y1 - y0; y++) {
106430
+ for (let x = 0; x < clippedW; x++) {
106431
+ let effectiveOp = op;
106432
+ if (hasMask) {
106433
+ const ma = roundedRectAlpha(srcOffsetX + x, srcOffsetY + y, sw, sh, borderRadius);
106434
+ if (ma <= 0) continue;
106435
+ effectiveOp *= ma;
106436
+ }
106437
+ const srcOff = ((srcOffsetY + y) * sw + srcOffsetX + x) * 6;
106438
+ const dstOff = ((y0 + y) * canvasWidth + x0 + x) * 6;
106439
+ if (effectiveOp >= 0.999) {
106440
+ source2.copy(canvas, dstOff, srcOff, srcOff + 6);
106441
+ } else {
106442
+ const invEff = 1 - effectiveOp;
106443
+ const sr = source2.readUInt16LE(srcOff);
106444
+ const sg = source2.readUInt16LE(srcOff + 2);
106445
+ const sb = source2.readUInt16LE(srcOff + 4);
106446
+ const dr = canvas.readUInt16LE(dstOff);
106447
+ const dg = canvas.readUInt16LE(dstOff + 2);
106448
+ const db = canvas.readUInt16LE(dstOff + 4);
106449
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
106450
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
106451
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
106452
+ }
106453
+ }
106454
+ }
106455
+ }
106456
+ }
106457
+ function blitRgb48leAffine(canvas, source2, matrix, srcW, srcH, canvasW, canvasH, opacity, borderRadius) {
106458
+ const a = matrix[0];
106459
+ const b = matrix[1];
106460
+ const c = matrix[2];
106461
+ const d = matrix[3];
106462
+ const tx = matrix[4];
106463
+ const ty = matrix[5];
106464
+ if (a === void 0 || b === void 0 || c === void 0 || d === void 0 || tx === void 0 || ty === void 0)
106465
+ return;
106466
+ const det = a * d - b * c;
106467
+ if (Math.abs(det) < 1e-10) return;
106468
+ const invA = d / det;
106469
+ const invB = -b / det;
106470
+ const invC = -c / det;
106471
+ const invD = a / det;
106472
+ const invTx = -(invA * tx + invC * ty);
106473
+ const invTy = -(invB * tx + invD * ty);
106474
+ const op = opacity ?? 1;
106475
+ const hasMask = borderRadius !== void 0;
106476
+ const corners = [
106477
+ [tx, ty],
106478
+ [a * srcW + tx, b * srcW + ty],
106479
+ [c * srcH + tx, d * srcH + ty],
106480
+ [a * srcW + c * srcH + tx, b * srcW + d * srcH + ty]
106481
+ ];
106482
+ let minX = canvasW, maxX = 0, minY = canvasH, maxY = 0;
106483
+ for (const corner of corners) {
106484
+ const cx = corner[0] ?? 0;
106485
+ const cy = corner[1] ?? 0;
106486
+ if (cx < minX) minX = cx;
106487
+ if (cx > maxX) maxX = cx;
106488
+ if (cy < minY) minY = cy;
106489
+ if (cy > maxY) maxY = cy;
106490
+ }
106491
+ const startX = Math.max(0, Math.floor(minX));
106492
+ const endX = Math.min(canvasW, Math.ceil(maxX));
106493
+ const startY = Math.max(0, Math.floor(minY));
106494
+ const endY = Math.min(canvasH, Math.ceil(maxY));
106495
+ for (let dy = startY; dy < endY; dy++) {
106496
+ for (let dx = startX; dx < endX; dx++) {
106497
+ const sx = invA * dx + invC * dy + invTx;
106498
+ const sy = invB * dx + invD * dy + invTy;
106499
+ if (sx < 0 || sy < 0 || sx >= srcW || sy >= srcH) continue;
106500
+ let effectiveOp = op;
106501
+ if (hasMask) {
106502
+ const ma = roundedRectAlpha(sx, sy, srcW, srcH, borderRadius);
106503
+ if (ma <= 0) continue;
106504
+ effectiveOp *= ma;
106505
+ }
106506
+ const x0 = Math.floor(sx);
106507
+ const y0 = Math.floor(sy);
106508
+ const fx = sx - x0;
106509
+ const fy = sy - y0;
106510
+ const x1 = Math.min(x0 + 1, srcW - 1);
106511
+ const y1 = Math.min(y0 + 1, srcH - 1);
106512
+ const off00 = (y0 * srcW + x0) * 6;
106513
+ const off10 = (y0 * srcW + x1) * 6;
106514
+ const off01 = (y1 * srcW + x0) * 6;
106515
+ const off11 = (y1 * srcW + x1) * 6;
106516
+ const w00 = (1 - fx) * (1 - fy);
106517
+ const w10 = fx * (1 - fy);
106518
+ const w01 = (1 - fx) * fy;
106519
+ const w11 = fx * fy;
106520
+ const sr = source2.readUInt16LE(off00) * w00 + source2.readUInt16LE(off10) * w10 + source2.readUInt16LE(off01) * w01 + source2.readUInt16LE(off11) * w11;
106521
+ const sg = source2.readUInt16LE(off00 + 2) * w00 + source2.readUInt16LE(off10 + 2) * w10 + source2.readUInt16LE(off01 + 2) * w01 + source2.readUInt16LE(off11 + 2) * w11;
106522
+ const sb = source2.readUInt16LE(off00 + 4) * w00 + source2.readUInt16LE(off10 + 4) * w10 + source2.readUInt16LE(off01 + 4) * w01 + source2.readUInt16LE(off11 + 4) * w11;
106523
+ const dstOff = (dy * canvasW + dx) * 6;
106524
+ if (effectiveOp >= 0.999) {
106525
+ canvas.writeUInt16LE(Math.round(sr), dstOff);
106526
+ canvas.writeUInt16LE(Math.round(sg), dstOff + 2);
106527
+ canvas.writeUInt16LE(Math.round(sb), dstOff + 4);
106528
+ } else {
106529
+ const invEff = 1 - effectiveOp;
106530
+ const dr = canvas.readUInt16LE(dstOff);
106531
+ const dg = canvas.readUInt16LE(dstOff + 2);
106532
+ const db = canvas.readUInt16LE(dstOff + 4);
106533
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
106534
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
106535
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
106536
+ }
106537
+ }
106538
+ }
106539
+ }
106540
+ function parseTransformMatrix(css) {
106541
+ if (!css || css === "none") return null;
106542
+ const match2 = css.match(
106543
+ /^matrix\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,)]+)\s*\)$/
106544
+ );
106545
+ if (!match2) return null;
106546
+ const values = match2.slice(1, 7).map(Number);
106547
+ if (!values.every(Number.isFinite)) return null;
106548
+ return values;
106549
+ }
106550
+
106551
+ // ../engine/src/utils/layerCompositor.ts
106552
+ function groupIntoLayers(elements) {
106553
+ const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
106554
+ const layers = [];
106555
+ for (const el of sorted) {
106556
+ if (el.isHdr) {
106557
+ layers.push({ type: "hdr", element: el });
106558
+ } else {
106559
+ const last2 = layers[layers.length - 1];
106560
+ if (last2 && last2.type === "dom") {
106561
+ last2.elementIds.push(el.id);
106562
+ } else {
106563
+ layers.push({ type: "dom", elementIds: [el.id] });
106564
+ }
106565
+ }
106566
+ }
106567
+ return layers;
106568
+ }
106569
+
106570
+ // ../engine/src/utils/shaderTransitions.ts
106571
+ var PQ_M1 = 0.1593017578125;
106572
+ var PQ_M2 = 78.84375;
106573
+ var PQ_C1 = 0.8359375;
106574
+ var PQ_C2 = 18.8515625;
106575
+ var PQ_C3 = 18.6875;
106576
+ function pqEotf(signal) {
106577
+ const sp = Math.pow(Math.max(0, signal), 1 / PQ_M2);
106578
+ const num = Math.max(sp - PQ_C1, 0);
106579
+ const den = PQ_C2 - PQ_C3 * sp;
106580
+ return den > 0 ? Math.pow(num / den, 1 / PQ_M1) : 0;
106581
+ }
106582
+ function pqOetf(linear) {
106583
+ const lp = Math.pow(Math.max(0, linear), PQ_M1);
106584
+ return Math.pow((PQ_C1 + PQ_C2 * lp) / (1 + PQ_C3 * lp), PQ_M2);
106585
+ }
106586
+ function hlgEotf(signal) {
106587
+ const a = 0.17883277;
106588
+ const b = 1 - 4 * a;
106589
+ const c = 0.5 - a * Math.log(4 * a);
106590
+ if (signal <= 0.5) {
106591
+ return signal * signal / 3;
106592
+ }
106593
+ return (Math.exp((signal - c) / a) + b) / 12;
106594
+ }
106595
+ function hlgOetf(linear) {
106596
+ const a = 0.17883277;
106597
+ const b = 1 - 4 * a;
106598
+ const c = 0.5 - a * Math.log(4 * a);
106599
+ if (linear <= 1 / 12) {
106600
+ return Math.sqrt(3 * linear);
106601
+ }
106602
+ return a * Math.log(12 * linear - b) + c;
106603
+ }
106604
+ function buildLut(fn) {
106605
+ const lut = new Uint16Array(65536);
106606
+ for (let i = 0; i < 65536; i++) {
106607
+ lut[i] = Math.round(fn(i / 65535) * 65535);
106608
+ }
106609
+ return lut;
106610
+ }
106611
+ var HLG_OOTF_LW = 1e3;
106612
+ var HLG_OOTF_GAMMA = 1.2 * Math.pow(1.111, Math.log2(HLG_OOTF_LW / 1e3));
106613
+ function hlgSceneToPqDisplay(sceneLinear) {
106614
+ const displayNits = HLG_OOTF_LW * Math.pow(Math.max(0, sceneLinear), HLG_OOTF_GAMMA);
106615
+ return displayNits / 1e4;
106616
+ }
106617
+ function pqDisplayToHlgScene(displayNormalized) {
106618
+ const displayNits = displayNormalized * 1e4;
106619
+ return Math.pow(Math.max(0, displayNits / HLG_OOTF_LW), 1 / HLG_OOTF_GAMMA);
106620
+ }
106621
+ var hlgToPqLut = null;
106622
+ var pqToHlgLut = null;
106623
+ function getHlgToPqLut() {
106624
+ if (!hlgToPqLut) hlgToPqLut = buildLut((v) => pqOetf(hlgSceneToPqDisplay(hlgEotf(v))));
106625
+ return hlgToPqLut;
106626
+ }
106627
+ function getPqToHlgLut() {
106628
+ if (!pqToHlgLut) pqToHlgLut = buildLut((v) => hlgOetf(pqDisplayToHlgScene(pqEotf(v))));
106629
+ return pqToHlgLut;
106630
+ }
106631
+ function convertTransfer(buf, from2, to) {
106632
+ if (from2 === to) return;
106633
+ const lut = from2 === "hlg" ? getHlgToPqLut() : getPqToHlgLut();
106634
+ const len = buf.length / 2;
106635
+ for (let i = 0; i < len; i++) {
106636
+ const off = i * 2;
106637
+ buf.writeUInt16LE(lut[buf.readUInt16LE(off)] ?? 0, off);
106638
+ }
106639
+ }
106640
+ function sampleRgb48le(buf, u, v, w, h) {
106641
+ const uc = Math.max(0, Math.min(1, u));
106642
+ const vc = Math.max(0, Math.min(1, v));
106643
+ const sx = uc * (w - 1);
106644
+ const sy = vc * (h - 1);
106645
+ const x0 = Math.floor(sx);
106646
+ const y0 = Math.floor(sy);
106647
+ const x1 = Math.min(x0 + 1, w - 1);
106648
+ const y1 = Math.min(y0 + 1, h - 1);
106649
+ const fx = sx - x0;
106650
+ const fy = sy - y0;
106651
+ const w00 = (1 - fx) * (1 - fy);
106652
+ const w10 = fx * (1 - fy);
106653
+ const w01 = (1 - fx) * fy;
106654
+ const w11 = fx * fy;
106655
+ const off00 = (y0 * w + x0) * 6;
106656
+ const off10 = (y0 * w + x1) * 6;
106657
+ const off01 = (y1 * w + x0) * 6;
106658
+ const off11 = (y1 * w + x1) * 6;
106659
+ const r = Math.round(
106660
+ buf.readUInt16LE(off00) * w00 + buf.readUInt16LE(off10) * w10 + buf.readUInt16LE(off01) * w01 + buf.readUInt16LE(off11) * w11
106661
+ );
106662
+ const g = Math.round(
106663
+ buf.readUInt16LE(off00 + 2) * w00 + buf.readUInt16LE(off10 + 2) * w10 + buf.readUInt16LE(off01 + 2) * w01 + buf.readUInt16LE(off11 + 2) * w11
106664
+ );
106665
+ const b = Math.round(
106666
+ buf.readUInt16LE(off00 + 4) * w00 + buf.readUInt16LE(off10 + 4) * w10 + buf.readUInt16LE(off01 + 4) * w01 + buf.readUInt16LE(off11 + 4) * w11
106667
+ );
106668
+ return [r, g, b];
106669
+ }
106670
+ function mix16(a, b, t) {
106671
+ return Math.round(a * (1 - t) + b * t);
106672
+ }
106673
+ function clamp16(v) {
106674
+ return Math.max(0, Math.min(65535, v));
106675
+ }
106676
+ function smoothstep(edge0, edge1, x) {
106677
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
106678
+ return t * t * (3 - 2 * t);
106679
+ }
106680
+ function hash(x, y) {
106681
+ return (Math.sin(x * 127.1 + y * 311.7) * 43758.5453 % 1 + 1) % 1;
106682
+ }
106683
+ function vnoise(px, py) {
106684
+ const ix = Math.floor(px);
106685
+ const iy = Math.floor(py);
106686
+ let fx = px - ix;
106687
+ let fy = py - iy;
106688
+ fx = fx * fx * fx * (fx * (fx * 6 - 15) + 10);
106689
+ fy = fy * fy * fy * (fy * (fy * 6 - 15) + 10);
106690
+ const h00 = hash(ix, iy);
106691
+ const h10 = hash(ix + 1, iy);
106692
+ const h01 = hash(ix, iy + 1);
106693
+ const h11 = hash(ix + 1, iy + 1);
106694
+ return h00 * (1 - fx) * (1 - fy) + h10 * fx * (1 - fy) + h01 * (1 - fx) * fy + h11 * fx * fy;
106695
+ }
106696
+ var ROT_A = 0.8;
106697
+ var ROT_B = 0.6;
106698
+ function fbm(px, py) {
106699
+ let value = 0;
106700
+ let amplitude = 0.5;
106701
+ let x = px;
106702
+ let y = py;
106703
+ for (let i = 0; i < 5; i++) {
106704
+ value += amplitude * vnoise(x, y);
106705
+ const nx = ROT_A * x - ROT_B * y;
106706
+ const ny = ROT_B * x + ROT_A * y;
106707
+ x = nx * 2.02;
106708
+ y = ny * 2.02;
106709
+ amplitude *= 0.5;
106710
+ }
106711
+ return value;
106712
+ }
106713
+ var TRANSITIONS = {};
106714
+ var crossfade = (from2, to, out, w, h, p) => {
106715
+ const inv = 1 - p;
106716
+ for (let i = 0; i < w * h; i++) {
106717
+ const o = i * 6;
106718
+ out.writeUInt16LE(Math.round(from2.readUInt16LE(o) * inv + to.readUInt16LE(o) * p), o);
106719
+ out.writeUInt16LE(
106720
+ Math.round(from2.readUInt16LE(o + 2) * inv + to.readUInt16LE(o + 2) * p),
106721
+ o + 2
106722
+ );
106723
+ out.writeUInt16LE(
106724
+ Math.round(from2.readUInt16LE(o + 4) * inv + to.readUInt16LE(o + 4) * p),
106725
+ o + 4
106726
+ );
106727
+ }
106728
+ };
106729
+ TRANSITIONS["crossfade"] = crossfade;
106730
+ var flashThroughWhite = (from2, to, out, w, h, p) => {
106731
+ const toWhite = smoothstep(0, 0.45, p);
106732
+ const fromWhite = 1 - smoothstep(0.5, 1, p);
106733
+ const blend = smoothstep(0.35, 0.65, p);
106734
+ for (let i = 0; i < w * h; i++) {
106735
+ const o = i * 6;
106736
+ const fromR = mix16(from2.readUInt16LE(o), 65535, toWhite);
106737
+ const fromG = mix16(from2.readUInt16LE(o + 2), 65535, toWhite);
106738
+ const fromB = mix16(from2.readUInt16LE(o + 4), 65535, toWhite);
106739
+ const toR = mix16(to.readUInt16LE(o), 65535, fromWhite);
106740
+ const toG = mix16(to.readUInt16LE(o + 2), 65535, fromWhite);
106741
+ const toB = mix16(to.readUInt16LE(o + 4), 65535, fromWhite);
106742
+ out.writeUInt16LE(mix16(fromR, toR, blend), o);
106743
+ out.writeUInt16LE(mix16(fromG, toG, blend), o + 2);
106744
+ out.writeUInt16LE(mix16(fromB, toB, blend), o + 4);
106745
+ }
106746
+ };
106747
+ TRANSITIONS["flash-through-white"] = flashThroughWhite;
106748
+ var chromaticSplit = (from2, to, out, w, h, p) => {
106749
+ for (let i = 0; i < w * h; i++) {
106750
+ const ux = i % w / w;
106751
+ const uy = Math.floor(i / w) / h;
106752
+ const o = i * 6;
106753
+ const cx = ux - 0.5;
106754
+ const cy = uy - 0.5;
106755
+ const fromShift = p * 0.06;
106756
+ const fr = sampleRgb48le(from2, ux + cx * fromShift, uy + cy * fromShift, w, h)[0];
106757
+ const fg = sampleRgb48le(from2, ux, uy, w, h)[1];
106758
+ const fb = sampleRgb48le(from2, ux - cx * fromShift, uy - cy * fromShift, w, h)[2];
106759
+ const toShift = (1 - p) * 0.06;
106760
+ const tr = sampleRgb48le(to, ux - cx * toShift, uy - cy * toShift, w, h)[0];
106761
+ const tg = sampleRgb48le(to, ux, uy, w, h)[1];
106762
+ const tb = sampleRgb48le(to, ux + cx * toShift, uy + cy * toShift, w, h)[2];
106763
+ out.writeUInt16LE(clamp16(mix16(fr, tr, p)), o);
106764
+ out.writeUInt16LE(clamp16(mix16(fg, tg, p)), o + 2);
106765
+ out.writeUInt16LE(clamp16(mix16(fb, tb, p)), o + 4);
106766
+ }
106767
+ };
106768
+ TRANSITIONS["chromatic-split"] = chromaticSplit;
106769
+ var sdfIris = (from2, to, out, w, h, p) => {
106770
+ const accentBright = [65535, 55e3, 35e3];
106771
+ for (let i = 0; i < w * h; i++) {
106772
+ const ux = i % w / w;
106773
+ const uy = Math.floor(i / w) / h;
106774
+ const o = i * 6;
106775
+ const ax = (ux - 0.5) * (w / h);
106776
+ const ay = uy - 0.5;
106777
+ const d = Math.sqrt(ax * ax + ay * ay);
106778
+ const radius = p * 1.2;
106779
+ const fw = 3e-3;
106780
+ const edge = smoothstep(radius + fw, radius - fw, d);
106781
+ const ring1 = Math.exp(-Math.abs(d - radius) * 25);
106782
+ const ring2 = Math.exp(-Math.abs(d - radius + 0.04) * 20) * 0.5;
106783
+ const ring3 = Math.exp(-Math.abs(d - radius + 0.08) * 15) * 0.25;
106784
+ const glow = (ring1 + ring2 + ring3) * p * (1 - p) * 4;
106785
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, ux, uy, w, h);
106786
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106787
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, edge) + accentBright[0] * glow * 0.6), o);
106788
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, edge) + accentBright[1] * glow * 0.6), o + 2);
106789
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, edge) + accentBright[2] * glow * 0.6), o + 4);
106790
+ }
106791
+ };
106792
+ TRANSITIONS["sdf-iris"] = sdfIris;
106793
+ function glitchRand(x, y) {
106794
+ return (Math.sin(x * 12.9898 + y * 78.233) * 43758.5453 % 1 + 1) % 1;
106795
+ }
106796
+ var glitch = (from2, to, out, w, h, p) => {
106797
+ const intensity = p * (1 - p) * 4;
106798
+ for (let i = 0; i < w * h; i++) {
106799
+ const ux = i % w / w;
106800
+ const uy = Math.floor(i / w) / h;
106801
+ const o = i * 6;
106802
+ const lineY = Math.floor(uy * 60) / 60;
106803
+ const lineDisp = (glitchRand(lineY, Math.floor(p * 17)) - 0.5) * 0.18 * intensity;
106804
+ const blockX = Math.floor(ux * 12);
106805
+ const blockY = Math.floor(uy * 8);
106806
+ const progressStep = Math.floor(p * 11);
106807
+ const br = glitchRand(blockX + progressStep, blockY + progressStep);
106808
+ const ba = (br >= 0.83 ? 1 : 0) * intensity;
106809
+ const bdx = (glitchRand(blockX * 2.1, blockY * 2.1) - 0.5) * 0.35 * ba;
106810
+ const bdy = (glitchRand(blockX * 3.7, blockY * 3.7) - 0.5) * 0.35 * ba;
106811
+ const uvx = Math.max(0, Math.min(1, ux + lineDisp + bdx));
106812
+ const uvy = Math.max(0, Math.min(1, uy + bdy));
106813
+ const shift = intensity * 0.035;
106814
+ const r = sampleRgb48le(from2, uvx + shift, uvy, w, h)[0];
106815
+ const g = sampleRgb48le(from2, uvx, uvy, w, h)[1];
106816
+ const b = sampleRgb48le(from2, uvx - shift, uvy, w, h)[2];
106817
+ let cr = r / 65535;
106818
+ let cg = g / 65535;
106819
+ let cb = b / 65535;
106820
+ const scanline = (uy * h * 0.5 % 1 + 1) % 1 >= 0.5 ? 0.05 * intensity : 0;
106821
+ cr -= scanline;
106822
+ cg -= scanline;
106823
+ cb -= scanline;
106824
+ const flicker = 1 + (glitchRand(Math.floor(p * 23), 0) - 0.5) * 0.3 * intensity;
106825
+ cr *= flicker;
106826
+ cg *= flicker;
106827
+ cb *= flicker;
106828
+ const levels = 256 - (256 - 8) * (intensity * 0.5);
106829
+ cr = Math.floor(cr * levels) / levels;
106830
+ cg = Math.floor(cg * levels) / levels;
106831
+ cb = Math.floor(cb * levels) / levels;
106832
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106833
+ out.writeUInt16LE(clamp16(mix16(Math.round(cr * 65535), toR, p)), o);
106834
+ out.writeUInt16LE(clamp16(mix16(Math.round(cg * 65535), toG, p)), o + 2);
106835
+ out.writeUInt16LE(clamp16(mix16(Math.round(cb * 65535), toB, p)), o + 4);
106836
+ }
106837
+ };
106838
+ TRANSITIONS["glitch"] = glitch;
106839
+ function aces(x) {
106840
+ return Math.max(0, Math.min(1, x * (2.51 * x + 0.03) / (x * (2.43 * x + 0.59) + 0.14)));
106841
+ }
106842
+ var lightLeak = (from2, to, out, w, h, p) => {
106843
+ const accent = [5e4 / 65535, 25e3 / 65535, 5e3 / 65535];
106844
+ const accentBright = [65535 / 65535, 55e3 / 65535, 35e3 / 65535];
106845
+ const lpx = 1.3;
106846
+ const lpy = -0.2;
106847
+ for (let i = 0; i < w * h; i++) {
106848
+ const ux = i % w / w;
106849
+ const uy = Math.floor(i / w) / h;
106850
+ const o = i * 6;
106851
+ const dx = ux - lpx;
106852
+ const dy = uy - lpy;
106853
+ const dist = Math.sqrt(dx * dx + dy * dy);
106854
+ const leak = Math.max(0, Math.min(1, Math.exp(-dist * 1.8) * p * 4));
106855
+ const warmR = accent[0] + (accentBright[0] - accent[0]) * dist * 0.7;
106856
+ const warmG = accent[1] + (accentBright[1] - accent[1]) * dist * 0.7;
106857
+ const warmB = accent[2] + (accentBright[2] - accent[2]) * dist * 0.7;
106858
+ const flare = Math.exp(-Math.abs(uy - (-0.2 + ux * 0.3)) * 15) * leak * 0.3;
106859
+ const [fr, fg, fb] = sampleRgb48le(from2, ux, uy, w, h);
106860
+ const fromR = fr / 65535;
106861
+ const fromG = fg / 65535;
106862
+ const fromB = fb / 65535;
106863
+ const overR = aces(fromR + warmR * leak * 3 + accentBright[0] * flare);
106864
+ const overG = aces(fromG + warmG * leak * 3 + accentBright[1] * flare);
106865
+ const overB = aces(fromB + warmB * leak * 3 + accentBright[2] * flare);
106866
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
106867
+ const blend = smoothstep(0.15, 0.85, p);
106868
+ out.writeUInt16LE(clamp16(mix16(Math.round(overR * 65535), toR, blend)), o);
106869
+ out.writeUInt16LE(clamp16(mix16(Math.round(overG * 65535), toG, blend)), o + 2);
106870
+ out.writeUInt16LE(clamp16(mix16(Math.round(overB * 65535), toB, blend)), o + 4);
106871
+ }
106872
+ };
106873
+ TRANSITIONS["light-leak"] = lightLeak;
106874
+ var crossWarpMorph = (from2, to, out, w, h, p) => {
106875
+ for (let i = 0; i < w * h; i++) {
106876
+ const ux = i % w / w;
106877
+ const uy = Math.floor(i / w) / h;
106878
+ const o = i * 6;
106879
+ const dispX = fbm(ux * 3, uy * 3) - 0.5;
106880
+ const dispY = fbm(ux * 3 + 7.3, uy * 3 + 3.7) - 0.5;
106881
+ const fromUx = Math.max(0, Math.min(1, ux + dispX * p * 0.5));
106882
+ const fromUy = Math.max(0, Math.min(1, uy + dispY * p * 0.5));
106883
+ const toUx = Math.max(0, Math.min(1, ux - dispX * (1 - p) * 0.5));
106884
+ const toUy = Math.max(0, Math.min(1, uy - dispY * (1 - p) * 0.5));
106885
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
106886
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
106887
+ const n = fbm(ux * 4 + 3.1, uy * 4 + 1.7);
106888
+ const blend = smoothstep(0.4, 0.6, n + p * 1.2 - 0.6);
106889
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, blend)), o);
106890
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, blend)), o + 2);
106891
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, blend)), o + 4);
106892
+ }
106893
+ };
106894
+ TRANSITIONS["cross-warp-morph"] = crossWarpMorph;
106895
+ var whipPan = (from2, to, out, w, h, p) => {
106896
+ const fromOff = p * 1.5;
106897
+ const toOff = (1 - p) * 1.5;
106898
+ for (let i = 0; i < w * h; i++) {
106899
+ const ux = i % w / w;
106900
+ const uy = Math.floor(i / w) / h;
106901
+ const o = i * 6;
106902
+ let fromR = 0, fromG = 0, fromB = 0;
106903
+ for (let s = 0; s < 10; s++) {
106904
+ const f = s / 10;
106905
+ const fuv = Math.max(0, Math.min(1, ux + fromOff + p * 0.08 * f));
106906
+ const [r, g, b] = sampleRgb48le(from2, fuv, uy, w, h);
106907
+ fromR += r;
106908
+ fromG += g;
106909
+ fromB += b;
106910
+ }
106911
+ fromR /= 10;
106912
+ fromG /= 10;
106913
+ fromB /= 10;
106914
+ let toR = 0, toG = 0, toB = 0;
106915
+ for (let s = 0; s < 10; s++) {
106916
+ const f = s / 10;
106917
+ const tuv = Math.max(0, Math.min(1, ux - toOff - (1 - p) * 0.08 * f));
106918
+ const [r, g, b] = sampleRgb48le(to, tuv, uy, w, h);
106919
+ toR += r;
106920
+ toG += g;
106921
+ toB += b;
106922
+ }
106923
+ toR /= 10;
106924
+ toG /= 10;
106925
+ toB /= 10;
106926
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromR), Math.round(toR), p)), o);
106927
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromG), Math.round(toG), p)), o + 2);
106928
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromB), Math.round(toB), p)), o + 4);
106929
+ }
106930
+ };
106931
+ TRANSITIONS["whip-pan"] = whipPan;
106932
+ var cinematicZoom = (from2, to, out, w, h, p) => {
106933
+ const fromS = p * 0.08;
106934
+ const toS = (1 - p) * 0.06;
106935
+ for (let i = 0; i < w * h; i++) {
106936
+ const ux = i % w / w;
106937
+ const uy = Math.floor(i / w) / h;
106938
+ const o = i * 6;
106939
+ const dx = ux - 0.5;
106940
+ const dy = uy - 0.5;
106941
+ let fr = 0, fg = 0, fb = 0;
106942
+ for (let s = 0; s < 12; s++) {
106943
+ const f = s / 12;
106944
+ const rr = sampleRgb48le(
106945
+ from2,
106946
+ ux - dx * fromS * 1.06 * f,
106947
+ uy - dy * fromS * 1.06 * f,
106948
+ w,
106949
+ h
106950
+ )[0];
106951
+ const gg = sampleRgb48le(from2, ux - dx * fromS * f, uy - dy * fromS * f, w, h)[1];
106952
+ const bb = sampleRgb48le(
106953
+ from2,
106954
+ ux - dx * fromS * 0.94 * f,
106955
+ uy - dy * fromS * 0.94 * f,
106956
+ w,
106957
+ h
106958
+ )[2];
106959
+ fr += rr;
106960
+ fg += gg;
106961
+ fb += bb;
106962
+ }
106963
+ fr /= 12;
106964
+ fg /= 12;
106965
+ fb /= 12;
106966
+ let tr = 0, tg = 0, tb = 0;
106967
+ for (let s = 0; s < 12; s++) {
106968
+ const f = s / 12;
106969
+ const rr = sampleRgb48le(to, ux + dx * toS * 1.06 * f, uy + dy * toS * 1.06 * f, w, h)[0];
106970
+ const gg = sampleRgb48le(to, ux + dx * toS * f, uy + dy * toS * f, w, h)[1];
106971
+ const bb = sampleRgb48le(to, ux + dx * toS * 0.94 * f, uy + dy * toS * 0.94 * f, w, h)[2];
106972
+ tr += rr;
106973
+ tg += gg;
106974
+ tb += bb;
106975
+ }
106976
+ tr /= 12;
106977
+ tg /= 12;
106978
+ tb /= 12;
106979
+ out.writeUInt16LE(clamp16(mix16(Math.round(fr), Math.round(tr), p)), o);
106980
+ out.writeUInt16LE(clamp16(mix16(Math.round(fg), Math.round(tg), p)), o + 2);
106981
+ out.writeUInt16LE(clamp16(mix16(Math.round(fb), Math.round(tb), p)), o + 4);
106982
+ }
106983
+ };
106984
+ TRANSITIONS["cinematic-zoom"] = cinematicZoom;
106985
+ var gravitationalLens = (from2, to, out, w, h, p) => {
106986
+ for (let i = 0; i < w * h; i++) {
106987
+ const ux = i % w / w;
106988
+ const uy = Math.floor(i / w) / h;
106989
+ const o = i * 6;
106990
+ const uvx = ux - 0.5;
106991
+ const uvy = uy - 0.5;
106992
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
106993
+ const pull = p * 2;
106994
+ const warpStr = pull * 0.3 / (dist + 0.1);
106995
+ const warpedX = Math.max(0, Math.min(1, ux - uvx * warpStr));
106996
+ const warpedY = Math.max(0, Math.min(1, uy - uvy * warpStr));
106997
+ const [, ag] = sampleRgb48le(from2, warpedX, warpedY, w, h);
106998
+ const horizon = smoothstep(0, 0.3, dist / (1 - p * 0.85 + 1e-3));
106999
+ const shift = pull * 0.02 / (dist + 0.2);
107000
+ const rSampX = Math.max(0, Math.min(1, ux - uvx * (warpStr + shift)));
107001
+ const rSampY = Math.max(0, Math.min(1, uy - uvy * (warpStr + shift)));
107002
+ const bSampX = Math.max(0, Math.min(1, ux - uvx * (warpStr - shift)));
107003
+ const bSampY = Math.max(0, Math.min(1, uy - uvy * (warpStr - shift)));
107004
+ const ar = sampleRgb48le(from2, rSampX, rSampY, w, h)[0];
107005
+ const ab = sampleRgb48le(from2, bSampX, bSampY, w, h)[2];
107006
+ const lensedR = Math.round(ar * horizon);
107007
+ const lensedG = Math.round(ag * horizon);
107008
+ const lensedB = Math.round(ab * horizon);
107009
+ const [toR, toG, toB] = sampleRgb48le(to, ux, uy, w, h);
107010
+ const blend = smoothstep(0.3, 0.9, p);
107011
+ out.writeUInt16LE(clamp16(mix16(lensedR, toR, blend)), o);
107012
+ out.writeUInt16LE(clamp16(mix16(lensedG, toG, blend)), o + 2);
107013
+ out.writeUInt16LE(clamp16(mix16(lensedB, toB, blend)), o + 4);
107014
+ }
107015
+ };
107016
+ TRANSITIONS["gravitational-lens"] = gravitationalLens;
107017
+ var rippleWaves = (from2, to, out, w, h, p) => {
107018
+ const accentBright = [65535, 55e3, 35e3];
107019
+ for (let i = 0; i < w * h; i++) {
107020
+ const ux = i % w / w;
107021
+ const uy = Math.floor(i / w) / h;
107022
+ const o = i * 6;
107023
+ const uvx = ux - 0.5;
107024
+ const uvy = uy - 0.5;
107025
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107026
+ const nux = uvx + 1e-3;
107027
+ const nuy = uvy + 1e-3;
107028
+ const nlen = Math.sqrt(nux * nux + nuy * nuy);
107029
+ const dirx = nux / nlen;
107030
+ const diry = nuy / nlen;
107031
+ const fromAmp = p * 0.04;
107032
+ const fw1 = Math.exp(Math.sin(dist * 25 - p * 12) - 1);
107033
+ const fw2 = Math.exp(Math.sin(dist * 50 - p * 18) - 1) * 0.5;
107034
+ const fromUx = Math.max(0, Math.min(1, ux + dirx * (fw1 + fw2) * fromAmp));
107035
+ const fromUy = Math.max(0, Math.min(1, uy + diry * (fw1 + fw2) * fromAmp));
107036
+ const toAmp = (1 - p) * 0.04;
107037
+ const tw1 = Math.exp(Math.sin(dist * 25 + p * 12) - 1);
107038
+ const tw2 = Math.exp(Math.sin(dist * 50 + p * 18) - 1) * 0.5;
107039
+ const toUx = Math.max(0, Math.min(1, ux - dirx * (tw1 + tw2) * toAmp));
107040
+ const toUy = Math.max(0, Math.min(1, uy - diry * (tw1 + tw2) * toAmp));
107041
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107042
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107043
+ const peak = fw1 * p;
107044
+ const tintR = accentBright[0] * peak * 0.1;
107045
+ const tintG = accentBright[1] * peak * 0.1;
107046
+ const tintB = accentBright[2] * peak * 0.1;
107047
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromR + tintR), toR, p)), o);
107048
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromG + tintG), toG, p)), o + 2);
107049
+ out.writeUInt16LE(clamp16(mix16(Math.round(fromB + tintB), toB, p)), o + 4);
107050
+ }
107051
+ };
107052
+ TRANSITIONS["ripple-waves"] = rippleWaves;
107053
+ var swirlVortex = (from2, to, out, w, h, p) => {
107054
+ for (let i = 0; i < w * h; i++) {
107055
+ const ux = i % w / w;
107056
+ const uy = Math.floor(i / w) / h;
107057
+ const o = i * 6;
107058
+ const uvx = ux - 0.5;
107059
+ const uvy = uy - 0.5;
107060
+ const dist = Math.sqrt(uvx * uvx + uvy * uvy);
107061
+ const warp = fbm(ux * 4, uy * 4) * 0.5;
107062
+ const fromAng = p * (1 - dist) * 10 + warp * p * 3;
107063
+ const fs8 = Math.sin(fromAng);
107064
+ const fc = Math.cos(fromAng);
107065
+ const fromUx = Math.max(0, Math.min(1, uvx * fc - uvy * fs8 + 0.5));
107066
+ const fromUy = Math.max(0, Math.min(1, uvx * fs8 + uvy * fc + 0.5));
107067
+ const toAng = -(1 - p) * (1 - dist) * 10 - warp * (1 - p) * 3;
107068
+ const ts = Math.sin(toAng);
107069
+ const tc = Math.cos(toAng);
107070
+ const toUx = Math.max(0, Math.min(1, uvx * tc - uvy * ts + 0.5));
107071
+ const toUy = Math.max(0, Math.min(1, uvx * ts + uvy * tc + 0.5));
107072
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, fromUy, w, h);
107073
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, toUy, w, h);
107074
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, p)), o);
107075
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, p)), o + 2);
107076
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, p)), o + 4);
107077
+ }
107078
+ };
107079
+ TRANSITIONS["swirl-vortex"] = swirlVortex;
107080
+ var thermalDistortion = (from2, to, out, w, h, p) => {
107081
+ const accentBright = [65535, 55e3, 35e3];
107082
+ for (let i = 0; i < w * h; i++) {
107083
+ const ux = i % w / w;
107084
+ const uy = Math.floor(i / w) / h;
107085
+ const o = i * 6;
107086
+ const heat = p * 1.5;
107087
+ const yFade = smoothstep(1, 0, uy);
107088
+ const shimmer = Math.sin(uy * 40 + fbm(ux * 6, uy * 6) * 8) * fbm(ux * 3 + 0, uy * 3 + p * 2);
107089
+ const dispX = shimmer * heat * 0.03 * yFade;
107090
+ const fromUx = Math.max(0, Math.min(1, ux + dispX));
107091
+ const [fromR, fromG, fromB] = sampleRgb48le(from2, fromUx, uy, w, h);
107092
+ const invShimmer = Math.sin(uy * 40 + fbm(ux * 6 + 3, uy * 6 + 3) * 8) * fbm(ux * 3 + 3, uy * 3 + p * 2);
107093
+ const dispX2 = invShimmer * (1 - p) * 0.03 * yFade;
107094
+ const toUx = Math.max(0, Math.min(1, ux + dispX2));
107095
+ const [toR, toG, toB] = sampleRgb48le(to, toUx, uy, w, h);
107096
+ const haze = heat * yFade * 0.15 * (1 - p);
107097
+ out.writeUInt16LE(clamp16(mix16(fromR, toR, p) + Math.round(accentBright[0] * haze)), o);
107098
+ out.writeUInt16LE(clamp16(mix16(fromG, toG, p) + Math.round(accentBright[1] * haze)), o + 2);
107099
+ out.writeUInt16LE(clamp16(mix16(fromB, toB, p) + Math.round(accentBright[2] * haze)), o + 4);
107100
+ }
107101
+ };
107102
+ TRANSITIONS["thermal-distortion"] = thermalDistortion;
107103
+ var domainWarp = (from2, to, out, w, h, p) => {
107104
+ const accentDark = [25e3, 8e3, 2e3];
107105
+ const accentBright = [65535, 55e3, 35e3];
107106
+ for (let i = 0; i < w * h; i++) {
107107
+ const ux = i % w / w;
107108
+ const uy = Math.floor(i / w) / h;
107109
+ const o = i * 6;
107110
+ const qx = fbm(ux * 3, uy * 3);
107111
+ const qy = fbm(ux * 3 + 5.2, uy * 3 + 1.3);
107112
+ const rx = fbm(ux * 3 + qx * 4 + 1.7, uy * 3 + qy * 4 + 9.2);
107113
+ const ry = fbm(ux * 3 + qx * 4 + 8.3, uy * 3 + qy * 4 + 2.8);
107114
+ const n = fbm(ux * 3 + rx * 2, uy * 3 + ry * 2);
107115
+ const warpDirX = (qx - 0.5) * 0.4;
107116
+ const warpDirY = (qy - 0.5) * 0.4;
107117
+ const aUx = Math.max(0, Math.min(1, ux + warpDirX * p));
107118
+ const aUy = Math.max(0, Math.min(1, uy + warpDirY * p));
107119
+ const bUx = Math.max(0, Math.min(1, ux - warpDirX * (1 - p)));
107120
+ const bUy = Math.max(0, Math.min(1, uy - warpDirY * (1 - p)));
107121
+ const [aR, aG, aB] = sampleRgb48le(from2, aUx, aUy, w, h);
107122
+ const [bR, bG, bB] = sampleRgb48le(to, bUx, bUy, w, h);
107123
+ const e = smoothstep(p - 0.08, p + 0.08, n);
107124
+ const ed = Math.abs(n - p);
107125
+ const pStep = p >= 1 ? 1 : 0;
107126
+ const em = smoothstep(0.1, 0, ed) * (1 - pStep);
107127
+ const ecBlend = smoothstep(0, 0.1, ed);
107128
+ const ecR = accentDark[0] + (accentBright[0] - accentDark[0]) * (1 - ecBlend);
107129
+ const ecG = accentDark[1] + (accentBright[1] - accentDark[1]) * (1 - ecBlend);
107130
+ const ecB = accentDark[2] + (accentBright[2] - accentDark[2]) * (1 - ecBlend);
107131
+ out.writeUInt16LE(clamp16(mix16(bR, aR, e) + Math.round(ecR * em * 2)), o);
107132
+ out.writeUInt16LE(clamp16(mix16(bG, aG, e) + Math.round(ecG * em * 2)), o + 2);
107133
+ out.writeUInt16LE(clamp16(mix16(bB, aB, e) + Math.round(ecB * em * 2)), o + 4);
107134
+ }
107135
+ };
107136
+ TRANSITIONS["domain-warp"] = domainWarp;
107137
+ function ridged(px, py) {
107138
+ let value = 0;
107139
+ let amplitude = 0.5;
107140
+ let x = px;
107141
+ let y = py;
107142
+ for (let i = 0; i < 5; i++) {
107143
+ value += amplitude * Math.abs(vnoise(x, y) * 2 - 1);
107144
+ const nx = ROT_A * x - ROT_B * y;
107145
+ const ny = ROT_B * x + ROT_A * y;
107146
+ x = nx * 2.02;
107147
+ y = ny * 2.02;
107148
+ amplitude *= 0.5;
107149
+ }
107150
+ return value;
107151
+ }
107152
+ var ridgedBurn = (from2, to, out, w, h, p) => {
107153
+ const accent = [5e4, 25e3, 5e3];
107154
+ const accentDark = [25e3, 8e3, 2e3];
107155
+ const accentBright = [65535, 55e3, 35e3];
107156
+ for (let i = 0; i < w * h; i++) {
107157
+ const ux = i % w / w;
107158
+ const uy = Math.floor(i / w) / h;
107159
+ const o = i * 6;
107160
+ const [aR, aG, aB] = sampleRgb48le(from2, ux, uy, w, h);
107161
+ const [bR, bG, bB] = sampleRgb48le(to, ux, uy, w, h);
107162
+ const n = ridged(ux * 4, uy * 4);
107163
+ const e = smoothstep(p - 0.04, p + 0.04, n);
107164
+ const heat = smoothstep(0.12, 0, Math.abs(n - p));
107165
+ const pStep = p >= 1 ? 1 : 0;
107166
+ const heatMasked = heat * (1 - pStep);
107167
+ let burnR = accentDark[0] + (accent[0] - accentDark[0]) * smoothstep(0, 0.25, heatMasked);
107168
+ let burnG = accentDark[1] + (accent[1] - accentDark[1]) * smoothstep(0, 0.25, heatMasked);
107169
+ let burnB = accentDark[2] + (accent[2] - accentDark[2]) * smoothstep(0, 0.25, heatMasked);
107170
+ const blend2 = smoothstep(0.25, 0.5, heatMasked);
107171
+ burnR = burnR + (accentBright[0] - burnR) * blend2;
107172
+ burnG = burnG + (accentBright[1] - burnG) * blend2;
107173
+ burnB = burnB + (accentBright[2] - burnB) * blend2;
107174
+ const blend3 = smoothstep(0.5, 1, heatMasked);
107175
+ burnR = burnR + (65535 - burnR) * blend3;
107176
+ burnG = burnG + (65535 - burnG) * blend3;
107177
+ burnB = burnB + (65535 - burnB) * blend3;
107178
+ const sparks = (vnoise(ux * 80, uy * 80) >= 0.92 ? 1 : 0) * heatMasked * 3;
107179
+ out.writeUInt16LE(
107180
+ clamp16(
107181
+ mix16(bR, aR, e) + Math.round(burnR * heatMasked * 3.5) + Math.round(accentBright[0] * sparks)
107182
+ ),
107183
+ o
107184
+ );
107185
+ out.writeUInt16LE(
107186
+ clamp16(
107187
+ mix16(bG, aG, e) + Math.round(burnG * heatMasked * 3.5) + Math.round(accentBright[1] * sparks)
107188
+ ),
107189
+ o + 2
107190
+ );
107191
+ out.writeUInt16LE(
107192
+ clamp16(
107193
+ mix16(bB, aB, e) + Math.round(burnB * heatMasked * 3.5) + Math.round(accentBright[2] * sparks)
107194
+ ),
107195
+ o + 4
107196
+ );
107197
+ }
107198
+ };
107199
+ TRANSITIONS["ridged-burn"] = ridgedBurn;
107200
+
105789
107201
  // src/services/renderOrchestrator.ts
105790
107202
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
105791
107203
  import { randomUUID } from "crypto";
@@ -106106,6 +107518,10 @@ var RENDER_MODE_SCRIPT = `(function() {
106106
107518
  }
106107
107519
  waitForPlayer();
106108
107520
  })();`;
107521
+ var HF_EARLY_STUB = `(function() {
107522
+ if (typeof window === "undefined") return;
107523
+ if (!window.__hf) window.__hf = {};
107524
+ })();`;
106109
107525
  var HF_BRIDGE_SCRIPT = `(function() {
106110
107526
  var __realSetInterval =
106111
107527
  window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
@@ -106151,20 +107567,24 @@ var HF_BRIDGE_SCRIPT = `(function() {
106151
107567
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
106152
107568
  return false;
106153
107569
  }
106154
- window.__hf = {
106155
- get duration() {
107570
+ var hf = window.__hf || {};
107571
+ Object.defineProperty(hf, "duration", {
107572
+ configurable: true,
107573
+ enumerable: true,
107574
+ get: function() {
106156
107575
  var d = p.getDuration();
106157
107576
  return d > 0 ? d : getDeclaredDuration();
106158
107577
  },
106159
- seek: function(t) {
106160
- p.renderSeek(t);
106161
- var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
106162
- if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
106163
- window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106164
- }
106165
- seekSameOriginChildFrames(window, nextTimeMs);
106166
- },
107578
+ });
107579
+ hf.seek = function(t) {
107580
+ p.renderSeek(t);
107581
+ var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
107582
+ if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
107583
+ window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
107584
+ }
107585
+ seekSameOriginChildFrames(window, nextTimeMs);
106167
107586
  };
107587
+ window.__hf = hf;
106168
107588
  return true;
106169
107589
  }
106170
107590
  if (bridge()) return;
@@ -106246,7 +107666,7 @@ ${headTags}`);
106246
107666
  }
106247
107667
  function createFileServer2(options) {
106248
107668
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
106249
- const preHeadScripts = options.preHeadScripts ?? [];
107669
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
106250
107670
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
106251
107671
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
106252
107672
  const app = new Hono2();
@@ -107506,6 +108926,24 @@ async function safeCleanup(label, fn, log = defaultLogger) {
107506
108926
  });
107507
108927
  }
107508
108928
  }
108929
+ var frameDirMaxIndexCache = /* @__PURE__ */ new Map();
108930
+ var FRAME_FILENAME_RE = /^frame_(\d+)\.png$/;
108931
+ function getMaxFrameIndex(frameDir) {
108932
+ const cached = frameDirMaxIndexCache.get(frameDir);
108933
+ if (cached !== void 0) return cached;
108934
+ let max = 0;
108935
+ try {
108936
+ for (const name of readdirSync6(frameDir)) {
108937
+ const m = FRAME_FILENAME_RE.exec(name);
108938
+ if (!m) continue;
108939
+ const n = Number(m[1]);
108940
+ if (Number.isFinite(n) && n > max) max = n;
108941
+ }
108942
+ } catch {
108943
+ }
108944
+ frameDirMaxIndexCache.set(frameDir, max);
108945
+ return max;
108946
+ }
107509
108947
  var RenderCancelledError = class extends Error {
107510
108948
  reason;
107511
108949
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -107607,6 +109045,63 @@ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
107607
109045
  reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
107608
109046
  });
107609
109047
  }
109048
+ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer) {
109049
+ const frameDir = hdrFrameDirs.get(el.id);
109050
+ const startTime = hdrStartTimes.get(el.id);
109051
+ if (!frameDir || startTime === void 0) {
109052
+ return;
109053
+ }
109054
+ const videoFrameIndex = Math.round((time - startTime) * fps) + 1;
109055
+ if (videoFrameIndex < 1) return;
109056
+ const maxIndex = getMaxFrameIndex(frameDir);
109057
+ const effectiveIndex = maxIndex > 0 ? Math.min(videoFrameIndex, maxIndex) : videoFrameIndex;
109058
+ const framePath = join15(frameDir, `frame_${String(effectiveIndex).padStart(4, "0")}.png`);
109059
+ if (!existsSync15(framePath)) {
109060
+ return;
109061
+ }
109062
+ try {
109063
+ const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync9(framePath));
109064
+ if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109065
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109066
+ }
109067
+ const viewportMatrix = parseTransformMatrix(el.transform);
109068
+ const br = el.borderRadius;
109069
+ const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
109070
+ const borderRadiusParam = hasBorderRadius ? br : void 0;
109071
+ if (viewportMatrix) {
109072
+ blitRgb48leAffine(
109073
+ canvas,
109074
+ hdrRgb,
109075
+ viewportMatrix,
109076
+ srcW,
109077
+ srcH,
109078
+ width,
109079
+ height,
109080
+ el.opacity < 0.999 ? el.opacity : void 0,
109081
+ borderRadiusParam
109082
+ );
109083
+ } else {
109084
+ blitRgb48leRegion(
109085
+ canvas,
109086
+ hdrRgb,
109087
+ el.x,
109088
+ el.y,
109089
+ srcW,
109090
+ srcH,
109091
+ width,
109092
+ height,
109093
+ el.opacity < 0.999 ? el.opacity : void 0,
109094
+ borderRadiusParam
109095
+ );
109096
+ }
109097
+ } catch (err) {
109098
+ if (log) {
109099
+ log.debug(`HDR blit failed for ${el.id}`, {
109100
+ error: err instanceof Error ? err.message : String(err)
109101
+ });
109102
+ }
109103
+ }
109104
+ }
107610
109105
  function createRenderJob(config2) {
107611
109106
  return {
107612
109107
  id: randomUUID(),
@@ -107877,6 +109372,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107877
109372
  perfStages.browserProbeMs = Date.now() - probeStart;
107878
109373
  job.duration = composition.duration;
107879
109374
  job.totalFrames = Math.ceil(composition.duration * job.config.fps);
109375
+ const totalFrames = job.totalFrames;
107880
109376
  if (job.duration <= 0) {
107881
109377
  const diagnostics = [];
107882
109378
  try {
@@ -107905,7 +109401,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107905
109401
  }
107906
109402
  }
107907
109403
  }
107908
- } catch {
109404
+ } catch (err) {
109405
+ log.warn("Failed to gather browser diagnostics for zero-duration composition", {
109406
+ error: err instanceof Error ? err.message : String(err)
109407
+ });
107909
109408
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
107910
109409
  }
107911
109410
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -107929,8 +109428,28 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107929
109428
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107930
109429
  let frameLookup = null;
107931
109430
  const compiledDir = join15(workDir, "compiled");
109431
+ let extractionResult = null;
109432
+ const nativeHdrVideoIds = /* @__PURE__ */ new Set();
109433
+ const videoTransfers = /* @__PURE__ */ new Map();
109434
+ if (job.config.hdr && composition.videos.length > 0) {
109435
+ await Promise.all(
109436
+ composition.videos.map(async (v) => {
109437
+ let videoPath = v.src;
109438
+ if (!videoPath.startsWith("/")) {
109439
+ const fromCompiled = existsSync15(join15(compiledDir, videoPath)) ? join15(compiledDir, videoPath) : join15(projectDir, videoPath);
109440
+ videoPath = fromCompiled;
109441
+ }
109442
+ if (!existsSync15(videoPath)) return;
109443
+ const meta = await extractVideoMetadata(videoPath);
109444
+ if (isHdrColorSpace(meta.colorSpace)) {
109445
+ nativeHdrVideoIds.add(v.id);
109446
+ videoTransfers.set(v.id, detectTransfer(meta.colorSpace));
109447
+ }
109448
+ })
109449
+ );
109450
+ }
107932
109451
  if (composition.videos.length > 0) {
107933
- const extractionResult = await extractAllVideoFrames(
109452
+ extractionResult = await extractAllVideoFrames(
107934
109453
  composition.videos,
107935
109454
  projectDir,
107936
109455
  { fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
@@ -107965,6 +109484,29 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107965
109484
  } else {
107966
109485
  perfStages.videoExtractMs = Date.now() - stage2Start;
107967
109486
  }
109487
+ let effectiveHdr;
109488
+ if (job.config.hdr && frameLookup) {
109489
+ const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
109490
+ const info = analyzeCompositionHdr(colorSpaces);
109491
+ if (info.hasHdr && info.dominantTransfer) {
109492
+ effectiveHdr = { transfer: info.dominantTransfer };
109493
+ }
109494
+ }
109495
+ if (job.config.hdr && !effectiveHdr && nativeHdrVideoIds.size > 0) {
109496
+ const firstTransfer = videoTransfers.values().next().value;
109497
+ if (firstTransfer) {
109498
+ effectiveHdr = { transfer: firstTransfer };
109499
+ }
109500
+ }
109501
+ if (effectiveHdr && outputFormat !== "mp4") {
109502
+ log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
109503
+ effectiveHdr = void 0;
109504
+ }
109505
+ if (effectiveHdr) {
109506
+ log.info(
109507
+ `[Render] HDR source detected \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
109508
+ );
109509
+ }
107968
109510
  const stage3Start = Date.now();
107969
109511
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
107970
109512
  const audioOutputPath = join15(workDir, "audio.aac");
@@ -108006,222 +109548,655 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108006
109548
  format: needsAlpha ? "png" : "jpeg",
108007
109549
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
108008
109550
  };
108009
- const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
109551
+ const workerCount = calculateOptimalWorkers(totalFrames, job.config.workers, cfg);
108010
109552
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
108011
109553
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
108012
109554
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
108013
- const preset = getEncoderPreset(job.config.quality, outputFormat);
108014
- const effectiveQuality = job.config.crf ?? preset.quality;
108015
- const effectiveBitrate = job.config.videoBitrate;
108016
- const baseEncoderOpts = {
108017
- fps: job.config.fps,
108018
- width,
108019
- height,
108020
- codec: preset.codec,
108021
- preset: preset.preset,
108022
- quality: effectiveQuality,
108023
- bitrate: effectiveBitrate,
108024
- pixelFormat: preset.pixelFormat,
108025
- useGpu: job.config.useGpu
108026
- };
109555
+ const hasHdrContent = effectiveHdr && nativeHdrVideoIds.size > 0;
109556
+ const encoderHdr = hasHdrContent ? effectiveHdr : void 0;
109557
+ const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
108027
109558
  job.framesRendered = 0;
108028
- let streamingEncoder = null;
108029
- if (enableStreamingEncode) {
108030
- streamingEncoder = await spawnStreamingEncoder(
109559
+ if (hasHdrContent) {
109560
+ log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
109561
+ const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
109562
+ const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
109563
+ for (const v of composition.videos) {
109564
+ if (!hdrVideoIds.includes(v.id)) continue;
109565
+ let srcPath = v.src;
109566
+ if (!srcPath.startsWith("/")) {
109567
+ const fromCompiled = join15(compiledDir, srcPath);
109568
+ srcPath = existsSync15(fromCompiled) ? fromCompiled : join15(projectDir, srcPath);
109569
+ }
109570
+ hdrVideoSrcPaths.set(v.id, srcPath);
109571
+ }
109572
+ if (!fileServer) throw new Error("fileServer must be initialized before HDR compositing");
109573
+ const domSession = await createCaptureSession(
109574
+ fileServer.url,
109575
+ framesDir,
109576
+ captureOptions,
109577
+ createVideoFrameInjector(frameLookup),
109578
+ cfg
109579
+ );
109580
+ await initializeSession(domSession);
109581
+ assertNotAborted();
109582
+ lastBrowserConsole = domSession.browserConsoleBuffer;
109583
+ await initTransparentBackground(domSession.page);
109584
+ const transitionMeta = await domSession.page.evaluate(() => {
109585
+ return window.__hf?.transitions ?? [];
109586
+ });
109587
+ const sceneElements = await domSession.page.evaluate(() => {
109588
+ const scenes = document.querySelectorAll(".scene");
109589
+ const map2 = {};
109590
+ for (const scene of scenes) {
109591
+ const els = scene.querySelectorAll("[data-start]");
109592
+ map2[scene.id] = Array.from(els).map((e) => e.id);
109593
+ }
109594
+ return map2;
109595
+ });
109596
+ const transitionRanges = transitionMeta.map((t) => ({
109597
+ ...t,
109598
+ startFrame: Math.floor(t.time * job.config.fps),
109599
+ endFrame: Math.ceil((t.time + t.duration) * job.config.fps)
109600
+ }));
109601
+ if (transitionRanges.length > 0) {
109602
+ log.info("[Render] Detected shader transitions for HDR compositing", {
109603
+ count: transitionRanges.length,
109604
+ transitions: transitionRanges.map((t) => ({
109605
+ shader: t.shader,
109606
+ from: t.fromScene,
109607
+ to: t.toScene,
109608
+ frames: `${t.startFrame}-${t.endFrame}`
109609
+ }))
109610
+ });
109611
+ }
109612
+ const hdrEncoder = await spawnStreamingEncoder(
108031
109613
  videoOnlyPath,
108032
109614
  {
108033
- ...baseEncoderOpts,
108034
- imageFormat: captureOptions.format || "jpeg"
109615
+ fps: job.config.fps,
109616
+ width,
109617
+ height,
109618
+ codec: preset.codec,
109619
+ preset: preset.preset,
109620
+ quality: preset.quality,
109621
+ pixelFormat: preset.pixelFormat,
109622
+ hdr: preset.hdr,
109623
+ rawInputFormat: "rgb48le"
108035
109624
  },
108036
- abortSignal
109625
+ abortSignal,
109626
+ { ffmpegStreamingTimeout: 36e5 }
108037
109627
  );
108038
109628
  assertNotAborted();
108039
- }
108040
- if (enableStreamingEncode && streamingEncoder) {
108041
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
108042
- const currentEncoder = streamingEncoder;
108043
- if (workerCount > 1) {
108044
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108045
- const onFrameBuffer = async (frameIndex, buffer) => {
108046
- await reorderBuffer.waitForFrame(frameIndex);
108047
- currentEncoder.writeFrame(buffer);
108048
- reorderBuffer.advanceTo(frameIndex + 1);
109629
+ const hdrExtractionDims = /* @__PURE__ */ new Map();
109630
+ const hdrVideoStartTimes = /* @__PURE__ */ new Map();
109631
+ for (const v of composition.videos) {
109632
+ if (hdrVideoIds.includes(v.id)) {
109633
+ hdrVideoStartTimes.set(v.id, v.start);
109634
+ }
109635
+ }
109636
+ const uniqueStartTimes = [...new Set(hdrVideoStartTimes.values())].sort((a, b) => a - b);
109637
+ for (const seekTime of uniqueStartTimes) {
109638
+ await domSession.page.evaluate((t) => {
109639
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109640
+ }, seekTime);
109641
+ if (domSession.onBeforeCapture) {
109642
+ await domSession.onBeforeCapture(domSession.page, seekTime);
109643
+ }
109644
+ const stacking = await queryElementStacking(domSession.page, nativeHdrVideoIds);
109645
+ for (const el of stacking) {
109646
+ if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !hdrExtractionDims.has(el.id)) {
109647
+ hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
109648
+ }
109649
+ }
109650
+ }
109651
+ const hdrFrameDirs = /* @__PURE__ */ new Map();
109652
+ for (const [videoId, srcPath] of hdrVideoSrcPaths) {
109653
+ const video = composition.videos.find((v) => v.id === videoId);
109654
+ if (!video) continue;
109655
+ const frameDir = join15(framesDir, `hdr_${videoId}`);
109656
+ mkdirSync10(frameDir, { recursive: true });
109657
+ const duration = video.end - video.start;
109658
+ const dims = hdrExtractionDims.get(videoId) ?? { width, height };
109659
+ const ffmpegArgs = [
109660
+ "-ss",
109661
+ String(video.mediaStart),
109662
+ "-i",
109663
+ srcPath,
109664
+ "-t",
109665
+ String(duration),
109666
+ "-r",
109667
+ String(job.config.fps),
109668
+ "-vf",
109669
+ `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
109670
+ "-pix_fmt",
109671
+ "rgb48le",
109672
+ "-c:v",
109673
+ "png",
109674
+ "-y",
109675
+ join15(frameDir, "frame_%04d.png")
109676
+ ];
109677
+ const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
109678
+ if (!result.success) {
109679
+ log.warn("HDR frame pre-extraction failed; loop will fill with black", {
109680
+ videoId,
109681
+ srcPath,
109682
+ stderr: result.stderr.slice(-400)
109683
+ });
109684
+ }
109685
+ hdrFrameDirs.set(videoId, frameDir);
109686
+ }
109687
+ assertNotAborted();
109688
+ try {
109689
+ let countNonZeroAlpha2 = function(rgba) {
109690
+ let n = 0;
109691
+ for (let p = 3; p < rgba.length; p += 4) {
109692
+ if (rgba[p] !== 0) n++;
109693
+ }
109694
+ return n;
109695
+ }, countNonZeroRgb482 = function(buf) {
109696
+ let n = 0;
109697
+ for (let p = 0; p < buf.length; p += 6) {
109698
+ if (buf[p] !== 0 || buf[p + 1] !== 0 || buf[p + 2] !== 0) n++;
109699
+ }
109700
+ return n;
108049
109701
  };
108050
- await executeParallelCapture(
108051
- fileServer.url,
108052
- workDir,
108053
- tasks,
108054
- captureOptions,
108055
- () => createVideoFrameInjector(frameLookup),
108056
- abortSignal,
108057
- (progress) => {
108058
- job.framesRendered = progress.capturedFrames;
108059
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108060
- const progressPct = 25 + frameProgress * 55;
108061
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108062
- updateJobStatus(
108063
- job,
108064
- "rendering",
108065
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108066
- Math.round(progressPct),
108067
- onProgress
109702
+ var countNonZeroAlpha = countNonZeroAlpha2, countNonZeroRgb48 = countNonZeroRgb482;
109703
+ const beforeCaptureHook = domSession.onBeforeCapture;
109704
+ const cleanedUpVideos = /* @__PURE__ */ new Set();
109705
+ const hdrVideoEndTimes = /* @__PURE__ */ new Map();
109706
+ for (const v of composition.videos) {
109707
+ if (hdrFrameDirs.has(v.id)) {
109708
+ hdrVideoEndTimes.set(v.id, v.end);
109709
+ }
109710
+ }
109711
+ const debugDumpEnabled = process.env.KEEP_TEMP === "1";
109712
+ const debugDumpDir = debugDumpEnabled ? join15(framesDir, "debug-composite") : null;
109713
+ if (debugDumpDir && !existsSync15(debugDumpDir)) {
109714
+ mkdirSync10(debugDumpDir, { recursive: true });
109715
+ }
109716
+ async function compositeToBuffer(canvas, time, fullStacking, elementFilter, debugFrameIndex = -1) {
109717
+ const filteredStacking = elementFilter ? fullStacking.filter((e) => elementFilter.has(e.id)) : fullStacking;
109718
+ const layers = groupIntoLayers(filteredStacking);
109719
+ const shouldLog = debugDumpEnabled && debugFrameIndex >= 0;
109720
+ if (shouldLog) {
109721
+ log.info("[diag] compositeToBuffer plan", {
109722
+ frame: debugFrameIndex,
109723
+ time: time.toFixed(3),
109724
+ filterSize: elementFilter?.size,
109725
+ fullStackingCount: fullStacking.length,
109726
+ filteredCount: filteredStacking.length,
109727
+ layerCount: layers.length,
109728
+ layers: layers.map(
109729
+ (l) => l.type === "hdr" ? {
109730
+ type: "hdr",
109731
+ id: l.element.id,
109732
+ z: l.element.zIndex,
109733
+ visible: l.element.visible,
109734
+ opacity: l.element.opacity,
109735
+ bounds: `${Math.round(l.element.x)},${Math.round(l.element.y)} ${Math.round(l.element.width)}x${Math.round(l.element.height)}`
109736
+ } : { type: "dom", ids: l.elementIds }
109737
+ )
109738
+ });
109739
+ }
109740
+ for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) {
109741
+ const layer = layers[layerIdx];
109742
+ if (layer.type === "hdr") {
109743
+ const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109744
+ blitHdrVideoLayer(
109745
+ canvas,
109746
+ layer.element,
109747
+ time,
109748
+ job.config.fps,
109749
+ hdrFrameDirs,
109750
+ hdrVideoStartTimes,
109751
+ width,
109752
+ height,
109753
+ log,
109754
+ videoTransfers.get(layer.element.id),
109755
+ effectiveHdr?.transfer
108068
109756
  );
109757
+ if (shouldLog) {
109758
+ const after2 = countNonZeroRgb482(canvas);
109759
+ const frameDir = hdrFrameDirs.get(layer.element.id);
109760
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
109761
+ const localTime = time - startTime;
109762
+ const frameNum = Math.floor(localTime * job.config.fps) + 1;
109763
+ const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
109764
+ log.info("[diag] hdr layer blit", {
109765
+ frame: debugFrameIndex,
109766
+ layerIdx,
109767
+ id: layer.element.id,
109768
+ pixelsAdded: after2 - before2,
109769
+ totalNonZero: after2,
109770
+ startTime,
109771
+ localTime: localTime.toFixed(3),
109772
+ hdrFrameNum: frameNum,
109773
+ expectedFrame,
109774
+ expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
109775
+ });
109776
+ }
109777
+ } else {
109778
+ const allElementIds = fullStacking.map((e) => e.id);
109779
+ const layerIds = new Set(layer.elementIds);
109780
+ const hideIds = allElementIds.filter((id) => !layerIds.has(id));
109781
+ await domSession.page.evaluate((t) => {
109782
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109783
+ }, time);
109784
+ if (beforeCaptureHook) {
109785
+ await beforeCaptureHook(domSession.page, time);
109786
+ }
109787
+ await applyDomLayerMask(domSession.page, layer.elementIds, hideIds);
109788
+ const domPng = await captureAlphaPng(domSession.page, width, height);
109789
+ await removeDomLayerMask(domSession.page, hideIds);
109790
+ try {
109791
+ const { data: domRgba } = decodePng(domPng);
109792
+ if (!effectiveHdr) {
109793
+ throw new Error(
109794
+ "Invariant violation: effectiveHdr is undefined inside HDR layer branch"
109795
+ );
109796
+ }
109797
+ const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109798
+ const alphaPixels = shouldLog ? countNonZeroAlpha2(domRgba) : 0;
109799
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, effectiveHdr.transfer);
109800
+ if (shouldLog && debugDumpDir) {
109801
+ const after2 = countNonZeroRgb482(canvas);
109802
+ const dumpName = `frame_${String(debugFrameIndex).padStart(4, "0")}_layer_${String(layerIdx).padStart(2, "0")}_dom.png`;
109803
+ const dumpPath = join15(debugDumpDir, dumpName);
109804
+ writeFileSync4(dumpPath, domPng);
109805
+ log.info("[diag] dom layer blit", {
109806
+ frame: debugFrameIndex,
109807
+ layerIdx,
109808
+ layerIds: layer.elementIds,
109809
+ hideCount: hideIds.length,
109810
+ pngBytes: domPng.length,
109811
+ alphaPixels,
109812
+ pixelsAdded: after2 - before2,
109813
+ totalNonZero: after2,
109814
+ dumpPath
109815
+ });
109816
+ }
109817
+ } catch (err) {
109818
+ log.warn("DOM layer decode/blit failed; skipping overlay", {
109819
+ layerIds: layer.elementIds,
109820
+ error: err instanceof Error ? err.message : String(err)
109821
+ });
109822
+ }
108069
109823
  }
108070
- },
108071
- onFrameBuffer,
108072
- cfg
108073
- );
108074
- if (probeSession) {
108075
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108076
- await closeCaptureSession(probeSession);
108077
- probeSession = null;
108078
- }
108079
- } else {
108080
- const videoInjector = createVideoFrameInjector(frameLookup);
108081
- const session = probeSession ?? await createCaptureSession(
108082
- fileServer.url,
108083
- framesDir,
108084
- captureOptions,
108085
- videoInjector,
108086
- cfg
108087
- );
108088
- if (probeSession) {
108089
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108090
- probeSession = null;
108091
- }
108092
- try {
108093
- if (!session.isInitialized) {
108094
- await initializeSession(session);
108095
109824
  }
109825
+ if (shouldLog && debugDumpDir) {
109826
+ const finalNonZero = countNonZeroRgb482(canvas);
109827
+ log.info("[diag] compositeToBuffer end", {
109828
+ frame: debugFrameIndex,
109829
+ finalNonZeroPixels: finalNonZero,
109830
+ totalPixels: width * height,
109831
+ coverage: (finalNonZero / (width * height) * 100).toFixed(1) + "%"
109832
+ });
109833
+ }
109834
+ }
109835
+ const bufSize = width * height * 6;
109836
+ const hasTransitions = transitionRanges.length > 0;
109837
+ const transBufferA = hasTransitions ? Buffer.alloc(bufSize) : null;
109838
+ const transBufferB = hasTransitions ? Buffer.alloc(bufSize) : null;
109839
+ const transOutput = hasTransitions ? Buffer.alloc(bufSize) : null;
109840
+ const normalCanvas = Buffer.alloc(bufSize);
109841
+ for (let i = 0; i < totalFrames; i++) {
108096
109842
  assertNotAborted();
108097
- lastBrowserConsole = session.browserConsoleBuffer;
108098
- for (let i = 0; i < job.totalFrames; i++) {
108099
- assertNotAborted();
108100
- const time = i / job.config.fps;
108101
- const { buffer } = await captureFrameToBuffer(session, i, time);
108102
- await reorderBuffer.waitForFrame(i);
108103
- currentEncoder.writeFrame(buffer);
108104
- reorderBuffer.advanceTo(i + 1);
108105
- job.framesRendered = i + 1;
108106
- const frameProgress = (i + 1) / job.totalFrames;
108107
- const progress = 25 + frameProgress * 55;
109843
+ const time = i / job.config.fps;
109844
+ await domSession.page.evaluate((t) => {
109845
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109846
+ }, time);
109847
+ if (beforeCaptureHook) {
109848
+ await beforeCaptureHook(domSession.page, time);
109849
+ }
109850
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
109851
+ const activeTransition = transitionRanges.find(
109852
+ (t) => i >= t.startFrame && i <= t.endFrame
109853
+ );
109854
+ if (i % 30 === 0) {
109855
+ const hdrEl = stackingInfo.find((e) => e.isHdr);
109856
+ log.debug("[Render] HDR layer composite frame", {
109857
+ frame: i,
109858
+ time: time.toFixed(2),
109859
+ hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
109860
+ stackingCount: stackingInfo.length,
109861
+ activeTransition: activeTransition?.shader
109862
+ });
109863
+ }
109864
+ if (activeTransition && transBufferA && transBufferB && transOutput) {
109865
+ const progress = activeTransition.endFrame === activeTransition.startFrame ? 1 : (i - activeTransition.startFrame) / (activeTransition.endFrame - activeTransition.startFrame);
109866
+ const sceneAIds = new Set(sceneElements[activeTransition.fromScene] ?? []);
109867
+ const sceneBIds = new Set(sceneElements[activeTransition.toScene] ?? []);
109868
+ transBufferA.fill(0);
109869
+ transBufferB.fill(0);
109870
+ for (const [sceneBuf, sceneIds] of [
109871
+ [transBufferA, sceneAIds],
109872
+ [transBufferB, sceneBIds]
109873
+ ]) {
109874
+ await domSession.page.evaluate((t) => {
109875
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
109876
+ }, time);
109877
+ if (beforeCaptureHook) {
109878
+ await beforeCaptureHook(domSession.page, time);
109879
+ }
109880
+ for (const el of stackingInfo) {
109881
+ if (!el.isHdr || !sceneIds.has(el.id)) continue;
109882
+ blitHdrVideoLayer(
109883
+ sceneBuf,
109884
+ el,
109885
+ time,
109886
+ job.config.fps,
109887
+ hdrFrameDirs,
109888
+ hdrVideoStartTimes,
109889
+ width,
109890
+ height,
109891
+ log,
109892
+ videoTransfers.get(el.id),
109893
+ effectiveHdr?.transfer
109894
+ );
109895
+ }
109896
+ const showIds = Array.from(sceneIds);
109897
+ const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrVideoIds.has(id));
109898
+ await applyDomLayerMask(domSession.page, showIds, hideIds);
109899
+ const domPng = await captureAlphaPng(domSession.page, width, height);
109900
+ await removeDomLayerMask(domSession.page, hideIds);
109901
+ try {
109902
+ const { data: domRgba } = decodePng(domPng);
109903
+ if (!effectiveHdr) {
109904
+ throw new Error(
109905
+ "Invariant violation: effectiveHdr is undefined inside hasHdrVideo branch"
109906
+ );
109907
+ }
109908
+ blitRgba8OverRgb48le(
109909
+ domRgba,
109910
+ sceneBuf,
109911
+ width,
109912
+ height,
109913
+ effectiveHdr.transfer
109914
+ );
109915
+ } catch (err) {
109916
+ log.warn("DOM layer decode/blit failed; skipping overlay for transition scene", {
109917
+ frameIndex: i,
109918
+ sceneIds: Array.from(sceneIds),
109919
+ error: err instanceof Error ? err.message : String(err)
109920
+ });
109921
+ }
109922
+ }
109923
+ const transitionFn = TRANSITIONS[activeTransition.shader] ?? crossfade;
109924
+ transitionFn(transBufferA, transBufferB, transOutput, width, height, progress);
109925
+ hdrEncoder.writeFrame(transOutput);
109926
+ } else {
109927
+ normalCanvas.fill(0);
109928
+ await compositeToBuffer(normalCanvas, time, stackingInfo, void 0, i);
109929
+ if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
109930
+ const previewPath = join15(
109931
+ debugDumpDir,
109932
+ `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`
109933
+ );
109934
+ writeFileSync4(previewPath, normalCanvas);
109935
+ }
109936
+ hdrEncoder.writeFrame(normalCanvas);
109937
+ }
109938
+ if (process.env.KEEP_TEMP !== "1") {
109939
+ for (const [videoId, endTime] of hdrVideoEndTimes) {
109940
+ if (time > endTime && !cleanedUpVideos.has(videoId)) {
109941
+ const stillNeeded = activeTransition && (sceneElements[activeTransition.fromScene]?.includes(videoId) || sceneElements[activeTransition.toScene]?.includes(videoId));
109942
+ if (!stillNeeded) {
109943
+ const frameDir = hdrFrameDirs.get(videoId);
109944
+ if (frameDir) {
109945
+ try {
109946
+ rmSync3(frameDir, { recursive: true, force: true });
109947
+ } catch (err) {
109948
+ log.warn("Failed to clean up HDR frame directory", {
109949
+ videoId,
109950
+ frameDir,
109951
+ error: err instanceof Error ? err.message : String(err)
109952
+ });
109953
+ }
109954
+ }
109955
+ cleanedUpVideos.add(videoId);
109956
+ }
109957
+ }
109958
+ }
109959
+ }
109960
+ job.framesRendered = i + 1;
109961
+ if ((i + 1) % 10 === 0 || i + 1 === totalFrames) {
109962
+ const frameProgress = (i + 1) / totalFrames;
108108
109963
  updateJobStatus(
108109
109964
  job,
108110
109965
  "rendering",
108111
- `Streaming frame ${i + 1}/${job.totalFrames}`,
108112
- Math.round(progress),
109966
+ `HDR composite frame ${i + 1}/${job.totalFrames}`,
109967
+ Math.round(25 + frameProgress * 55),
108113
109968
  onProgress
108114
109969
  );
108115
109970
  }
108116
- } finally {
108117
- lastBrowserConsole = session.browserConsoleBuffer;
108118
- await closeCaptureSession(session);
108119
109971
  }
109972
+ } finally {
109973
+ lastBrowserConsole = domSession.browserConsoleBuffer;
109974
+ await closeCaptureSession(domSession);
108120
109975
  }
108121
- const encodeResult = await currentEncoder.close();
109976
+ const hdrEncodeResult = await hdrEncoder.close();
108122
109977
  assertNotAborted();
108123
- if (!encodeResult.success) {
108124
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
109978
+ if (!hdrEncodeResult.success) {
109979
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
108125
109980
  }
108126
109981
  perfStages.captureMs = Date.now() - stage4Start;
108127
- perfStages.encodeMs = encodeResult.durationMs;
109982
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
108128
109983
  } else {
108129
- if (workerCount > 1) {
108130
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108131
- await executeParallelCapture(
108132
- fileServer.url,
108133
- workDir,
108134
- tasks,
108135
- captureOptions,
108136
- () => createVideoFrameInjector(frameLookup),
108137
- abortSignal,
108138
- (progress) => {
108139
- job.framesRendered = progress.capturedFrames;
108140
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108141
- const progressPct = 25 + frameProgress * 45;
108142
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
109984
+ let streamingEncoder = null;
109985
+ if (enableStreamingEncode) {
109986
+ streamingEncoder = await spawnStreamingEncoder(
109987
+ videoOnlyPath,
109988
+ {
109989
+ fps: job.config.fps,
109990
+ width,
109991
+ height,
109992
+ codec: preset.codec,
109993
+ preset: preset.preset,
109994
+ quality: preset.quality,
109995
+ pixelFormat: preset.pixelFormat,
109996
+ useGpu: job.config.useGpu,
109997
+ imageFormat: captureOptions.format || "jpeg",
109998
+ hdr: preset.hdr
109999
+ },
110000
+ abortSignal
110001
+ );
110002
+ assertNotAborted();
110003
+ }
110004
+ if (enableStreamingEncode && streamingEncoder) {
110005
+ const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
110006
+ const currentEncoder = streamingEncoder;
110007
+ if (workerCount > 1) {
110008
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
110009
+ const onFrameBuffer = async (frameIndex, buffer) => {
110010
+ await reorderBuffer.waitForFrame(frameIndex);
110011
+ currentEncoder.writeFrame(buffer);
110012
+ reorderBuffer.advanceTo(frameIndex + 1);
110013
+ };
110014
+ await executeParallelCapture(
110015
+ fileServer.url,
110016
+ workDir,
110017
+ tasks,
110018
+ captureOptions,
110019
+ () => createVideoFrameInjector(frameLookup),
110020
+ abortSignal,
110021
+ (progress) => {
110022
+ job.framesRendered = progress.capturedFrames;
110023
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
110024
+ const progressPct = 25 + frameProgress * 55;
110025
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
110026
+ updateJobStatus(
110027
+ job,
110028
+ "rendering",
110029
+ `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
110030
+ Math.round(progressPct),
110031
+ onProgress
110032
+ );
110033
+ }
110034
+ },
110035
+ onFrameBuffer,
110036
+ cfg
110037
+ );
110038
+ if (probeSession) {
110039
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
110040
+ await closeCaptureSession(probeSession);
110041
+ probeSession = null;
110042
+ }
110043
+ } else {
110044
+ const videoInjector = createVideoFrameInjector(frameLookup);
110045
+ const session = probeSession ?? await createCaptureSession(
110046
+ fileServer.url,
110047
+ framesDir,
110048
+ captureOptions,
110049
+ videoInjector,
110050
+ cfg
110051
+ );
110052
+ if (probeSession) {
110053
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
110054
+ probeSession = null;
110055
+ }
110056
+ try {
110057
+ if (!session.isInitialized) {
110058
+ await initializeSession(session);
110059
+ }
110060
+ assertNotAborted();
110061
+ lastBrowserConsole = session.browserConsoleBuffer;
110062
+ for (let i = 0; i < totalFrames; i++) {
110063
+ assertNotAborted();
110064
+ const time = i / job.config.fps;
110065
+ const { buffer } = await captureFrameToBuffer(session, i, time);
110066
+ await reorderBuffer.waitForFrame(i);
110067
+ currentEncoder.writeFrame(buffer);
110068
+ reorderBuffer.advanceTo(i + 1);
110069
+ job.framesRendered = i + 1;
110070
+ const frameProgress = (i + 1) / totalFrames;
110071
+ const progress = 25 + frameProgress * 55;
108143
110072
  updateJobStatus(
108144
110073
  job,
108145
110074
  "rendering",
108146
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108147
- Math.round(progressPct),
110075
+ `Streaming frame ${i + 1}/${job.totalFrames}`,
110076
+ Math.round(progress),
108148
110077
  onProgress
108149
110078
  );
108150
110079
  }
108151
- },
108152
- void 0,
108153
- cfg
108154
- );
108155
- await mergeWorkerFrames(workDir, tasks, framesDir);
108156
- if (probeSession) {
108157
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108158
- await closeCaptureSession(probeSession);
108159
- probeSession = null;
110080
+ } finally {
110081
+ lastBrowserConsole = session.browserConsoleBuffer;
110082
+ await closeCaptureSession(session);
110083
+ }
108160
110084
  }
108161
- } else {
108162
- const videoInjector = createVideoFrameInjector(frameLookup);
108163
- const session = probeSession ?? await createCaptureSession(
108164
- fileServer.url,
108165
- framesDir,
108166
- captureOptions,
108167
- videoInjector,
108168
- cfg
108169
- );
108170
- if (probeSession) {
108171
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108172
- probeSession = null;
110085
+ const encodeResult = await currentEncoder.close();
110086
+ assertNotAborted();
110087
+ if (!encodeResult.success) {
110088
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108173
110089
  }
108174
- try {
108175
- if (!session.isInitialized) {
108176
- await initializeSession(session);
110090
+ perfStages.captureMs = Date.now() - stage4Start;
110091
+ perfStages.encodeMs = encodeResult.durationMs;
110092
+ } else {
110093
+ if (workerCount > 1) {
110094
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
110095
+ await executeParallelCapture(
110096
+ fileServer.url,
110097
+ workDir,
110098
+ tasks,
110099
+ captureOptions,
110100
+ () => createVideoFrameInjector(frameLookup),
110101
+ abortSignal,
110102
+ (progress) => {
110103
+ job.framesRendered = progress.capturedFrames;
110104
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
110105
+ const progressPct = 25 + frameProgress * 45;
110106
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
110107
+ updateJobStatus(
110108
+ job,
110109
+ "rendering",
110110
+ `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
110111
+ Math.round(progressPct),
110112
+ onProgress
110113
+ );
110114
+ }
110115
+ },
110116
+ void 0,
110117
+ cfg
110118
+ );
110119
+ await mergeWorkerFrames(workDir, tasks, framesDir);
110120
+ if (probeSession) {
110121
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
110122
+ await closeCaptureSession(probeSession);
110123
+ probeSession = null;
108177
110124
  }
108178
- assertNotAborted();
108179
- lastBrowserConsole = session.browserConsoleBuffer;
108180
- for (let i = 0; i < job.totalFrames; i++) {
108181
- assertNotAborted();
108182
- const time = i / job.config.fps;
108183
- await captureFrame(session, i, time);
108184
- job.framesRendered = i + 1;
108185
- const frameProgress = (i + 1) / job.totalFrames;
108186
- const progress = 25 + frameProgress * 45;
108187
- updateJobStatus(
108188
- job,
108189
- "rendering",
108190
- `Capturing frame ${i + 1}/${job.totalFrames}`,
108191
- Math.round(progress),
108192
- onProgress
108193
- );
110125
+ } else {
110126
+ const videoInjector = createVideoFrameInjector(frameLookup);
110127
+ const session = probeSession ?? await createCaptureSession(
110128
+ fileServer.url,
110129
+ framesDir,
110130
+ captureOptions,
110131
+ videoInjector,
110132
+ cfg
110133
+ );
110134
+ if (probeSession) {
110135
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
110136
+ probeSession = null;
108194
110137
  }
108195
- } finally {
108196
- lastBrowserConsole = session.browserConsoleBuffer;
108197
- await closeCaptureSession(session);
110138
+ try {
110139
+ if (!session.isInitialized) {
110140
+ await initializeSession(session);
110141
+ }
110142
+ assertNotAborted();
110143
+ lastBrowserConsole = session.browserConsoleBuffer;
110144
+ for (let i = 0; i < job.totalFrames; i++) {
110145
+ assertNotAborted();
110146
+ const time = i / job.config.fps;
110147
+ await captureFrame(session, i, time);
110148
+ job.framesRendered = i + 1;
110149
+ const frameProgress = (i + 1) / job.totalFrames;
110150
+ const progress = 25 + frameProgress * 45;
110151
+ updateJobStatus(
110152
+ job,
110153
+ "rendering",
110154
+ `Capturing frame ${i + 1}/${job.totalFrames}`,
110155
+ Math.round(progress),
110156
+ onProgress
110157
+ );
110158
+ }
110159
+ } finally {
110160
+ lastBrowserConsole = session.browserConsoleBuffer;
110161
+ await closeCaptureSession(session);
110162
+ }
110163
+ }
110164
+ perfStages.captureMs = Date.now() - stage4Start;
110165
+ const stage5Start = Date.now();
110166
+ updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
110167
+ const frameExt = needsAlpha ? "png" : "jpg";
110168
+ const framePattern = `frame_%06d.${frameExt}`;
110169
+ const encoderOpts = {
110170
+ fps: job.config.fps,
110171
+ width,
110172
+ height,
110173
+ codec: preset.codec,
110174
+ preset: preset.preset,
110175
+ quality: preset.quality,
110176
+ pixelFormat: preset.pixelFormat,
110177
+ useGpu: job.config.useGpu,
110178
+ hdr: preset.hdr
110179
+ };
110180
+ const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
110181
+ framesDir,
110182
+ framePattern,
110183
+ videoOnlyPath,
110184
+ encoderOpts,
110185
+ chunkedEncodeSize,
110186
+ abortSignal
110187
+ ) : await encodeFramesFromDir(
110188
+ framesDir,
110189
+ framePattern,
110190
+ videoOnlyPath,
110191
+ encoderOpts,
110192
+ abortSignal
110193
+ );
110194
+ assertNotAborted();
110195
+ if (!encodeResult.success) {
110196
+ throw new Error(`Encoding failed: ${encodeResult.error}`);
108198
110197
  }
110198
+ perfStages.encodeMs = Date.now() - stage5Start;
108199
110199
  }
108200
- perfStages.captureMs = Date.now() - stage4Start;
108201
- const stage5Start = Date.now();
108202
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
108203
- const frameExt = needsAlpha ? "png" : "jpg";
108204
- const framePattern = `frame_%06d.${frameExt}`;
108205
- const encoderOpts = baseEncoderOpts;
108206
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
108207
- framesDir,
108208
- framePattern,
108209
- videoOnlyPath,
108210
- encoderOpts,
108211
- chunkedEncodeSize,
108212
- abortSignal
108213
- ) : await encodeFramesFromDir(
108214
- framesDir,
108215
- framePattern,
108216
- videoOnlyPath,
108217
- encoderOpts,
108218
- abortSignal
108219
- );
108220
- assertNotAborted();
108221
- if (!encodeResult.success) {
108222
- throw new Error(`Encoding failed: ${encodeResult.error}`);
108223
- }
108224
- perfStages.encodeMs = Date.now() - stage5Start;
108225
110200
  }
108226
110201
  if (probeSession !== null) {
108227
110202
  const remainingProbeSession = probeSession;
@@ -108265,12 +110240,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108265
110240
  chunkedEncode: enableChunkedEncode,
108266
110241
  chunkSizeFrames: enableChunkedEncode ? chunkedEncodeSize : null,
108267
110242
  compositionDurationSeconds: composition.duration,
108268
- totalFrames: job.totalFrames,
110243
+ totalFrames,
108269
110244
  resolution: { width, height },
108270
110245
  videoCount: composition.videos.length,
108271
110246
  audioCount: composition.audios.length,
108272
110247
  stages: perfStages,
108273
- captureAvgMs: job.totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / job.totalFrames) : void 0
110248
+ captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0
108274
110249
  };
108275
110250
  job.perfSummary = perfSummary;
108276
110251
  if (job.config.debug) {
@@ -108288,6 +110263,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108288
110263
  const debugOutput = join15(workDir, `output${videoExt}`);
108289
110264
  copyFileSync2(outputPath, debugOutput);
108290
110265
  }
110266
+ } else if (process.env.KEEP_TEMP === "1") {
110267
+ log.info("KEEP_TEMP=1 \u2014 leaving workDir on disk for inspection", { workDir });
108291
110268
  } else {
108292
110269
  await safeCleanup(
108293
110270
  "remove workDir",