@hyperframes/producer 0.4.5 → 0.4.7

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
@@ -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",
@@ -100907,6 +100917,24 @@ async function pageScreenshotCapture(page, options) {
100907
100917
  });
100908
100918
  return Buffer.from(result.data, "base64");
100909
100919
  }
100920
+ async function initTransparentBackground(page) {
100921
+ const client = await getCdpSession(page);
100922
+ await client.send("Emulation.setDefaultBackgroundColorOverride", {
100923
+ color: { r: 0, g: 0, b: 0, a: 0 }
100924
+ });
100925
+ }
100926
+ async function captureAlphaPng(page, width, height) {
100927
+ const client = await getCdpSession(page);
100928
+ const result = await client.send("Page.captureScreenshot", {
100929
+ format: "png",
100930
+ fromSurface: true,
100931
+ captureBeyondViewport: false,
100932
+ optimizeForSpeed: false,
100933
+ // must be false to preserve alpha
100934
+ clip: { x: 0, y: 0, width, height, scale: 1 }
100935
+ });
100936
+ return Buffer.from(result.data, "base64");
100937
+ }
100910
100938
  async function injectVideoFramesBatch(page, updates) {
100911
100939
  if (updates.length === 0) return;
100912
100940
  await page.evaluate(
@@ -100928,16 +100956,7 @@ async function injectVideoFramesBatch(page, updates) {
100928
100956
  video.parentNode?.insertBefore(img, video.nextSibling);
100929
100957
  }
100930
100958
  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 {
100959
+ {
100941
100960
  const videoRect = video.getBoundingClientRect();
100942
100961
  const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
100943
100962
  const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
@@ -100988,14 +101007,22 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
100988
101007
  const active = new Set(ids);
100989
101008
  const videos = Array.from(document.querySelectorAll("video[data-start]"));
100990
101009
  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
101010
  const img = video.nextElementSibling;
100997
- if (img && img.classList.contains("__render_frame__")) {
100998
- img.style.visibility = "hidden";
101011
+ const hasImg = img && img.classList.contains("__render_frame__");
101012
+ if (active.has(video.id)) {
101013
+ video.style.setProperty("visibility", "hidden", "important");
101014
+ video.style.setProperty("pointer-events", "none", "important");
101015
+ if (hasImg) {
101016
+ img.style.visibility = "visible";
101017
+ }
101018
+ } else {
101019
+ video.style.removeProperty("display");
101020
+ video.style.setProperty("visibility", "hidden", "important");
101021
+ video.style.setProperty("opacity", "0", "important");
101022
+ video.style.setProperty("pointer-events", "none", "important");
101023
+ if (hasImg) {
101024
+ img.style.visibility = "hidden";
101025
+ }
100999
101026
  }
101000
101027
  }
101001
101028
  }, activeVideoIds);
@@ -101071,6 +101098,15 @@ function isFontResourceError(type, text, locationUrl) {
101071
101098
  `${locationUrl} ${text}`
101072
101099
  );
101073
101100
  }
101101
+ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100) {
101102
+ const deadline = Date.now() + timeoutMs;
101103
+ while (Date.now() < deadline) {
101104
+ const ready = Boolean(await page.evaluate(expression));
101105
+ if (ready) return true;
101106
+ await new Promise((resolve13) => setTimeout(resolve13, intervalMs));
101107
+ }
101108
+ return Boolean(await page.evaluate(expression));
101109
+ }
101074
101110
  async function initializeSession(session) {
101075
101111
  const { page, serverUrl } = session;
101076
101112
  page.on("console", (msg) => {
@@ -101100,14 +101136,26 @@ async function initializeSession(session) {
101100
101136
  if (session.captureMode === "screenshot") {
101101
101137
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
101102
101138
  const pageReadyTimeout2 = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
101103
- await page.waitForFunction(
101139
+ const pageReady2 = await pollPageExpression(
101140
+ page,
101104
101141
  `!!(window.__hf && typeof window.__hf.seek === "function" && window.__hf.duration > 0)`,
101105
- { timeout: pageReadyTimeout2 }
101142
+ pageReadyTimeout2
101106
101143
  );
101107
- await page.waitForFunction(
101144
+ if (!pageReady2) {
101145
+ throw new Error(
101146
+ `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
101147
+ );
101148
+ }
101149
+ const videosReady = await pollPageExpression(
101150
+ page,
101108
101151
  `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
101109
- { timeout: pageReadyTimeout2 }
101152
+ pageReadyTimeout2
101110
101153
  );
101154
+ if (!videosReady) {
101155
+ throw new Error(
101156
+ `[FrameCapture] video metadata not ready after ${pageReadyTimeout2}ms. Video elements must load metadata before capture starts.`
101157
+ );
101158
+ }
101111
101159
  await page.evaluate(`document.fonts?.ready`);
101112
101160
  session.isInitialized = true;
101113
101161
  return;
@@ -101431,7 +101479,7 @@ var ENCODER_PRESETS = {
101431
101479
  standard: { preset: "medium", quality: 18, codec: "h264" },
101432
101480
  high: { preset: "slow", quality: 15, codec: "h264" }
101433
101481
  };
101434
- function getEncoderPreset(quality, format3 = "mp4") {
101482
+ function getEncoderPreset(quality, format3 = "mp4", hdr) {
101435
101483
  const base = ENCODER_PRESETS[quality];
101436
101484
  if (format3 === "webm") {
101437
101485
  return {
@@ -101449,6 +101497,15 @@ function getEncoderPreset(quality, format3 = "mp4") {
101449
101497
  pixelFormat: "yuva444p10le"
101450
101498
  };
101451
101499
  }
101500
+ if (hdr) {
101501
+ return {
101502
+ preset: base.preset === "ultrafast" ? "fast" : base.preset,
101503
+ quality: base.quality,
101504
+ codec: "h265",
101505
+ pixelFormat: "yuv420p10le",
101506
+ hdr
101507
+ };
101508
+ }
101452
101509
  return { ...base, pixelFormat: "yuv420p" };
101453
101510
  }
101454
101511
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -101506,6 +101563,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
101506
101563
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
101507
101564
  }
101508
101565
  }
101566
+ if (codec === "h265") {
101567
+ args.push("-tag:v", "hvc1");
101568
+ }
101509
101569
  } else if (codec === "vp9") {
101510
101570
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
101511
101571
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -101812,31 +101872,79 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
101812
101872
  import { spawn as spawn6 } from "child_process";
101813
101873
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync4 } from "fs";
101814
101874
  import { dirname as dirname6 } from "path";
101875
+
101876
+ // ../engine/src/utils/hdr.ts
101877
+ function isHdrColorSpace(cs) {
101878
+ if (!cs) return false;
101879
+ return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
101880
+ }
101881
+ var DEFAULT_HDR10_MASTERING = {
101882
+ masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
101883
+ maxCll: "1000,400"
101884
+ };
101885
+ function getHdrEncoderColorParams(transfer, mastering = DEFAULT_HDR10_MASTERING) {
101886
+ const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
101887
+ const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
101888
+ const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
101889
+ return {
101890
+ colorPrimaries: "bt2020",
101891
+ colorTrc,
101892
+ colorspace: "bt2020nc",
101893
+ pixelFormat: "yuv420p10le",
101894
+ x265ColorParams: `${tagging}:${metadata}`,
101895
+ mastering
101896
+ };
101897
+ }
101898
+ function analyzeCompositionHdr(colorSpaces) {
101899
+ let hasPq = false;
101900
+ let hasHdr = false;
101901
+ for (const cs of colorSpaces) {
101902
+ if (!isHdrColorSpace(cs)) continue;
101903
+ hasHdr = true;
101904
+ if (cs?.colorTransfer === "smpte2084") hasPq = true;
101905
+ }
101906
+ if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
101907
+ const dominantTransfer = hasPq ? "pq" : "hlg";
101908
+ return { hasHdr: true, dominantTransfer };
101909
+ }
101910
+
101911
+ // ../engine/src/services/streamingEncoder.ts
101815
101912
  function createFrameReorderBuffer(startFrame, endFrame) {
101816
- let nextFrame = startFrame;
101817
- let waiters = [];
101818
- const resolveWaiters = () => {
101819
- for (const waiter of waiters.slice()) {
101820
- if (waiter.frame === nextFrame) {
101821
- waiter.resolve();
101822
- waiters = waiters.filter((w) => w !== waiter);
101823
- }
101913
+ let cursor = startFrame;
101914
+ const pending = /* @__PURE__ */ new Map();
101915
+ const enqueueAt = (frame, resolve13) => {
101916
+ const list = pending.get(frame);
101917
+ if (list === void 0) {
101918
+ pending.set(frame, [resolve13]);
101919
+ } else {
101920
+ list.push(resolve13);
101824
101921
  }
101825
101922
  };
101826
- return {
101827
- waitForFrame: (frame) => new Promise((resolve13) => {
101828
- waiters.push({ frame, resolve: resolve13 });
101829
- resolveWaiters();
101830
- }),
101831
- advanceTo: (frame) => {
101832
- nextFrame = frame;
101833
- resolveWaiters();
101834
- },
101835
- waitForAllDone: () => new Promise((resolve13) => {
101836
- waiters.push({ frame: endFrame, resolve: resolve13 });
101837
- resolveWaiters();
101838
- })
101923
+ const flushAt = (frame) => {
101924
+ const list = pending.get(frame);
101925
+ if (list === void 0) return;
101926
+ pending.delete(frame);
101927
+ for (const resolve13 of list) resolve13();
101839
101928
  };
101929
+ const waitForFrame = (frame) => new Promise((resolve13) => {
101930
+ if (frame === cursor) {
101931
+ resolve13();
101932
+ return;
101933
+ }
101934
+ enqueueAt(frame, resolve13);
101935
+ });
101936
+ const advanceTo = (frame) => {
101937
+ cursor = frame;
101938
+ flushAt(frame);
101939
+ };
101940
+ const waitForAllDone = () => new Promise((resolve13) => {
101941
+ if (cursor >= endFrame) {
101942
+ resolve13();
101943
+ return;
101944
+ }
101945
+ enqueueAt(endFrame, resolve13);
101946
+ });
101947
+ return { waitForFrame, advanceTo, waitForAllDone };
101840
101948
  }
101841
101949
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101842
101950
  const {
@@ -101849,19 +101957,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101849
101957
  useGpu = false,
101850
101958
  imageFormat = "jpeg"
101851
101959
  } = options;
101852
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
101853
- const args = [
101854
- "-f",
101855
- "image2pipe",
101856
- "-vcodec",
101857
- inputCodec,
101858
- "-framerate",
101859
- String(fps),
101860
- "-i",
101861
- "-",
101862
- "-r",
101863
- String(fps)
101864
- ];
101960
+ const args = [];
101961
+ if (options.rawInputFormat) {
101962
+ const hdrTransfer = options.hdr?.transfer;
101963
+ const inputColorTrc = hdrTransfer === "pq" ? "smpte2084" : hdrTransfer === "hlg" ? "arib-std-b67" : void 0;
101964
+ args.push(
101965
+ "-f",
101966
+ "rawvideo",
101967
+ "-pix_fmt",
101968
+ options.rawInputFormat,
101969
+ "-s",
101970
+ `${options.width}x${options.height}`,
101971
+ "-framerate",
101972
+ String(fps)
101973
+ );
101974
+ if (inputColorTrc) {
101975
+ args.push(
101976
+ "-color_primaries",
101977
+ "bt2020",
101978
+ "-color_trc",
101979
+ inputColorTrc,
101980
+ "-colorspace",
101981
+ "bt2020nc"
101982
+ );
101983
+ }
101984
+ args.push("-i", "-");
101985
+ } else {
101986
+ const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
101987
+ args.push("-f", "image2pipe", "-vcodec", inputCodec, "-framerate", String(fps), "-i", "-");
101988
+ }
101989
+ args.push("-r", String(fps));
101865
101990
  const shouldUseGpu = useGpu && gpuEncoder !== null;
101866
101991
  if (codec === "h264" || codec === "h265") {
101867
101992
  if (shouldUseGpu) {
@@ -101899,12 +102024,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101899
102024
  if (bitrate) args.push("-b:v", bitrate);
101900
102025
  else args.push("-crf", String(quality));
101901
102026
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
101902
- const colorParams = "colorprim=bt709:transfer=bt709:colormatrix=bt709";
102027
+ const colorParams = options.rawInputFormat && options.hdr ? getHdrEncoderColorParams(options.hdr.transfer).x265ColorParams : "colorprim=bt709:transfer=bt709:colormatrix=bt709";
101903
102028
  if (preset === "ultrafast") {
101904
102029
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
101905
102030
  } else {
101906
102031
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
101907
102032
  }
102033
+ if (codec === "h265") {
102034
+ args.push("-tag:v", "hvc1");
102035
+ }
101908
102036
  }
101909
102037
  } else if (codec === "vp9") {
101910
102038
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -101920,17 +102048,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101920
102048
  return [...args, "-y", outputPath];
101921
102049
  }
101922
102050
  if (codec === "h264" || codec === "h265") {
101923
- args.push(
101924
- "-colorspace:v",
101925
- "bt709",
101926
- "-color_primaries:v",
101927
- "bt709",
101928
- "-color_trc:v",
101929
- "bt709",
101930
- "-color_range",
101931
- "tv"
101932
- );
101933
- if (gpuEncoder === "vaapi") {
102051
+ if (options.rawInputFormat && options.hdr) {
102052
+ args.push(
102053
+ "-colorspace:v",
102054
+ "bt2020nc",
102055
+ "-color_primaries:v",
102056
+ "bt2020",
102057
+ "-color_trc:v",
102058
+ options.hdr.transfer === "pq" ? "smpte2084" : "arib-std-b67",
102059
+ "-color_range",
102060
+ "tv"
102061
+ );
102062
+ } else {
102063
+ args.push(
102064
+ "-colorspace:v",
102065
+ "bt709",
102066
+ "-color_primaries:v",
102067
+ "bt709",
102068
+ "-color_trc:v",
102069
+ "bt709",
102070
+ "-color_range",
102071
+ "tv"
102072
+ );
102073
+ }
102074
+ if (options.rawInputFormat) {
102075
+ } else if (gpuEncoder === "vaapi") {
101934
102076
  const vfIdx = args.indexOf("-vf");
101935
102077
  if (vfIdx !== -1) {
101936
102078
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -102000,14 +102142,16 @@ Process error: ${err.message}`;
102000
102142
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
102001
102143
  return false;
102002
102144
  }
102003
- return ffmpeg.stdin.write(buffer);
102145
+ const copy = Buffer.from(buffer);
102146
+ return ffmpeg.stdin.write(copy);
102004
102147
  },
102005
102148
  close: async () => {
102006
102149
  clearTimeout(timer2);
102007
102150
  if (signal) signal.removeEventListener("abort", onAbort);
102008
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
102151
+ const stdin = ffmpeg.stdin;
102152
+ if (stdin && !stdin.destroyed) {
102009
102153
  await new Promise((resolve13) => {
102010
- ffmpeg.stdin.end(() => resolve13());
102154
+ stdin.end(() => resolve13());
102011
102155
  });
102012
102156
  }
102013
102157
  await exitPromise;
@@ -102111,6 +102255,10 @@ async function extractVideoMetadata(filePath) {
102111
102255
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
102112
102256
  const fps = avgFps || rFps;
102113
102257
  const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
102258
+ const colorTransfer = videoStream.color_transfer || "";
102259
+ const colorPrimaries = videoStream.color_primaries || "";
102260
+ const colorSpaceVal = videoStream.color_space || "";
102261
+ const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
102114
102262
  return {
102115
102263
  durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
102116
102264
  width: videoStream.width || 0,
@@ -102118,7 +102266,8 @@ async function extractVideoMetadata(filePath) {
102118
102266
  fps,
102119
102267
  videoCodec: videoStream.codec_name || "unknown",
102120
102268
  hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
102121
- isVFR
102269
+ isVFR,
102270
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
102122
102271
  };
102123
102272
  })();
102124
102273
  videoMetadataCache.set(filePath, probePromise);
@@ -102326,18 +102475,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102326
102475
  const metadata = await extractVideoMetadata(videoPath);
102327
102476
  const framePattern = `frame_%05d.${format3}`;
102328
102477
  const outputPattern = join8(videoOutputDir, framePattern);
102329
- const args = [
102330
- "-ss",
102331
- String(startTime),
102332
- "-i",
102333
- videoPath,
102334
- "-t",
102335
- String(duration),
102336
- "-vf",
102337
- `fps=${fps}`,
102338
- "-q:v",
102339
- format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
102340
- ];
102478
+ const isHdr = isHdrColorSpace(metadata.colorSpace);
102479
+ const isMacOS = process.platform === "darwin";
102480
+ const args = [];
102481
+ if (isHdr && isMacOS) {
102482
+ args.push("-hwaccel", "videotoolbox");
102483
+ }
102484
+ args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
102485
+ const vfFilters = [];
102486
+ if (isHdr && isMacOS) {
102487
+ vfFilters.push("format=nv12");
102488
+ }
102489
+ vfFilters.push(`fps=${fps}`);
102490
+ args.push("-vf", vfFilters.join(","));
102491
+ args.push("-q:v", format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
102341
102492
  if (format3 === "png") args.push("-compression_level", "6");
102342
102493
  args.push("-y", outputPattern);
102343
102494
  return new Promise((resolve13, reject) => {
@@ -102397,30 +102548,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102397
102548
  });
102398
102549
  });
102399
102550
  }
102551
+ async function convertSdrToHdr(inputPath, outputPath, signal, config2) {
102552
+ const timeout2 = config2?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
102553
+ const args = [
102554
+ "-i",
102555
+ inputPath,
102556
+ "-vf",
102557
+ "colorspace=all=bt2020:iall=bt709:range=tv",
102558
+ "-color_primaries",
102559
+ "bt2020",
102560
+ "-color_trc",
102561
+ "arib-std-b67",
102562
+ "-colorspace",
102563
+ "bt2020nc",
102564
+ "-c:v",
102565
+ "libx264",
102566
+ "-preset",
102567
+ "fast",
102568
+ "-crf",
102569
+ "16",
102570
+ "-c:a",
102571
+ "copy",
102572
+ "-y",
102573
+ outputPath
102574
+ ];
102575
+ const result = await runFfmpeg(args, { signal, timeout: timeout2 });
102576
+ if (!result.success) {
102577
+ throw new Error(
102578
+ `SDR\u2192HDR conversion failed (exit ${result.exitCode}): ${result.stderr.slice(-300)}`
102579
+ );
102580
+ }
102581
+ }
102400
102582
  async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
102401
102583
  const startTime = Date.now();
102402
102584
  const extracted = [];
102403
102585
  const errors = [];
102404
102586
  let totalFramesExtracted = 0;
102587
+ const resolvedVideos = [];
102588
+ for (const video of videos) {
102589
+ if (signal?.aborted) break;
102590
+ try {
102591
+ let videoPath = video.src;
102592
+ if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
102593
+ const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
102594
+ videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
102595
+ }
102596
+ if (isHttpUrl(videoPath)) {
102597
+ const downloadDir = join8(options.outputDir, "_downloads");
102598
+ mkdirSync5(downloadDir, { recursive: true });
102599
+ videoPath = await downloadToTemp(videoPath, downloadDir);
102600
+ }
102601
+ if (!existsSync8(videoPath)) {
102602
+ errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
102603
+ continue;
102604
+ }
102605
+ resolvedVideos.push({ video, videoPath });
102606
+ } catch (err) {
102607
+ errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
102608
+ }
102609
+ }
102610
+ const videoColorSpaces = await Promise.all(
102611
+ resolvedVideos.map(async ({ videoPath }) => {
102612
+ const metadata = await extractVideoMetadata(videoPath);
102613
+ return metadata.colorSpace;
102614
+ })
102615
+ );
102616
+ const hasAnyHdr = videoColorSpaces.some(isHdrColorSpace);
102617
+ if (hasAnyHdr) {
102618
+ const convertDir = join8(options.outputDir, "_hdr_normalized");
102619
+ mkdirSync5(convertDir, { recursive: true });
102620
+ for (let i = 0; i < resolvedVideos.length; i++) {
102621
+ if (signal?.aborted) break;
102622
+ const cs = videoColorSpaces[i] ?? null;
102623
+ if (!isHdrColorSpace(cs)) {
102624
+ const entry = resolvedVideos[i];
102625
+ if (!entry) continue;
102626
+ const convertedPath = join8(convertDir, `${entry.video.id}_hdr.mp4`);
102627
+ try {
102628
+ await convertSdrToHdr(entry.videoPath, convertedPath, signal, config2);
102629
+ entry.videoPath = convertedPath;
102630
+ } catch (err) {
102631
+ errors.push({
102632
+ videoId: entry.video.id,
102633
+ error: `SDR\u2192HDR conversion failed: ${err instanceof Error ? err.message : String(err)}`
102634
+ });
102635
+ }
102636
+ }
102637
+ }
102638
+ }
102405
102639
  const results = await Promise.all(
102406
- videos.map(async (video) => {
102640
+ resolvedVideos.map(async ({ video, videoPath }) => {
102407
102641
  if (signal?.aborted) {
102408
102642
  throw new Error("Video frame extraction cancelled");
102409
102643
  }
102410
102644
  try {
102411
- let videoPath = video.src;
102412
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
102413
- const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
102414
- videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
102415
- }
102416
- if (isHttpUrl(videoPath)) {
102417
- const downloadDir = join8(options.outputDir, "_downloads");
102418
- mkdirSync5(downloadDir, { recursive: true });
102419
- videoPath = await downloadToTemp(videoPath, downloadDir);
102420
- }
102421
- if (!existsSync8(videoPath)) {
102422
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
102423
- }
102424
102645
  let videoDuration = video.end - video.start;
102425
102646
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
102426
102647
  const metadata = await extractVideoMetadata(videoPath);
@@ -102655,6 +102876,74 @@ function createVideoFrameInjector(frameLookup, config2) {
102655
102876
  }
102656
102877
  };
102657
102878
  }
102879
+ async function hideVideoElements(page, videoIds) {
102880
+ if (videoIds.length === 0) return;
102881
+ await page.evaluate((ids) => {
102882
+ for (const id of ids) {
102883
+ const el = document.getElementById(id);
102884
+ if (el) {
102885
+ el.style.setProperty("visibility", "hidden", "important");
102886
+ el.style.setProperty("opacity", "0", "important");
102887
+ const img = document.getElementById(`__render_frame_${id}__`);
102888
+ if (img) img.style.setProperty("visibility", "hidden", "important");
102889
+ }
102890
+ }
102891
+ }, videoIds);
102892
+ }
102893
+ async function showVideoElements(page, videoIds) {
102894
+ if (videoIds.length === 0) return;
102895
+ await page.evaluate((ids) => {
102896
+ for (const id of ids) {
102897
+ const el = document.getElementById(id);
102898
+ if (el) {
102899
+ el.style.removeProperty("visibility");
102900
+ el.style.removeProperty("opacity");
102901
+ const img = document.getElementById(`__render_frame_${id}__`);
102902
+ if (img) img.style.removeProperty("visibility");
102903
+ }
102904
+ }
102905
+ }, videoIds);
102906
+ }
102907
+ async function queryElementStacking(page, nativeHdrVideoIds) {
102908
+ const hdrIds = Array.from(nativeHdrVideoIds);
102909
+ return page.evaluate((hdrIdList) => {
102910
+ const hdrSet = new Set(hdrIdList);
102911
+ const elements = document.querySelectorAll("[data-start]");
102912
+ const results = [];
102913
+ function getEffectiveZIndex(node) {
102914
+ let current = node;
102915
+ while (current) {
102916
+ const cs = window.getComputedStyle(current);
102917
+ const pos = cs.position;
102918
+ const z = parseInt(cs.zIndex);
102919
+ if (!Number.isNaN(z) && pos !== "static") return z;
102920
+ current = current.parentElement;
102921
+ }
102922
+ return 0;
102923
+ }
102924
+ for (const el of elements) {
102925
+ const id = el.id;
102926
+ if (!id) continue;
102927
+ const rect = el.getBoundingClientRect();
102928
+ const style = window.getComputedStyle(el);
102929
+ const zIndex = getEffectiveZIndex(el);
102930
+ const opacity = parseFloat(style.opacity) || 1;
102931
+ const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
102932
+ results.push({
102933
+ id,
102934
+ zIndex,
102935
+ x: Math.round(rect.x),
102936
+ y: Math.round(rect.y),
102937
+ width: Math.round(rect.width),
102938
+ height: Math.round(rect.height),
102939
+ opacity,
102940
+ visible,
102941
+ isHdr: hdrSet.has(id)
102942
+ });
102943
+ }
102944
+ return results;
102945
+ }, hdrIds);
102946
+ }
102658
102947
 
102659
102948
  // ../engine/src/services/audioMixer.ts
102660
102949
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
@@ -105765,6 +106054,292 @@ var serve = (options, listeningListener) => {
105765
106054
  return server;
105766
106055
  };
105767
106056
 
106057
+ // ../engine/src/utils/alphaBlit.ts
106058
+ import { inflateSync } from "zlib";
106059
+ function paeth(a, b, c) {
106060
+ const p = a + b - c;
106061
+ const pa = Math.abs(p - a);
106062
+ const pb = Math.abs(p - b);
106063
+ const pc = Math.abs(p - c);
106064
+ if (pa <= pb && pa <= pc) return a;
106065
+ if (pb <= pc) return b;
106066
+ return c;
106067
+ }
106068
+ function decodePngRaw(buf, caller) {
106069
+ 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) {
106070
+ throw new Error(`${caller}: not a PNG file`);
106071
+ }
106072
+ let pos = 8;
106073
+ let width = 0;
106074
+ let height = 0;
106075
+ let bitDepth = 0;
106076
+ let colorType = 0;
106077
+ let interlace = 0;
106078
+ let sawIhdr = false;
106079
+ const idatChunks = [];
106080
+ while (pos + 12 <= buf.length) {
106081
+ const chunkLen = buf.readUInt32BE(pos);
106082
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
106083
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
106084
+ if (chunkType === "IHDR") {
106085
+ width = chunkData.readUInt32BE(0);
106086
+ height = chunkData.readUInt32BE(4);
106087
+ bitDepth = chunkData[8] ?? 0;
106088
+ colorType = chunkData[9] ?? 0;
106089
+ interlace = chunkData[12] ?? 0;
106090
+ sawIhdr = true;
106091
+ } else if (chunkType === "IDAT") {
106092
+ idatChunks.push(Buffer.from(chunkData));
106093
+ } else if (chunkType === "IEND") {
106094
+ break;
106095
+ }
106096
+ pos += 12 + chunkLen;
106097
+ }
106098
+ if (!sawIhdr) {
106099
+ throw new Error(`${caller}: PNG missing IHDR chunk`);
106100
+ }
106101
+ if (colorType !== 2 && colorType !== 6) {
106102
+ throw new Error(`${caller}: unsupported color type ${colorType} (expected 2=RGB or 6=RGBA)`);
106103
+ }
106104
+ if (interlace !== 0) {
106105
+ throw new Error(
106106
+ `${caller}: Adam7-interlaced PNGs are not supported (interlace method ${interlace})`
106107
+ );
106108
+ }
106109
+ const channels = colorType === 6 ? 4 : 3;
106110
+ const bpp = channels * (bitDepth / 8);
106111
+ const stride = width * bpp;
106112
+ const compressed = Buffer.concat(idatChunks);
106113
+ const decompressed = inflateSync(compressed);
106114
+ const rawPixels = Buffer.allocUnsafe(height * stride);
106115
+ const prevRow = new Uint8Array(stride);
106116
+ const currRow = new Uint8Array(stride);
106117
+ let srcPos = 0;
106118
+ for (let y = 0; y < height; y++) {
106119
+ const filterType = decompressed[srcPos++] ?? 0;
106120
+ const rawRow = decompressed.subarray(srcPos, srcPos + stride);
106121
+ srcPos += stride;
106122
+ switch (filterType) {
106123
+ case 0:
106124
+ currRow.set(rawRow);
106125
+ break;
106126
+ case 1:
106127
+ for (let x = 0; x < stride; x++) {
106128
+ currRow[x] = (rawRow[x] ?? 0) + (x >= bpp ? currRow[x - bpp] ?? 0 : 0) & 255;
106129
+ }
106130
+ break;
106131
+ case 2:
106132
+ for (let x = 0; x < stride; x++) {
106133
+ currRow[x] = (rawRow[x] ?? 0) + (prevRow[x] ?? 0) & 255;
106134
+ }
106135
+ break;
106136
+ case 3:
106137
+ for (let x = 0; x < stride; x++) {
106138
+ const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
106139
+ const up = prevRow[x] ?? 0;
106140
+ currRow[x] = (rawRow[x] ?? 0) + Math.floor((left2 + up) / 2) & 255;
106141
+ }
106142
+ break;
106143
+ case 4:
106144
+ for (let x = 0; x < stride; x++) {
106145
+ const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
106146
+ const up = prevRow[x] ?? 0;
106147
+ const upLeft = x >= bpp ? prevRow[x - bpp] ?? 0 : 0;
106148
+ currRow[x] = (rawRow[x] ?? 0) + paeth(left2, up, upLeft) & 255;
106149
+ }
106150
+ break;
106151
+ default:
106152
+ throw new Error(`${caller}: unknown filter type ${filterType} at row ${y}`);
106153
+ }
106154
+ rawPixels.set(currRow, y * stride);
106155
+ prevRow.set(currRow);
106156
+ }
106157
+ return { width, height, bitDepth, colorType, rawPixels };
106158
+ }
106159
+ function decodePng(buf) {
106160
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePng");
106161
+ if (bitDepth !== 8) {
106162
+ throw new Error(`decodePng: unsupported bit depth ${bitDepth} (expected 8)`);
106163
+ }
106164
+ const output2 = new Uint8Array(width * height * 4);
106165
+ if (colorType === 6) {
106166
+ output2.set(rawPixels);
106167
+ } else {
106168
+ for (let i = 0; i < width * height; i++) {
106169
+ output2[i * 4 + 0] = rawPixels[i * 3 + 0] ?? 0;
106170
+ output2[i * 4 + 1] = rawPixels[i * 3 + 1] ?? 0;
106171
+ output2[i * 4 + 2] = rawPixels[i * 3 + 2] ?? 0;
106172
+ output2[i * 4 + 3] = 255;
106173
+ }
106174
+ }
106175
+ return { width, height, data: output2 };
106176
+ }
106177
+ function decodePngToRgb48le(buf) {
106178
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePngToRgb48le");
106179
+ if (bitDepth !== 16) {
106180
+ throw new Error(`decodePngToRgb48le: unsupported bit depth ${bitDepth} (expected 16)`);
106181
+ }
106182
+ const bpp = colorType === 6 ? 8 : 6;
106183
+ const output2 = Buffer.allocUnsafe(width * height * 6);
106184
+ for (let y = 0; y < height; y++) {
106185
+ const dstBase = y * width * 6;
106186
+ const srcRowBase = y * width * bpp;
106187
+ for (let x = 0; x < width; x++) {
106188
+ const srcBase = srcRowBase + x * bpp;
106189
+ output2[dstBase + x * 6 + 0] = rawPixels[srcBase + 1] ?? 0;
106190
+ output2[dstBase + x * 6 + 1] = rawPixels[srcBase + 0] ?? 0;
106191
+ output2[dstBase + x * 6 + 2] = rawPixels[srcBase + 3] ?? 0;
106192
+ output2[dstBase + x * 6 + 3] = rawPixels[srcBase + 2] ?? 0;
106193
+ output2[dstBase + x * 6 + 4] = rawPixels[srcBase + 5] ?? 0;
106194
+ output2[dstBase + x * 6 + 5] = rawPixels[srcBase + 4] ?? 0;
106195
+ }
106196
+ }
106197
+ return { width, height, data: output2 };
106198
+ }
106199
+ function buildSrgbToHdrLut(transfer) {
106200
+ const lut = new Uint16Array(256);
106201
+ const hlgA = 0.17883277;
106202
+ const hlgB = 1 - 4 * hlgA;
106203
+ const hlgC = 0.5 - hlgA * Math.log(4 * hlgA);
106204
+ const pqM1 = 0.1593017578125;
106205
+ const pqM2 = 78.84375;
106206
+ const pqC1 = 0.8359375;
106207
+ const pqC2 = 18.8515625;
106208
+ const pqC3 = 18.6875;
106209
+ const pqMaxNits = 1e4;
106210
+ const sdrNits = 203;
106211
+ for (let i = 0; i < 256; i++) {
106212
+ const v = i / 255;
106213
+ const linear = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
106214
+ let signal;
106215
+ if (transfer === "hlg") {
106216
+ signal = linear <= 1 / 12 ? Math.sqrt(3 * linear) : hlgA * Math.log(12 * linear - hlgB) + hlgC;
106217
+ } else {
106218
+ const Lp = Math.max(0, linear * sdrNits / pqMaxNits);
106219
+ const Lm1 = Math.pow(Lp, pqM1);
106220
+ signal = Math.pow((pqC1 + pqC2 * Lm1) / (1 + pqC3 * Lm1), pqM2);
106221
+ }
106222
+ lut[i] = Math.min(65535, Math.round(signal * 65535));
106223
+ }
106224
+ return lut;
106225
+ }
106226
+ var SRGB_TO_HLG = buildSrgbToHdrLut("hlg");
106227
+ var SRGB_TO_PQ = buildSrgbToHdrLut("pq");
106228
+ function getSrgbToHdrLut(transfer) {
106229
+ return transfer === "pq" ? SRGB_TO_PQ : SRGB_TO_HLG;
106230
+ }
106231
+ function blitRgba8OverRgb48le(domRgba, canvas, width, height, transfer = "hlg") {
106232
+ const pixelCount = width * height;
106233
+ const lut = getSrgbToHdrLut(transfer);
106234
+ for (let i = 0; i < pixelCount; i++) {
106235
+ const da = domRgba[i * 4 + 3] ?? 0;
106236
+ if (da === 0) {
106237
+ continue;
106238
+ } else if (da === 255) {
106239
+ const r16 = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
106240
+ const g16 = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
106241
+ const b16 = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
106242
+ canvas.writeUInt16LE(r16, i * 6);
106243
+ canvas.writeUInt16LE(g16, i * 6 + 2);
106244
+ canvas.writeUInt16LE(b16, i * 6 + 4);
106245
+ } else {
106246
+ const alpha = da / 255;
106247
+ const invAlpha = 1 - alpha;
106248
+ const hdrR = (canvas[i * 6 + 0] ?? 0) | (canvas[i * 6 + 1] ?? 0) << 8;
106249
+ const hdrG = (canvas[i * 6 + 2] ?? 0) | (canvas[i * 6 + 3] ?? 0) << 8;
106250
+ const hdrB = (canvas[i * 6 + 4] ?? 0) | (canvas[i * 6 + 5] ?? 0) << 8;
106251
+ const domR = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
106252
+ const domG = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
106253
+ const domB = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
106254
+ canvas.writeUInt16LE(Math.round(domR * alpha + hdrR * invAlpha), i * 6);
106255
+ canvas.writeUInt16LE(Math.round(domG * alpha + hdrG * invAlpha), i * 6 + 2);
106256
+ canvas.writeUInt16LE(Math.round(domB * alpha + hdrB * invAlpha), i * 6 + 4);
106257
+ }
106258
+ }
106259
+ }
106260
+ function cornerAlpha(px, py, cx, cy, r) {
106261
+ const dx = px - cx;
106262
+ const dy = py - cy;
106263
+ const dist = Math.sqrt(dx * dx + dy * dy);
106264
+ if (dist > r + 0.5) return 0;
106265
+ if (dist > r - 0.5) return r + 0.5 - dist;
106266
+ return 1;
106267
+ }
106268
+ function roundedRectAlpha(px, py, w, h, radii) {
106269
+ const [tl, tr, br, bl] = radii;
106270
+ if (px < tl && py < tl) return cornerAlpha(px, py, tl, tl, tl);
106271
+ if (px >= w - tr && py < tr) return cornerAlpha(px, py, w - tr, tr, tr);
106272
+ if (px >= w - br && py >= h - br) return cornerAlpha(px, py, w - br, h - br, br);
106273
+ if (px < bl && py >= h - bl) return cornerAlpha(px, py, bl, h - bl, bl);
106274
+ return 1;
106275
+ }
106276
+ function blitRgb48leRegion(canvas, source2, dx, dy, sw, sh, canvasWidth, canvasHeight, opacity, borderRadius) {
106277
+ if (sw <= 0 || sh <= 0) return;
106278
+ const op = opacity ?? 1;
106279
+ const x0 = Math.max(0, dx);
106280
+ const y0 = Math.max(0, dy);
106281
+ const x1 = Math.min(canvasWidth, dx + sw);
106282
+ const y1 = Math.min(canvasHeight, dy + sh);
106283
+ if (x0 >= x1 || y0 >= y1) return;
106284
+ const clippedW = x1 - x0;
106285
+ const srcOffsetX = x0 - dx;
106286
+ const srcOffsetY = y0 - dy;
106287
+ const hasMask = borderRadius !== void 0;
106288
+ if (op >= 0.999 && !hasMask) {
106289
+ for (let y = 0; y < y1 - y0; y++) {
106290
+ const srcRowOff = ((srcOffsetY + y) * sw + srcOffsetX) * 6;
106291
+ const dstRowOff = ((y0 + y) * canvasWidth + x0) * 6;
106292
+ source2.copy(canvas, dstRowOff, srcRowOff, srcRowOff + clippedW * 6);
106293
+ }
106294
+ } else {
106295
+ for (let y = 0; y < y1 - y0; y++) {
106296
+ for (let x = 0; x < clippedW; x++) {
106297
+ let effectiveOp = op;
106298
+ if (hasMask) {
106299
+ const ma = roundedRectAlpha(srcOffsetX + x, srcOffsetY + y, sw, sh, borderRadius);
106300
+ if (ma <= 0) continue;
106301
+ effectiveOp *= ma;
106302
+ }
106303
+ const srcOff = ((srcOffsetY + y) * sw + srcOffsetX + x) * 6;
106304
+ const dstOff = ((y0 + y) * canvasWidth + x0 + x) * 6;
106305
+ if (effectiveOp >= 0.999) {
106306
+ source2.copy(canvas, dstOff, srcOff, srcOff + 6);
106307
+ } else {
106308
+ const invEff = 1 - effectiveOp;
106309
+ const sr = source2.readUInt16LE(srcOff);
106310
+ const sg = source2.readUInt16LE(srcOff + 2);
106311
+ const sb = source2.readUInt16LE(srcOff + 4);
106312
+ const dr = canvas.readUInt16LE(dstOff);
106313
+ const dg = canvas.readUInt16LE(dstOff + 2);
106314
+ const db = canvas.readUInt16LE(dstOff + 4);
106315
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
106316
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
106317
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
106318
+ }
106319
+ }
106320
+ }
106321
+ }
106322
+ }
106323
+
106324
+ // ../engine/src/utils/layerCompositor.ts
106325
+ function groupIntoLayers(elements) {
106326
+ const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
106327
+ const layers = [];
106328
+ for (const el of sorted) {
106329
+ if (el.isHdr) {
106330
+ layers.push({ type: "hdr", element: el });
106331
+ } else {
106332
+ const last2 = layers[layers.length - 1];
106333
+ if (last2 && last2.type === "dom") {
106334
+ last2.elementIds.push(el.id);
106335
+ } else {
106336
+ layers.push({ type: "dom", elementIds: [el.id] });
106337
+ }
106338
+ }
106339
+ }
106340
+ return layers;
106341
+ }
106342
+
105768
106343
  // src/services/renderOrchestrator.ts
105769
106344
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
105770
106345
  import { randomUUID } from "crypto";
@@ -105871,6 +106446,102 @@ var MIME_TYPES = {
105871
106446
  ".ttf": "font/ttf",
105872
106447
  ".otf": "font/otf"
105873
106448
  };
106449
+ var VIRTUAL_TIME_SHIM = String.raw`(function() {
106450
+ if (window.__HF_VIRTUAL_TIME__) return;
106451
+
106452
+ var virtualNowMs = 0;
106453
+ var rafId = 1;
106454
+ var rafQueue = [];
106455
+ var OriginalDate = Date;
106456
+ var originalSetTimeout = window.setTimeout.bind(window);
106457
+ var originalClearTimeout = window.clearTimeout.bind(window);
106458
+ var originalSetInterval = window.setInterval.bind(window);
106459
+ var originalClearInterval = window.clearInterval.bind(window);
106460
+ var originalRequestAnimationFrame = window.requestAnimationFrame
106461
+ ? window.requestAnimationFrame.bind(window)
106462
+ : null;
106463
+ var originalCancelAnimationFrame = window.cancelAnimationFrame
106464
+ ? window.cancelAnimationFrame.bind(window)
106465
+ : null;
106466
+
106467
+ function flushAnimationFrame() {
106468
+ if (!rafQueue.length) return;
106469
+ var current = rafQueue.slice();
106470
+ rafQueue.length = 0;
106471
+ for (var i = 0; i < current.length; i++) {
106472
+ var entry = current[i];
106473
+ if (entry.cancelled) continue;
106474
+ try {
106475
+ entry.callback(virtualNowMs);
106476
+ } catch {}
106477
+ }
106478
+ }
106479
+
106480
+ function VirtualDate() {
106481
+ var args = Array.prototype.slice.call(arguments);
106482
+ if (!(this instanceof VirtualDate)) {
106483
+ return OriginalDate.apply(null, args.length ? args : [virtualNowMs]);
106484
+ }
106485
+ var instance = args.length ? new (Function.prototype.bind.apply(OriginalDate, [null].concat(args)))() : new OriginalDate(virtualNowMs);
106486
+ Object.setPrototypeOf(instance, VirtualDate.prototype);
106487
+ return instance;
106488
+ }
106489
+
106490
+ VirtualDate.prototype = OriginalDate.prototype;
106491
+ Object.setPrototypeOf(VirtualDate, OriginalDate);
106492
+ VirtualDate.now = function() { return virtualNowMs; };
106493
+ VirtualDate.parse = OriginalDate.parse.bind(OriginalDate);
106494
+ VirtualDate.UTC = OriginalDate.UTC.bind(OriginalDate);
106495
+
106496
+ try {
106497
+ Object.defineProperty(window, "Date", {
106498
+ configurable: true,
106499
+ writable: true,
106500
+ value: VirtualDate,
106501
+ });
106502
+ } catch {}
106503
+
106504
+ if (window.performance && typeof window.performance.now === "function") {
106505
+ try {
106506
+ Object.defineProperty(window.performance, "now", {
106507
+ configurable: true,
106508
+ value: function() { return virtualNowMs; },
106509
+ });
106510
+ } catch {}
106511
+ }
106512
+
106513
+ window.requestAnimationFrame = function(callback) {
106514
+ if (typeof callback !== "function") return 0;
106515
+ var entry = { id: rafId++, callback: callback, cancelled: false };
106516
+ rafQueue.push(entry);
106517
+ return entry.id;
106518
+ };
106519
+ window.cancelAnimationFrame = function(id) {
106520
+ for (var i = 0; i < rafQueue.length; i++) {
106521
+ if (rafQueue[i].id === id) {
106522
+ rafQueue[i].cancelled = true;
106523
+ }
106524
+ }
106525
+ };
106526
+
106527
+ window.__HF_VIRTUAL_TIME__ = {
106528
+ originalSetTimeout: originalSetTimeout,
106529
+ originalClearTimeout: originalClearTimeout,
106530
+ originalSetInterval: originalSetInterval,
106531
+ originalClearInterval: originalClearInterval,
106532
+ originalRequestAnimationFrame: originalRequestAnimationFrame,
106533
+ originalCancelAnimationFrame: originalCancelAnimationFrame,
106534
+ seekToTime: function(nextTimeMs) {
106535
+ var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
106536
+ virtualNowMs = safeTimeMs;
106537
+ flushAnimationFrame();
106538
+ return virtualNowMs;
106539
+ },
106540
+ getTime: function() {
106541
+ return virtualNowMs;
106542
+ },
106543
+ };
106544
+ })();`;
105874
106545
  var RENDER_SEEK_MODE = process.env.PRODUCER_RUNTIME_RENDER_SEEK_MODE === "strict-boundary" ? "strict-boundary" : "preview-phase";
105875
106546
  var RENDER_SEEK_DIAGNOSTICS = process.env.PRODUCER_DEBUG_SEEK_DIAGNOSTICS === "true";
105876
106547
  var RENDER_SEEK_STEP = Math.max(
@@ -105882,6 +106553,10 @@ var RENDER_SEEK_OFFSET_FRACTION = Math.max(
105882
106553
  Math.min(0.95, Number(process.env.PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION || 0.5))
105883
106554
  );
105884
106555
  var RENDER_MODE_SCRIPT = `(function() {
106556
+ var __realSetTimeout =
106557
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetTimeout === "function"
106558
+ ? window.__HF_VIRTUAL_TIME__.originalSetTimeout
106559
+ : window.setTimeout.bind(window);
105885
106560
  var __seekMode = ${JSON.stringify(RENDER_SEEK_MODE)};
105886
106561
  var __seekDiagnostics = ${RENDER_SEEK_DIAGNOSTICS ? "true" : "false"};
105887
106562
  var __seekStep = ${RENDER_SEEK_STEP};
@@ -105975,40 +106650,88 @@ var RENDER_MODE_SCRIPT = `(function() {
105975
106650
  window.__renderReady = true;
105976
106651
  return;
105977
106652
  }
105978
- setTimeout(waitForPlayer, 50);
106653
+ __realSetTimeout(waitForPlayer, 50);
105979
106654
  return;
105980
106655
  }
105981
106656
  if (installMediaFallbackPlayer()) {
105982
106657
  return;
105983
106658
  }
105984
- setTimeout(waitForPlayer, 50);
106659
+ __realSetTimeout(waitForPlayer, 50);
105985
106660
  }
105986
106661
  waitForPlayer();
105987
106662
  })();`;
106663
+ var HF_EARLY_STUB = `(function() {
106664
+ if (typeof window === "undefined") return;
106665
+ if (!window.__hf) window.__hf = {};
106666
+ })();`;
105988
106667
  var HF_BRIDGE_SCRIPT = `(function() {
106668
+ var __realSetInterval =
106669
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
106670
+ ? window.__HF_VIRTUAL_TIME__.originalSetInterval
106671
+ : window.setInterval.bind(window);
106672
+ var __realClearInterval =
106673
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalClearInterval === "function"
106674
+ ? window.__HF_VIRTUAL_TIME__.originalClearInterval
106675
+ : window.clearInterval.bind(window);
105989
106676
  function getDeclaredDuration() {
105990
106677
  var root = document.querySelector('[data-composition-id]');
105991
106678
  if (!root) return 0;
105992
106679
  var d = Number(root.getAttribute('data-duration'));
105993
106680
  return Number.isFinite(d) && d > 0 ? d : 0;
105994
106681
  }
106682
+ function seekSameOriginChildFrames(frameWindow, nextTimeMs) {
106683
+ var frames;
106684
+ try {
106685
+ frames = frameWindow.frames;
106686
+ } catch (_error) {
106687
+ return;
106688
+ }
106689
+ if (!frames || typeof frames.length !== "number") return;
106690
+ for (var i = 0; i < frames.length; i++) {
106691
+ var childWindow = null;
106692
+ try {
106693
+ childWindow = frames[i];
106694
+ if (!childWindow || childWindow === frameWindow) continue;
106695
+ if (
106696
+ childWindow.__HF_VIRTUAL_TIME__ &&
106697
+ typeof childWindow.__HF_VIRTUAL_TIME__.seekToTime === "function"
106698
+ ) {
106699
+ childWindow.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106700
+ }
106701
+ } catch (_error) {
106702
+ continue;
106703
+ }
106704
+ seekSameOriginChildFrames(childWindow, nextTimeMs);
106705
+ }
106706
+ }
105995
106707
  function bridge() {
105996
106708
  var p = window.__player;
105997
106709
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
105998
106710
  return false;
105999
106711
  }
106000
- window.__hf = {
106001
- get duration() {
106712
+ var hf = window.__hf || {};
106713
+ Object.defineProperty(hf, "duration", {
106714
+ configurable: true,
106715
+ enumerable: true,
106716
+ get: function() {
106002
106717
  var d = p.getDuration();
106003
106718
  return d > 0 ? d : getDeclaredDuration();
106004
106719
  },
106005
- seek: function(t) { p.renderSeek(t); },
106720
+ });
106721
+ hf.seek = function(t) {
106722
+ p.renderSeek(t);
106723
+ var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
106724
+ if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
106725
+ window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106726
+ }
106727
+ seekSameOriginChildFrames(window, nextTimeMs);
106006
106728
  };
106729
+ window.__hf = hf;
106007
106730
  return true;
106008
106731
  }
106009
106732
  if (bridge()) return;
106010
- var iv = setInterval(function() {
106011
- if (bridge()) clearInterval(iv);
106733
+ var iv = __realSetInterval(function() {
106734
+ if (bridge()) __realClearInterval(iv);
106012
106735
  }, 50);
106013
106736
  })();`;
106014
106737
  function stripEmbeddedRuntimeScripts(html) {
@@ -106070,8 +106793,22 @@ function injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbedded) {
106070
106793
  }
106071
106794
  return html;
106072
106795
  }
106796
+ function injectScriptsAtHeadStart(html, scripts) {
106797
+ if (scripts.length === 0) return html;
106798
+ const headTags = scripts.map((src) => `<script>${src}</script>`).join("\n");
106799
+ if (html.includes("<head")) {
106800
+ return html.replace(/<head\b[^>]*>/i, (match2) => `${match2}
106801
+ ${headTags}`);
106802
+ }
106803
+ if (html.includes("<body")) {
106804
+ return html.replace("<body", () => `${headTags}
106805
+ <body`);
106806
+ }
106807
+ return headTags + "\n" + html;
106808
+ }
106073
106809
  function createFileServer2(options) {
106074
106810
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
106811
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
106075
106812
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
106076
106813
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
106077
106814
  const app = new Hono2();
@@ -106095,7 +106832,11 @@ function createFileServer2(options) {
106095
106832
  if (ext === ".html") {
106096
106833
  const rawHtml = readFileSync6(filePath, "utf-8");
106097
106834
  const isIndex = relativePath === "index.html";
106098
- const html = isIndex ? injectScriptsIntoHtml(rawHtml, headScripts, bodyScripts, stripEmbeddedRuntime) : rawHtml;
106835
+ let html = rawHtml;
106836
+ if (preHeadScripts.length > 0) {
106837
+ html = injectScriptsAtHeadStart(html, preHeadScripts);
106838
+ }
106839
+ html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
106099
106840
  return c.text(html, 200, { "Content-Type": contentType });
106100
106841
  }
106101
106842
  const content = readFileSync6(filePath);
@@ -106543,6 +107284,37 @@ function dedupeElementsById(elements) {
106543
107284
  }
106544
107285
  return Array.from(deduped.values());
106545
107286
  }
107287
+ var INLINE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
107288
+ function stripJsComments(source2) {
107289
+ return source2.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
107290
+ }
107291
+ function detectRenderModeHints(html) {
107292
+ const reasons = [];
107293
+ const { document: document2 } = parseHTML(html);
107294
+ if (document2.querySelector("iframe")) {
107295
+ reasons.push({
107296
+ code: "iframe",
107297
+ message: "Detected <iframe> in the composition DOM. Nested iframe animation is routed through screenshot capture mode for compatibility."
107298
+ });
107299
+ }
107300
+ let scriptMatch;
107301
+ const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
107302
+ while ((scriptMatch = scriptPattern.exec(html)) !== null) {
107303
+ const attrs = scriptMatch[1] || "";
107304
+ if (/\bsrc\s*=/i.test(attrs)) continue;
107305
+ const content = stripJsComments(scriptMatch[2] || "");
107306
+ if (!/requestAnimationFrame\s*\(/.test(content)) continue;
107307
+ reasons.push({
107308
+ code: "requestAnimationFrame",
107309
+ message: "Detected raw requestAnimationFrame() in an inline script. This render is routed through screenshot capture mode with virtual time enabled."
107310
+ });
107311
+ break;
107312
+ }
107313
+ return {
107314
+ recommendScreenshot: reasons.length > 0,
107315
+ reasons
107316
+ };
107317
+ }
106546
107318
  async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
106547
107319
  let filePath = src;
106548
107320
  if (isHttpUrl(src)) {
@@ -107106,6 +107878,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107106
107878
  /(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
107107
107879
  "$1"
107108
107880
  );
107881
+ const renderModeHints = detectRenderModeHints(sanitizedHtml);
107109
107882
  const coalescedHtml = await injectDeterministicFontFaces(
107110
107883
  coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
107111
107884
  );
@@ -107149,7 +107922,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107149
107922
  externalAssets,
107150
107923
  width,
107151
107924
  height,
107152
- staticDuration
107925
+ staticDuration,
107926
+ renderModeHints
107153
107927
  };
107154
107928
  }
107155
107929
  async function discoverMediaFromBrowser(page) {
@@ -107243,7 +108017,8 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
107243
108017
  subCompositions,
107244
108018
  videos,
107245
108019
  audios,
107246
- unresolvedCompositions: remaining
108020
+ unresolvedCompositions: remaining,
108021
+ renderModeHints: compiled.renderModeHints
107247
108022
  };
107248
108023
  }
107249
108024
 
@@ -107293,6 +108068,24 @@ async function safeCleanup(label, fn, log = defaultLogger) {
107293
108068
  });
107294
108069
  }
107295
108070
  }
108071
+ var frameDirMaxIndexCache = /* @__PURE__ */ new Map();
108072
+ var FRAME_FILENAME_RE = /^frame_(\d+)\.png$/;
108073
+ function getMaxFrameIndex(frameDir) {
108074
+ const cached = frameDirMaxIndexCache.get(frameDir);
108075
+ if (cached !== void 0) return cached;
108076
+ let max = 0;
108077
+ try {
108078
+ for (const name of readdirSync6(frameDir)) {
108079
+ const m = FRAME_FILENAME_RE.exec(name);
108080
+ if (!m) continue;
108081
+ const n = Number(m[1]);
108082
+ if (Number.isFinite(n) && n > max) max = n;
108083
+ }
108084
+ } catch {
108085
+ }
108086
+ frameDirMaxIndexCache.set(frameDir, max);
108087
+ return max;
108088
+ }
107296
108089
  var RenderCancelledError = class extends Error {
107297
108090
  reason;
107298
108091
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -107380,11 +108173,20 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
107380
108173
  end: a.end,
107381
108174
  mediaStart: a.mediaStart
107382
108175
  })),
107383
- subCompositions: Array.from(compiled.subCompositions.keys())
108176
+ subCompositions: Array.from(compiled.subCompositions.keys()),
108177
+ renderModeHints: compiled.renderModeHints
107384
108178
  };
107385
108179
  writeFileSync4(join15(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
107386
108180
  }
107387
108181
  }
108182
+ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
108183
+ if (cfg.forceScreenshot || !compiled.renderModeHints.recommendScreenshot) return;
108184
+ cfg.forceScreenshot = true;
108185
+ log.warn("Auto-selected screenshot capture mode for render compatibility", {
108186
+ reasonCodes: compiled.renderModeHints.reasons.map((reason) => reason.code),
108187
+ reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
108188
+ });
108189
+ }
107388
108190
  function createRenderJob(config2) {
107389
108191
  return {
107390
108192
  id: randomUUID(),
@@ -107497,6 +108299,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107497
108299
  let compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"));
107498
108300
  assertNotAborted();
107499
108301
  perfStages.compileOnlyMs = Date.now() - compileStart;
108302
+ applyRenderModeHints(cfg, compiled, log);
107500
108303
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
107501
108304
  log.info("Compiled composition metadata", {
107502
108305
  entryFile,
@@ -107504,7 +108307,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107504
108307
  width: compiled.width,
107505
108308
  height: compiled.height,
107506
108309
  videoCount: compiled.videos.length,
107507
- audioCount: compiled.audios.length
108310
+ audioCount: compiled.audios.length,
108311
+ renderModeHints: compiled.renderModeHints
107508
108312
  });
107509
108313
  const composition = {
107510
108314
  duration: compiled.staticDuration,
@@ -107524,7 +108328,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107524
108328
  fileServer = await createFileServer2({
107525
108329
  projectDir,
107526
108330
  compiledDir: join15(workDir, "compiled"),
107527
- port: 0
108331
+ port: 0,
108332
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
107528
108333
  });
107529
108334
  assertNotAborted();
107530
108335
  const captureOpts = {
@@ -107680,7 +108485,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107680
108485
  }
107681
108486
  }
107682
108487
  }
107683
- } catch {
108488
+ } catch (err) {
108489
+ log.warn("Failed to gather browser diagnostics for zero-duration composition", {
108490
+ error: err instanceof Error ? err.message : String(err)
108491
+ });
107684
108492
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
107685
108493
  }
107686
108494
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -107704,8 +108512,26 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107704
108512
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107705
108513
  let frameLookup = null;
107706
108514
  const compiledDir = join15(workDir, "compiled");
108515
+ let extractionResult = null;
108516
+ const nativeHdrVideoIds = /* @__PURE__ */ new Set();
108517
+ if (composition.videos.length > 0) {
108518
+ await Promise.all(
108519
+ composition.videos.map(async (v) => {
108520
+ let videoPath = v.src;
108521
+ if (!videoPath.startsWith("/")) {
108522
+ const fromCompiled = existsSync15(join15(compiledDir, videoPath)) ? join15(compiledDir, videoPath) : join15(projectDir, videoPath);
108523
+ videoPath = fromCompiled;
108524
+ }
108525
+ if (!existsSync15(videoPath)) return;
108526
+ const meta = await extractVideoMetadata(videoPath);
108527
+ if (isHdrColorSpace(meta.colorSpace)) {
108528
+ nativeHdrVideoIds.add(v.id);
108529
+ }
108530
+ })
108531
+ );
108532
+ }
107707
108533
  if (composition.videos.length > 0) {
107708
- const extractionResult = await extractAllVideoFrames(
108534
+ extractionResult = await extractAllVideoFrames(
107709
108535
  composition.videos,
107710
108536
  projectDir,
107711
108537
  { fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
@@ -107740,6 +108566,23 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107740
108566
  } else {
107741
108567
  perfStages.videoExtractMs = Date.now() - stage2Start;
107742
108568
  }
108569
+ let effectiveHdr;
108570
+ if (frameLookup) {
108571
+ const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
108572
+ const info = analyzeCompositionHdr(colorSpaces);
108573
+ if (info.hasHdr && info.dominantTransfer) {
108574
+ effectiveHdr = { transfer: info.dominantTransfer };
108575
+ }
108576
+ }
108577
+ if (effectiveHdr && outputFormat !== "mp4") {
108578
+ log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
108579
+ effectiveHdr = void 0;
108580
+ }
108581
+ if (effectiveHdr) {
108582
+ log.info(
108583
+ `[Render] HDR source detected \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
108584
+ );
108585
+ }
107743
108586
  const stage3Start = Date.now();
107744
108587
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
107745
108588
  const audioOutputPath = join15(workDir, "audio.aac");
@@ -107767,7 +108610,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107767
108610
  fileServer = await createFileServer2({
107768
108611
  projectDir,
107769
108612
  compiledDir: join15(workDir, "compiled"),
107770
- port: 0
108613
+ port: 0,
108614
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
107771
108615
  });
107772
108616
  assertNotAborted();
107773
108617
  }
@@ -107784,218 +108628,398 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107784
108628
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
107785
108629
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
107786
108630
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
107787
- const preset = getEncoderPreset(job.config.quality, outputFormat);
107788
- const effectiveQuality = job.config.crf ?? preset.quality;
107789
- const effectiveBitrate = job.config.videoBitrate;
107790
- const baseEncoderOpts = {
107791
- fps: job.config.fps,
107792
- width,
107793
- height,
107794
- codec: preset.codec,
107795
- preset: preset.preset,
107796
- quality: effectiveQuality,
107797
- bitrate: effectiveBitrate,
107798
- pixelFormat: preset.pixelFormat,
107799
- useGpu: job.config.useGpu
107800
- };
108631
+ const hasHdrVideo = effectiveHdr && composition.videos.length > 0 && frameLookup;
108632
+ const encoderHdr = hasHdrVideo ? effectiveHdr : void 0;
108633
+ const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
107801
108634
  job.framesRendered = 0;
107802
- let streamingEncoder = null;
107803
- if (enableStreamingEncode) {
107804
- streamingEncoder = await spawnStreamingEncoder(
108635
+ if (hasHdrVideo) {
108636
+ log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
108637
+ const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
108638
+ const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
108639
+ for (const v of composition.videos) {
108640
+ if (!hdrVideoIds.includes(v.id)) continue;
108641
+ let srcPath = v.src;
108642
+ if (!srcPath.startsWith("/")) {
108643
+ const fromCompiled = join15(compiledDir, srcPath);
108644
+ srcPath = existsSync15(fromCompiled) ? fromCompiled : join15(projectDir, srcPath);
108645
+ }
108646
+ hdrVideoSrcPaths.set(v.id, srcPath);
108647
+ }
108648
+ const domSession = await createCaptureSession(
108649
+ fileServer.url,
108650
+ framesDir,
108651
+ captureOptions,
108652
+ createVideoFrameInjector(frameLookup),
108653
+ cfg
108654
+ );
108655
+ await initializeSession(domSession);
108656
+ assertNotAborted();
108657
+ lastBrowserConsole = domSession.browserConsoleBuffer;
108658
+ await initTransparentBackground(domSession.page);
108659
+ const hdrEncoder = await spawnStreamingEncoder(
107805
108660
  videoOnlyPath,
107806
108661
  {
107807
- ...baseEncoderOpts,
107808
- imageFormat: captureOptions.format || "jpeg"
108662
+ fps: job.config.fps,
108663
+ width,
108664
+ height,
108665
+ codec: preset.codec,
108666
+ preset: preset.preset,
108667
+ quality: preset.quality,
108668
+ pixelFormat: preset.pixelFormat,
108669
+ hdr: preset.hdr,
108670
+ rawInputFormat: "rgb48le"
107809
108671
  },
107810
- abortSignal
108672
+ abortSignal,
108673
+ { ffmpegStreamingTimeout: 36e5 }
107811
108674
  );
107812
108675
  assertNotAborted();
107813
- }
107814
- if (enableStreamingEncode && streamingEncoder) {
107815
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
107816
- const currentEncoder = streamingEncoder;
107817
- if (workerCount > 1) {
107818
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
107819
- const onFrameBuffer = async (frameIndex, buffer) => {
107820
- await reorderBuffer.waitForFrame(frameIndex);
107821
- currentEncoder.writeFrame(buffer);
107822
- reorderBuffer.advanceTo(frameIndex + 1);
107823
- };
107824
- await executeParallelCapture(
107825
- fileServer.url,
107826
- workDir,
107827
- tasks,
107828
- captureOptions,
107829
- () => createVideoFrameInjector(frameLookup),
107830
- abortSignal,
107831
- (progress) => {
107832
- job.framesRendered = progress.capturedFrames;
107833
- const frameProgress = progress.capturedFrames / progress.totalFrames;
107834
- const progressPct = 25 + frameProgress * 55;
107835
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
107836
- updateJobStatus(
107837
- job,
107838
- "rendering",
107839
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
107840
- Math.round(progressPct),
107841
- onProgress
108676
+ const { execSync: execSync2 } = await import("child_process");
108677
+ const hdrFrameDirs = /* @__PURE__ */ new Map();
108678
+ for (const [videoId, srcPath] of hdrVideoSrcPaths) {
108679
+ const video = composition.videos.find((v) => v.id === videoId);
108680
+ if (!video) continue;
108681
+ const frameDir = join15(framesDir, `hdr_${videoId}`);
108682
+ mkdirSync10(frameDir, { recursive: true });
108683
+ const duration = video.end - video.start;
108684
+ try {
108685
+ execSync2(
108686
+ `ffmpeg -ss ${video.mediaStart} -i "${srcPath}" -t ${duration} -r ${job.config.fps} -vf "scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}" -pix_fmt rgb48le -c:v png "${join15(frameDir, "frame_%04d.png")}"`,
108687
+ { maxBuffer: 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
108688
+ );
108689
+ } catch (err) {
108690
+ log.warn("HDR frame pre-extraction failed; loop will fill with black", {
108691
+ videoId,
108692
+ srcPath,
108693
+ error: err instanceof Error ? err.message : String(err)
108694
+ });
108695
+ }
108696
+ hdrFrameDirs.set(videoId, frameDir);
108697
+ }
108698
+ assertNotAborted();
108699
+ try {
108700
+ const beforeCaptureHook = domSession.onBeforeCapture;
108701
+ for (let i = 0; i < job.totalFrames; i++) {
108702
+ assertNotAborted();
108703
+ const time = i / job.config.fps;
108704
+ await domSession.page.evaluate((t) => {
108705
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
108706
+ }, time);
108707
+ if (beforeCaptureHook) {
108708
+ await beforeCaptureHook(domSession.page, time);
108709
+ }
108710
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
108711
+ const layers = groupIntoLayers(stackingInfo);
108712
+ if (i % 30 === 0) {
108713
+ const hdrEl = stackingInfo.find((e) => e.isHdr);
108714
+ const hdrInLayers = layers.some((l) => l.type === "hdr");
108715
+ log.debug("[Render] HDR layer composite frame", {
108716
+ frame: i,
108717
+ time: time.toFixed(2),
108718
+ hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
108719
+ hdrLayerPresent: hdrInLayers,
108720
+ layerCount: layers.length
108721
+ });
108722
+ }
108723
+ const canvas = Buffer.alloc(width * height * 6);
108724
+ for (const layer of layers) {
108725
+ if (layer.type === "hdr") {
108726
+ const el = layer.element;
108727
+ const frameDir = hdrFrameDirs.get(el.id);
108728
+ const video = composition.videos.find((v) => v.id === el.id);
108729
+ if (!frameDir || !video) continue;
108730
+ const videoFrameIndex = Math.round((time - video.start) * job.config.fps) + 1;
108731
+ const maxIndex = getMaxFrameIndex(frameDir);
108732
+ const inBounds = videoFrameIndex >= 1 && (maxIndex === 0 || videoFrameIndex <= maxIndex);
108733
+ const framePath = inBounds ? join15(frameDir, `frame_${String(videoFrameIndex).padStart(4, "0")}.png`) : null;
108734
+ if (framePath !== null && existsSync15(framePath)) {
108735
+ try {
108736
+ const hdrRgb = decodePngToRgb48le(readFileSync9(framePath)).data;
108737
+ blitRgb48leRegion(
108738
+ canvas,
108739
+ hdrRgb,
108740
+ el.x,
108741
+ el.y,
108742
+ el.width,
108743
+ el.height,
108744
+ width,
108745
+ height,
108746
+ el.opacity < 0.999 ? el.opacity : void 0
108747
+ );
108748
+ } catch (err) {
108749
+ log.warn("HDR layer decode/blit failed; skipping layer for frame", {
108750
+ frameIndex: i,
108751
+ videoId: el.id,
108752
+ framePath,
108753
+ error: err instanceof Error ? err.message : String(err)
108754
+ });
108755
+ }
108756
+ }
108757
+ } else {
108758
+ const allElementIds = stackingInfo.map((e) => e.id);
108759
+ const layerIds = new Set(layer.elementIds);
108760
+ const hideIds = allElementIds.filter(
108761
+ (id) => !layerIds.has(id) || nativeHdrVideoIds.has(id)
107842
108762
  );
108763
+ await hideVideoElements(domSession.page, hideIds);
108764
+ const domPng = await captureAlphaPng(domSession.page, width, height);
108765
+ await showVideoElements(domSession.page, hideIds);
108766
+ await domSession.page.evaluate((t) => {
108767
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
108768
+ }, time);
108769
+ try {
108770
+ const { data: domRgba } = decodePng(domPng);
108771
+ const hdrTransfer = effectiveHdr ? effectiveHdr.transfer : "hlg";
108772
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, hdrTransfer);
108773
+ } catch (err) {
108774
+ log.warn("DOM layer decode/blit failed; skipping overlay for frame", {
108775
+ frameIndex: i,
108776
+ layerIds: layer.elementIds,
108777
+ error: err instanceof Error ? err.message : String(err)
108778
+ });
108779
+ }
107843
108780
  }
107844
- },
107845
- onFrameBuffer,
107846
- cfg
107847
- );
107848
- if (probeSession) {
107849
- lastBrowserConsole = probeSession.browserConsoleBuffer;
107850
- await closeCaptureSession(probeSession);
107851
- probeSession = null;
107852
- }
107853
- } else {
107854
- const videoInjector = createVideoFrameInjector(frameLookup);
107855
- const session = probeSession ?? await createCaptureSession(
107856
- fileServer.url,
107857
- framesDir,
107858
- captureOptions,
107859
- videoInjector,
107860
- cfg
107861
- );
107862
- if (probeSession) {
107863
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
107864
- probeSession = null;
107865
- }
107866
- try {
107867
- if (!session.isInitialized) {
107868
- await initializeSession(session);
107869
108781
  }
107870
- assertNotAborted();
107871
- lastBrowserConsole = session.browserConsoleBuffer;
107872
- for (let i = 0; i < job.totalFrames; i++) {
107873
- assertNotAborted();
107874
- const time = i / job.config.fps;
107875
- const { buffer } = await captureFrameToBuffer(session, i, time);
107876
- await reorderBuffer.waitForFrame(i);
107877
- currentEncoder.writeFrame(buffer);
107878
- reorderBuffer.advanceTo(i + 1);
107879
- job.framesRendered = i + 1;
108782
+ hdrEncoder.writeFrame(canvas);
108783
+ job.framesRendered = i + 1;
108784
+ if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
107880
108785
  const frameProgress = (i + 1) / job.totalFrames;
107881
- const progress = 25 + frameProgress * 55;
107882
108786
  updateJobStatus(
107883
108787
  job,
107884
108788
  "rendering",
107885
- `Streaming frame ${i + 1}/${job.totalFrames}`,
107886
- Math.round(progress),
108789
+ `HDR composite frame ${i + 1}/${job.totalFrames}`,
108790
+ Math.round(25 + frameProgress * 55),
107887
108791
  onProgress
107888
108792
  );
107889
108793
  }
107890
- } finally {
107891
- lastBrowserConsole = session.browserConsoleBuffer;
107892
- await closeCaptureSession(session);
107893
108794
  }
108795
+ } finally {
108796
+ lastBrowserConsole = domSession.browserConsoleBuffer;
108797
+ await closeCaptureSession(domSession);
107894
108798
  }
107895
- const encodeResult = await currentEncoder.close();
108799
+ const hdrEncodeResult = await hdrEncoder.close();
107896
108800
  assertNotAborted();
107897
- if (!encodeResult.success) {
107898
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108801
+ if (!hdrEncodeResult.success) {
108802
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
107899
108803
  }
107900
108804
  perfStages.captureMs = Date.now() - stage4Start;
107901
- perfStages.encodeMs = encodeResult.durationMs;
108805
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
107902
108806
  } else {
107903
- if (workerCount > 1) {
107904
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
107905
- await executeParallelCapture(
107906
- fileServer.url,
107907
- workDir,
107908
- tasks,
107909
- captureOptions,
107910
- () => createVideoFrameInjector(frameLookup),
107911
- abortSignal,
107912
- (progress) => {
107913
- job.framesRendered = progress.capturedFrames;
107914
- const frameProgress = progress.capturedFrames / progress.totalFrames;
107915
- const progressPct = 25 + frameProgress * 45;
107916
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108807
+ let streamingEncoder = null;
108808
+ if (enableStreamingEncode) {
108809
+ streamingEncoder = await spawnStreamingEncoder(
108810
+ videoOnlyPath,
108811
+ {
108812
+ fps: job.config.fps,
108813
+ width,
108814
+ height,
108815
+ codec: preset.codec,
108816
+ preset: preset.preset,
108817
+ quality: preset.quality,
108818
+ pixelFormat: preset.pixelFormat,
108819
+ useGpu: job.config.useGpu,
108820
+ imageFormat: captureOptions.format || "jpeg",
108821
+ hdr: preset.hdr
108822
+ },
108823
+ abortSignal
108824
+ );
108825
+ assertNotAborted();
108826
+ }
108827
+ if (enableStreamingEncode && streamingEncoder) {
108828
+ const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
108829
+ const currentEncoder = streamingEncoder;
108830
+ if (workerCount > 1) {
108831
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108832
+ const onFrameBuffer = async (frameIndex, buffer) => {
108833
+ await reorderBuffer.waitForFrame(frameIndex);
108834
+ currentEncoder.writeFrame(buffer);
108835
+ reorderBuffer.advanceTo(frameIndex + 1);
108836
+ };
108837
+ await executeParallelCapture(
108838
+ fileServer.url,
108839
+ workDir,
108840
+ tasks,
108841
+ captureOptions,
108842
+ () => createVideoFrameInjector(frameLookup),
108843
+ abortSignal,
108844
+ (progress) => {
108845
+ job.framesRendered = progress.capturedFrames;
108846
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
108847
+ const progressPct = 25 + frameProgress * 55;
108848
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108849
+ updateJobStatus(
108850
+ job,
108851
+ "rendering",
108852
+ `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108853
+ Math.round(progressPct),
108854
+ onProgress
108855
+ );
108856
+ }
108857
+ },
108858
+ onFrameBuffer,
108859
+ cfg
108860
+ );
108861
+ if (probeSession) {
108862
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
108863
+ await closeCaptureSession(probeSession);
108864
+ probeSession = null;
108865
+ }
108866
+ } else {
108867
+ const videoInjector = createVideoFrameInjector(frameLookup);
108868
+ const session = probeSession ?? await createCaptureSession(
108869
+ fileServer.url,
108870
+ framesDir,
108871
+ captureOptions,
108872
+ videoInjector,
108873
+ cfg
108874
+ );
108875
+ if (probeSession) {
108876
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108877
+ probeSession = null;
108878
+ }
108879
+ try {
108880
+ if (!session.isInitialized) {
108881
+ await initializeSession(session);
108882
+ }
108883
+ assertNotAborted();
108884
+ lastBrowserConsole = session.browserConsoleBuffer;
108885
+ for (let i = 0; i < job.totalFrames; i++) {
108886
+ assertNotAborted();
108887
+ const time = i / job.config.fps;
108888
+ const { buffer } = await captureFrameToBuffer(session, i, time);
108889
+ await reorderBuffer.waitForFrame(i);
108890
+ currentEncoder.writeFrame(buffer);
108891
+ reorderBuffer.advanceTo(i + 1);
108892
+ job.framesRendered = i + 1;
108893
+ const frameProgress = (i + 1) / job.totalFrames;
108894
+ const progress = 25 + frameProgress * 55;
107917
108895
  updateJobStatus(
107918
108896
  job,
107919
108897
  "rendering",
107920
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
107921
- Math.round(progressPct),
108898
+ `Streaming frame ${i + 1}/${job.totalFrames}`,
108899
+ Math.round(progress),
107922
108900
  onProgress
107923
108901
  );
107924
108902
  }
107925
- },
107926
- void 0,
107927
- cfg
107928
- );
107929
- await mergeWorkerFrames(workDir, tasks, framesDir);
107930
- if (probeSession) {
107931
- lastBrowserConsole = probeSession.browserConsoleBuffer;
107932
- await closeCaptureSession(probeSession);
107933
- probeSession = null;
108903
+ } finally {
108904
+ lastBrowserConsole = session.browserConsoleBuffer;
108905
+ await closeCaptureSession(session);
108906
+ }
107934
108907
  }
107935
- } else {
107936
- const videoInjector = createVideoFrameInjector(frameLookup);
107937
- const session = probeSession ?? await createCaptureSession(
107938
- fileServer.url,
107939
- framesDir,
107940
- captureOptions,
107941
- videoInjector,
107942
- cfg
107943
- );
107944
- if (probeSession) {
107945
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
107946
- probeSession = null;
108908
+ const encodeResult = await currentEncoder.close();
108909
+ assertNotAborted();
108910
+ if (!encodeResult.success) {
108911
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
107947
108912
  }
107948
- try {
107949
- if (!session.isInitialized) {
107950
- await initializeSession(session);
108913
+ perfStages.captureMs = Date.now() - stage4Start;
108914
+ perfStages.encodeMs = encodeResult.durationMs;
108915
+ } else {
108916
+ if (workerCount > 1) {
108917
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108918
+ await executeParallelCapture(
108919
+ fileServer.url,
108920
+ workDir,
108921
+ tasks,
108922
+ captureOptions,
108923
+ () => createVideoFrameInjector(frameLookup),
108924
+ abortSignal,
108925
+ (progress) => {
108926
+ job.framesRendered = progress.capturedFrames;
108927
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
108928
+ const progressPct = 25 + frameProgress * 45;
108929
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108930
+ updateJobStatus(
108931
+ job,
108932
+ "rendering",
108933
+ `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108934
+ Math.round(progressPct),
108935
+ onProgress
108936
+ );
108937
+ }
108938
+ },
108939
+ void 0,
108940
+ cfg
108941
+ );
108942
+ await mergeWorkerFrames(workDir, tasks, framesDir);
108943
+ if (probeSession) {
108944
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
108945
+ await closeCaptureSession(probeSession);
108946
+ probeSession = null;
107951
108947
  }
107952
- assertNotAborted();
107953
- lastBrowserConsole = session.browserConsoleBuffer;
107954
- for (let i = 0; i < job.totalFrames; i++) {
107955
- assertNotAborted();
107956
- const time = i / job.config.fps;
107957
- await captureFrame(session, i, time);
107958
- job.framesRendered = i + 1;
107959
- const frameProgress = (i + 1) / job.totalFrames;
107960
- const progress = 25 + frameProgress * 45;
107961
- updateJobStatus(
107962
- job,
107963
- "rendering",
107964
- `Capturing frame ${i + 1}/${job.totalFrames}`,
107965
- Math.round(progress),
107966
- onProgress
107967
- );
108948
+ } else {
108949
+ const videoInjector = createVideoFrameInjector(frameLookup);
108950
+ const session = probeSession ?? await createCaptureSession(
108951
+ fileServer.url,
108952
+ framesDir,
108953
+ captureOptions,
108954
+ videoInjector,
108955
+ cfg
108956
+ );
108957
+ if (probeSession) {
108958
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108959
+ probeSession = null;
107968
108960
  }
107969
- } finally {
107970
- lastBrowserConsole = session.browserConsoleBuffer;
107971
- await closeCaptureSession(session);
108961
+ try {
108962
+ if (!session.isInitialized) {
108963
+ await initializeSession(session);
108964
+ }
108965
+ assertNotAborted();
108966
+ lastBrowserConsole = session.browserConsoleBuffer;
108967
+ for (let i = 0; i < job.totalFrames; i++) {
108968
+ assertNotAborted();
108969
+ const time = i / job.config.fps;
108970
+ await captureFrame(session, i, time);
108971
+ job.framesRendered = i + 1;
108972
+ const frameProgress = (i + 1) / job.totalFrames;
108973
+ const progress = 25 + frameProgress * 45;
108974
+ updateJobStatus(
108975
+ job,
108976
+ "rendering",
108977
+ `Capturing frame ${i + 1}/${job.totalFrames}`,
108978
+ Math.round(progress),
108979
+ onProgress
108980
+ );
108981
+ }
108982
+ } finally {
108983
+ lastBrowserConsole = session.browserConsoleBuffer;
108984
+ await closeCaptureSession(session);
108985
+ }
108986
+ }
108987
+ perfStages.captureMs = Date.now() - stage4Start;
108988
+ const stage5Start = Date.now();
108989
+ updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
108990
+ const frameExt = needsAlpha ? "png" : "jpg";
108991
+ const framePattern = `frame_%06d.${frameExt}`;
108992
+ const encoderOpts = {
108993
+ fps: job.config.fps,
108994
+ width,
108995
+ height,
108996
+ codec: preset.codec,
108997
+ preset: preset.preset,
108998
+ quality: preset.quality,
108999
+ pixelFormat: preset.pixelFormat,
109000
+ useGpu: job.config.useGpu,
109001
+ hdr: preset.hdr
109002
+ };
109003
+ const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
109004
+ framesDir,
109005
+ framePattern,
109006
+ videoOnlyPath,
109007
+ encoderOpts,
109008
+ chunkedEncodeSize,
109009
+ abortSignal
109010
+ ) : await encodeFramesFromDir(
109011
+ framesDir,
109012
+ framePattern,
109013
+ videoOnlyPath,
109014
+ encoderOpts,
109015
+ abortSignal
109016
+ );
109017
+ assertNotAborted();
109018
+ if (!encodeResult.success) {
109019
+ throw new Error(`Encoding failed: ${encodeResult.error}`);
107972
109020
  }
109021
+ perfStages.encodeMs = Date.now() - stage5Start;
107973
109022
  }
107974
- perfStages.captureMs = Date.now() - stage4Start;
107975
- const stage5Start = Date.now();
107976
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
107977
- const frameExt = needsAlpha ? "png" : "jpg";
107978
- const framePattern = `frame_%06d.${frameExt}`;
107979
- const encoderOpts = baseEncoderOpts;
107980
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
107981
- framesDir,
107982
- framePattern,
107983
- videoOnlyPath,
107984
- encoderOpts,
107985
- chunkedEncodeSize,
107986
- abortSignal
107987
- ) : await encodeFramesFromDir(
107988
- framesDir,
107989
- framePattern,
107990
- videoOnlyPath,
107991
- encoderOpts,
107992
- abortSignal
107993
- );
107994
- assertNotAborted();
107995
- if (!encodeResult.success) {
107996
- throw new Error(`Encoding failed: ${encodeResult.error}`);
107997
- }
107998
- perfStages.encodeMs = Date.now() - stage5Start;
107999
109023
  }
108000
109024
  if (probeSession !== null) {
108001
109025
  const remainingProbeSession = probeSession;