@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.
@@ -92210,6 +92210,7 @@ import {
92210
92210
  mkdirSync as mkdirSync10,
92211
92211
  rmSync as rmSync3,
92212
92212
  readFileSync as readFileSync9,
92213
+ readdirSync as readdirSync6,
92213
92214
  writeFileSync as writeFileSync4,
92214
92215
  copyFileSync as copyFileSync2,
92215
92216
  appendFileSync
@@ -101457,6 +101458,8 @@ var DEFAULT_CONFIG = {
101457
101458
  ffmpegEncodeTimeout: 6e5,
101458
101459
  ffmpegProcessTimeout: 3e5,
101459
101460
  ffmpegStreamingTimeout: 6e5,
101461
+ hdr: false,
101462
+ hdrAutoDetect: true,
101460
101463
  audioGain: 1.35,
101461
101464
  frameDataUriCacheLimit: 256,
101462
101465
  playerReadyTimeout: 45e3,
@@ -101513,6 +101516,12 @@ function resolveConfig(overrides) {
101513
101516
  "FFMPEG_STREAMING_TIMEOUT_MS",
101514
101517
  DEFAULT_CONFIG.ffmpegStreamingTimeout
101515
101518
  ),
101519
+ hdr: (() => {
101520
+ const raw2 = env2("PRODUCER_HDR_TRANSFER");
101521
+ if (raw2 === "hlg" || raw2 === "pq") return { transfer: raw2 };
101522
+ return void 0;
101523
+ })(),
101524
+ hdrAutoDetect: envBool("PRODUCER_HDR_AUTO_DETECT", DEFAULT_CONFIG.hdrAutoDetect),
101516
101525
  audioGain: envNum("PRODUCER_AUDIO_GAIN", DEFAULT_CONFIG.audioGain),
101517
101526
  frameDataUriCacheLimit: Math.max(
101518
101527
  32,
@@ -101709,7 +101718,8 @@ function buildChromeArgs(options, config2) {
101709
101718
  "--font-render-hinting=none",
101710
101719
  "--force-color-profile=srgb",
101711
101720
  `--window-size=${options.width},${options.height}`,
101712
- // Remotion perf flags — prevent Chrome from throttling background tabs/timers
101721
+ // Prevent Chrome from throttling background tabs/timers — critical when the
101722
+ // page is offscreen during headless capture
101713
101723
  "--disable-background-timer-throttling",
101714
101724
  "--disable-backgrounding-occluded-windows",
101715
101725
  "--disable-renderer-backgrounding",
@@ -103696,6 +103706,24 @@ async function pageScreenshotCapture(page, options) {
103696
103706
  });
103697
103707
  return Buffer.from(result.data, "base64");
103698
103708
  }
103709
+ async function initTransparentBackground(page) {
103710
+ const client = await getCdpSession(page);
103711
+ await client.send("Emulation.setDefaultBackgroundColorOverride", {
103712
+ color: { r: 0, g: 0, b: 0, a: 0 }
103713
+ });
103714
+ }
103715
+ async function captureAlphaPng(page, width, height) {
103716
+ const client = await getCdpSession(page);
103717
+ const result = await client.send("Page.captureScreenshot", {
103718
+ format: "png",
103719
+ fromSurface: true,
103720
+ captureBeyondViewport: false,
103721
+ optimizeForSpeed: false,
103722
+ // must be false to preserve alpha
103723
+ clip: { x: 0, y: 0, width, height, scale: 1 }
103724
+ });
103725
+ return Buffer.from(result.data, "base64");
103726
+ }
103699
103727
  async function injectVideoFramesBatch(page, updates) {
103700
103728
  if (updates.length === 0) return;
103701
103729
  await page.evaluate(
@@ -103717,16 +103745,7 @@ async function injectVideoFramesBatch(page, updates) {
103717
103745
  video.parentNode?.insertBefore(img, video.nextSibling);
103718
103746
  }
103719
103747
  if (!img) continue;
103720
- if (!sourceIsStatic) {
103721
- img.style.position = computedStyle.position;
103722
- img.style.width = computedStyle.width;
103723
- img.style.height = computedStyle.height;
103724
- img.style.top = computedStyle.top;
103725
- img.style.left = computedStyle.left;
103726
- img.style.right = computedStyle.right;
103727
- img.style.bottom = computedStyle.bottom;
103728
- img.style.inset = computedStyle.inset;
103729
- } else {
103748
+ {
103730
103749
  const videoRect = video.getBoundingClientRect();
103731
103750
  const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
103732
103751
  const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
@@ -103777,14 +103796,22 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
103777
103796
  const active = new Set(ids);
103778
103797
  const videos = Array.from(document.querySelectorAll("video[data-start]"));
103779
103798
  for (const video of videos) {
103780
- if (active.has(video.id)) continue;
103781
- video.style.removeProperty("display");
103782
- video.style.setProperty("visibility", "hidden", "important");
103783
- video.style.setProperty("opacity", "0", "important");
103784
- video.style.setProperty("pointer-events", "none", "important");
103785
103799
  const img = video.nextElementSibling;
103786
- if (img && img.classList.contains("__render_frame__")) {
103787
- img.style.visibility = "hidden";
103800
+ const hasImg = img && img.classList.contains("__render_frame__");
103801
+ if (active.has(video.id)) {
103802
+ video.style.setProperty("visibility", "hidden", "important");
103803
+ video.style.setProperty("pointer-events", "none", "important");
103804
+ if (hasImg) {
103805
+ img.style.visibility = "visible";
103806
+ }
103807
+ } else {
103808
+ video.style.removeProperty("display");
103809
+ video.style.setProperty("visibility", "hidden", "important");
103810
+ video.style.setProperty("opacity", "0", "important");
103811
+ video.style.setProperty("pointer-events", "none", "important");
103812
+ if (hasImg) {
103813
+ img.style.visibility = "hidden";
103814
+ }
103788
103815
  }
103789
103816
  }
103790
103817
  }, activeVideoIds);
@@ -103860,6 +103887,15 @@ function isFontResourceError(type, text, locationUrl) {
103860
103887
  `${locationUrl} ${text}`
103861
103888
  );
103862
103889
  }
103890
+ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100) {
103891
+ const deadline = Date.now() + timeoutMs;
103892
+ while (Date.now() < deadline) {
103893
+ const ready = Boolean(await page.evaluate(expression));
103894
+ if (ready) return true;
103895
+ await new Promise((resolve13) => setTimeout(resolve13, intervalMs));
103896
+ }
103897
+ return Boolean(await page.evaluate(expression));
103898
+ }
103863
103899
  async function initializeSession(session) {
103864
103900
  const { page, serverUrl } = session;
103865
103901
  page.on("console", (msg) => {
@@ -103889,14 +103925,26 @@ async function initializeSession(session) {
103889
103925
  if (session.captureMode === "screenshot") {
103890
103926
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
103891
103927
  const pageReadyTimeout2 = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
103892
- await page.waitForFunction(
103928
+ const pageReady2 = await pollPageExpression(
103929
+ page,
103893
103930
  `!!(window.__hf && typeof window.__hf.seek === "function" && window.__hf.duration > 0)`,
103894
- { timeout: pageReadyTimeout2 }
103931
+ pageReadyTimeout2
103895
103932
  );
103896
- await page.waitForFunction(
103933
+ if (!pageReady2) {
103934
+ throw new Error(
103935
+ `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
103936
+ );
103937
+ }
103938
+ const videosReady = await pollPageExpression(
103939
+ page,
103897
103940
  `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
103898
- { timeout: pageReadyTimeout2 }
103941
+ pageReadyTimeout2
103899
103942
  );
103943
+ if (!videosReady) {
103944
+ throw new Error(
103945
+ `[FrameCapture] video metadata not ready after ${pageReadyTimeout2}ms. Video elements must load metadata before capture starts.`
103946
+ );
103947
+ }
103900
103948
  await page.evaluate(`document.fonts?.ready`);
103901
103949
  session.isInitialized = true;
103902
103950
  return;
@@ -104220,7 +104268,7 @@ var ENCODER_PRESETS = {
104220
104268
  standard: { preset: "medium", quality: 18, codec: "h264" },
104221
104269
  high: { preset: "slow", quality: 15, codec: "h264" }
104222
104270
  };
104223
- function getEncoderPreset(quality, format3 = "mp4") {
104271
+ function getEncoderPreset(quality, format3 = "mp4", hdr) {
104224
104272
  const base = ENCODER_PRESETS[quality];
104225
104273
  if (format3 === "webm") {
104226
104274
  return {
@@ -104238,6 +104286,15 @@ function getEncoderPreset(quality, format3 = "mp4") {
104238
104286
  pixelFormat: "yuva444p10le"
104239
104287
  };
104240
104288
  }
104289
+ if (hdr) {
104290
+ return {
104291
+ preset: base.preset === "ultrafast" ? "fast" : base.preset,
104292
+ quality: base.quality,
104293
+ codec: "h265",
104294
+ pixelFormat: "yuv420p10le",
104295
+ hdr
104296
+ };
104297
+ }
104241
104298
  return { ...base, pixelFormat: "yuv420p" };
104242
104299
  }
104243
104300
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -104295,6 +104352,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
104295
104352
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
104296
104353
  }
104297
104354
  }
104355
+ if (codec === "h265") {
104356
+ args.push("-tag:v", "hvc1");
104357
+ }
104298
104358
  } else if (codec === "vp9") {
104299
104359
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
104300
104360
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -104601,31 +104661,79 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
104601
104661
  import { spawn as spawn6 } from "child_process";
104602
104662
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync4 } from "fs";
104603
104663
  import { dirname as dirname6 } from "path";
104664
+
104665
+ // ../engine/src/utils/hdr.ts
104666
+ function isHdrColorSpace(cs) {
104667
+ if (!cs) return false;
104668
+ return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
104669
+ }
104670
+ var DEFAULT_HDR10_MASTERING = {
104671
+ masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
104672
+ maxCll: "1000,400"
104673
+ };
104674
+ function getHdrEncoderColorParams(transfer, mastering = DEFAULT_HDR10_MASTERING) {
104675
+ const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
104676
+ const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
104677
+ const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
104678
+ return {
104679
+ colorPrimaries: "bt2020",
104680
+ colorTrc,
104681
+ colorspace: "bt2020nc",
104682
+ pixelFormat: "yuv420p10le",
104683
+ x265ColorParams: `${tagging}:${metadata}`,
104684
+ mastering
104685
+ };
104686
+ }
104687
+ function analyzeCompositionHdr(colorSpaces) {
104688
+ let hasPq = false;
104689
+ let hasHdr = false;
104690
+ for (const cs of colorSpaces) {
104691
+ if (!isHdrColorSpace(cs)) continue;
104692
+ hasHdr = true;
104693
+ if (cs?.colorTransfer === "smpte2084") hasPq = true;
104694
+ }
104695
+ if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
104696
+ const dominantTransfer = hasPq ? "pq" : "hlg";
104697
+ return { hasHdr: true, dominantTransfer };
104698
+ }
104699
+
104700
+ // ../engine/src/services/streamingEncoder.ts
104604
104701
  function createFrameReorderBuffer(startFrame, endFrame) {
104605
- let nextFrame = startFrame;
104606
- let waiters = [];
104607
- const resolveWaiters = () => {
104608
- for (const waiter of waiters.slice()) {
104609
- if (waiter.frame === nextFrame) {
104610
- waiter.resolve();
104611
- waiters = waiters.filter((w) => w !== waiter);
104612
- }
104702
+ let cursor = startFrame;
104703
+ const pending = /* @__PURE__ */ new Map();
104704
+ const enqueueAt = (frame, resolve13) => {
104705
+ const list = pending.get(frame);
104706
+ if (list === void 0) {
104707
+ pending.set(frame, [resolve13]);
104708
+ } else {
104709
+ list.push(resolve13);
104613
104710
  }
104614
104711
  };
104615
- return {
104616
- waitForFrame: (frame) => new Promise((resolve13) => {
104617
- waiters.push({ frame, resolve: resolve13 });
104618
- resolveWaiters();
104619
- }),
104620
- advanceTo: (frame) => {
104621
- nextFrame = frame;
104622
- resolveWaiters();
104623
- },
104624
- waitForAllDone: () => new Promise((resolve13) => {
104625
- waiters.push({ frame: endFrame, resolve: resolve13 });
104626
- resolveWaiters();
104627
- })
104712
+ const flushAt = (frame) => {
104713
+ const list = pending.get(frame);
104714
+ if (list === void 0) return;
104715
+ pending.delete(frame);
104716
+ for (const resolve13 of list) resolve13();
104628
104717
  };
104718
+ const waitForFrame = (frame) => new Promise((resolve13) => {
104719
+ if (frame === cursor) {
104720
+ resolve13();
104721
+ return;
104722
+ }
104723
+ enqueueAt(frame, resolve13);
104724
+ });
104725
+ const advanceTo = (frame) => {
104726
+ cursor = frame;
104727
+ flushAt(frame);
104728
+ };
104729
+ const waitForAllDone = () => new Promise((resolve13) => {
104730
+ if (cursor >= endFrame) {
104731
+ resolve13();
104732
+ return;
104733
+ }
104734
+ enqueueAt(endFrame, resolve13);
104735
+ });
104736
+ return { waitForFrame, advanceTo, waitForAllDone };
104629
104737
  }
104630
104738
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104631
104739
  const {
@@ -104638,19 +104746,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104638
104746
  useGpu = false,
104639
104747
  imageFormat = "jpeg"
104640
104748
  } = options;
104641
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
104642
- const args = [
104643
- "-f",
104644
- "image2pipe",
104645
- "-vcodec",
104646
- inputCodec,
104647
- "-framerate",
104648
- String(fps),
104649
- "-i",
104650
- "-",
104651
- "-r",
104652
- String(fps)
104653
- ];
104749
+ const args = [];
104750
+ if (options.rawInputFormat) {
104751
+ const hdrTransfer = options.hdr?.transfer;
104752
+ const inputColorTrc = hdrTransfer === "pq" ? "smpte2084" : hdrTransfer === "hlg" ? "arib-std-b67" : void 0;
104753
+ args.push(
104754
+ "-f",
104755
+ "rawvideo",
104756
+ "-pix_fmt",
104757
+ options.rawInputFormat,
104758
+ "-s",
104759
+ `${options.width}x${options.height}`,
104760
+ "-framerate",
104761
+ String(fps)
104762
+ );
104763
+ if (inputColorTrc) {
104764
+ args.push(
104765
+ "-color_primaries",
104766
+ "bt2020",
104767
+ "-color_trc",
104768
+ inputColorTrc,
104769
+ "-colorspace",
104770
+ "bt2020nc"
104771
+ );
104772
+ }
104773
+ args.push("-i", "-");
104774
+ } else {
104775
+ const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
104776
+ args.push("-f", "image2pipe", "-vcodec", inputCodec, "-framerate", String(fps), "-i", "-");
104777
+ }
104778
+ args.push("-r", String(fps));
104654
104779
  const shouldUseGpu = useGpu && gpuEncoder !== null;
104655
104780
  if (codec === "h264" || codec === "h265") {
104656
104781
  if (shouldUseGpu) {
@@ -104688,12 +104813,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104688
104813
  if (bitrate) args.push("-b:v", bitrate);
104689
104814
  else args.push("-crf", String(quality));
104690
104815
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
104691
- const colorParams = "colorprim=bt709:transfer=bt709:colormatrix=bt709";
104816
+ const colorParams = options.rawInputFormat && options.hdr ? getHdrEncoderColorParams(options.hdr.transfer).x265ColorParams : "colorprim=bt709:transfer=bt709:colormatrix=bt709";
104692
104817
  if (preset === "ultrafast") {
104693
104818
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
104694
104819
  } else {
104695
104820
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
104696
104821
  }
104822
+ if (codec === "h265") {
104823
+ args.push("-tag:v", "hvc1");
104824
+ }
104697
104825
  }
104698
104826
  } else if (codec === "vp9") {
104699
104827
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -104709,17 +104837,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104709
104837
  return [...args, "-y", outputPath];
104710
104838
  }
104711
104839
  if (codec === "h264" || codec === "h265") {
104712
- args.push(
104713
- "-colorspace:v",
104714
- "bt709",
104715
- "-color_primaries:v",
104716
- "bt709",
104717
- "-color_trc:v",
104718
- "bt709",
104719
- "-color_range",
104720
- "tv"
104721
- );
104722
- if (gpuEncoder === "vaapi") {
104840
+ if (options.rawInputFormat && options.hdr) {
104841
+ args.push(
104842
+ "-colorspace:v",
104843
+ "bt2020nc",
104844
+ "-color_primaries:v",
104845
+ "bt2020",
104846
+ "-color_trc:v",
104847
+ options.hdr.transfer === "pq" ? "smpte2084" : "arib-std-b67",
104848
+ "-color_range",
104849
+ "tv"
104850
+ );
104851
+ } else {
104852
+ args.push(
104853
+ "-colorspace:v",
104854
+ "bt709",
104855
+ "-color_primaries:v",
104856
+ "bt709",
104857
+ "-color_trc:v",
104858
+ "bt709",
104859
+ "-color_range",
104860
+ "tv"
104861
+ );
104862
+ }
104863
+ if (options.rawInputFormat) {
104864
+ } else if (gpuEncoder === "vaapi") {
104723
104865
  const vfIdx = args.indexOf("-vf");
104724
104866
  if (vfIdx !== -1) {
104725
104867
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -104789,14 +104931,16 @@ Process error: ${err.message}`;
104789
104931
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
104790
104932
  return false;
104791
104933
  }
104792
- return ffmpeg.stdin.write(buffer);
104934
+ const copy = Buffer.from(buffer);
104935
+ return ffmpeg.stdin.write(copy);
104793
104936
  },
104794
104937
  close: async () => {
104795
104938
  clearTimeout(timer2);
104796
104939
  if (signal) signal.removeEventListener("abort", onAbort);
104797
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
104940
+ const stdin = ffmpeg.stdin;
104941
+ if (stdin && !stdin.destroyed) {
104798
104942
  await new Promise((resolve13) => {
104799
- ffmpeg.stdin.end(() => resolve13());
104943
+ stdin.end(() => resolve13());
104800
104944
  });
104801
104945
  }
104802
104946
  await exitPromise;
@@ -104900,6 +105044,10 @@ async function extractVideoMetadata(filePath) {
104900
105044
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
104901
105045
  const fps = avgFps || rFps;
104902
105046
  const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
105047
+ const colorTransfer = videoStream.color_transfer || "";
105048
+ const colorPrimaries = videoStream.color_primaries || "";
105049
+ const colorSpaceVal = videoStream.color_space || "";
105050
+ const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
104903
105051
  return {
104904
105052
  durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
104905
105053
  width: videoStream.width || 0,
@@ -104907,7 +105055,8 @@ async function extractVideoMetadata(filePath) {
104907
105055
  fps,
104908
105056
  videoCodec: videoStream.codec_name || "unknown",
104909
105057
  hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
104910
- isVFR
105058
+ isVFR,
105059
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
104911
105060
  };
104912
105061
  })();
104913
105062
  videoMetadataCache.set(filePath, probePromise);
@@ -105115,18 +105264,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
105115
105264
  const metadata = await extractVideoMetadata(videoPath);
105116
105265
  const framePattern = `frame_%05d.${format3}`;
105117
105266
  const outputPattern = join8(videoOutputDir, framePattern);
105118
- const args = [
105119
- "-ss",
105120
- String(startTime),
105121
- "-i",
105122
- videoPath,
105123
- "-t",
105124
- String(duration),
105125
- "-vf",
105126
- `fps=${fps}`,
105127
- "-q:v",
105128
- format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
105129
- ];
105267
+ const isHdr = isHdrColorSpace(metadata.colorSpace);
105268
+ const isMacOS = process.platform === "darwin";
105269
+ const args = [];
105270
+ if (isHdr && isMacOS) {
105271
+ args.push("-hwaccel", "videotoolbox");
105272
+ }
105273
+ args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
105274
+ const vfFilters = [];
105275
+ if (isHdr && isMacOS) {
105276
+ vfFilters.push("format=nv12");
105277
+ }
105278
+ vfFilters.push(`fps=${fps}`);
105279
+ args.push("-vf", vfFilters.join(","));
105280
+ args.push("-q:v", format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
105130
105281
  if (format3 === "png") args.push("-compression_level", "6");
105131
105282
  args.push("-y", outputPattern);
105132
105283
  return new Promise((resolve13, reject) => {
@@ -105186,30 +105337,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
105186
105337
  });
105187
105338
  });
105188
105339
  }
105340
+ async function convertSdrToHdr(inputPath, outputPath, signal, config2) {
105341
+ const timeout2 = config2?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
105342
+ const args = [
105343
+ "-i",
105344
+ inputPath,
105345
+ "-vf",
105346
+ "colorspace=all=bt2020:iall=bt709:range=tv",
105347
+ "-color_primaries",
105348
+ "bt2020",
105349
+ "-color_trc",
105350
+ "arib-std-b67",
105351
+ "-colorspace",
105352
+ "bt2020nc",
105353
+ "-c:v",
105354
+ "libx264",
105355
+ "-preset",
105356
+ "fast",
105357
+ "-crf",
105358
+ "16",
105359
+ "-c:a",
105360
+ "copy",
105361
+ "-y",
105362
+ outputPath
105363
+ ];
105364
+ const result = await runFfmpeg(args, { signal, timeout: timeout2 });
105365
+ if (!result.success) {
105366
+ throw new Error(
105367
+ `SDR\u2192HDR conversion failed (exit ${result.exitCode}): ${result.stderr.slice(-300)}`
105368
+ );
105369
+ }
105370
+ }
105189
105371
  async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
105190
105372
  const startTime = Date.now();
105191
105373
  const extracted = [];
105192
105374
  const errors = [];
105193
105375
  let totalFramesExtracted = 0;
105376
+ const resolvedVideos = [];
105377
+ for (const video of videos) {
105378
+ if (signal?.aborted) break;
105379
+ try {
105380
+ let videoPath = video.src;
105381
+ if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
105382
+ const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
105383
+ videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
105384
+ }
105385
+ if (isHttpUrl(videoPath)) {
105386
+ const downloadDir = join8(options.outputDir, "_downloads");
105387
+ mkdirSync5(downloadDir, { recursive: true });
105388
+ videoPath = await downloadToTemp(videoPath, downloadDir);
105389
+ }
105390
+ if (!existsSync8(videoPath)) {
105391
+ errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
105392
+ continue;
105393
+ }
105394
+ resolvedVideos.push({ video, videoPath });
105395
+ } catch (err) {
105396
+ errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
105397
+ }
105398
+ }
105399
+ const videoColorSpaces = await Promise.all(
105400
+ resolvedVideos.map(async ({ videoPath }) => {
105401
+ const metadata = await extractVideoMetadata(videoPath);
105402
+ return metadata.colorSpace;
105403
+ })
105404
+ );
105405
+ const hasAnyHdr = videoColorSpaces.some(isHdrColorSpace);
105406
+ if (hasAnyHdr) {
105407
+ const convertDir = join8(options.outputDir, "_hdr_normalized");
105408
+ mkdirSync5(convertDir, { recursive: true });
105409
+ for (let i = 0; i < resolvedVideos.length; i++) {
105410
+ if (signal?.aborted) break;
105411
+ const cs = videoColorSpaces[i] ?? null;
105412
+ if (!isHdrColorSpace(cs)) {
105413
+ const entry = resolvedVideos[i];
105414
+ if (!entry) continue;
105415
+ const convertedPath = join8(convertDir, `${entry.video.id}_hdr.mp4`);
105416
+ try {
105417
+ await convertSdrToHdr(entry.videoPath, convertedPath, signal, config2);
105418
+ entry.videoPath = convertedPath;
105419
+ } catch (err) {
105420
+ errors.push({
105421
+ videoId: entry.video.id,
105422
+ error: `SDR\u2192HDR conversion failed: ${err instanceof Error ? err.message : String(err)}`
105423
+ });
105424
+ }
105425
+ }
105426
+ }
105427
+ }
105194
105428
  const results = await Promise.all(
105195
- videos.map(async (video) => {
105429
+ resolvedVideos.map(async ({ video, videoPath }) => {
105196
105430
  if (signal?.aborted) {
105197
105431
  throw new Error("Video frame extraction cancelled");
105198
105432
  }
105199
105433
  try {
105200
- let videoPath = video.src;
105201
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
105202
- const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
105203
- videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
105204
- }
105205
- if (isHttpUrl(videoPath)) {
105206
- const downloadDir = join8(options.outputDir, "_downloads");
105207
- mkdirSync5(downloadDir, { recursive: true });
105208
- videoPath = await downloadToTemp(videoPath, downloadDir);
105209
- }
105210
- if (!existsSync8(videoPath)) {
105211
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
105212
- }
105213
105434
  let videoDuration = video.end - video.start;
105214
105435
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
105215
105436
  const metadata = await extractVideoMetadata(videoPath);
@@ -105444,6 +105665,74 @@ function createVideoFrameInjector(frameLookup, config2) {
105444
105665
  }
105445
105666
  };
105446
105667
  }
105668
+ async function hideVideoElements(page, videoIds) {
105669
+ if (videoIds.length === 0) return;
105670
+ await page.evaluate((ids) => {
105671
+ for (const id of ids) {
105672
+ const el = document.getElementById(id);
105673
+ if (el) {
105674
+ el.style.setProperty("visibility", "hidden", "important");
105675
+ el.style.setProperty("opacity", "0", "important");
105676
+ const img = document.getElementById(`__render_frame_${id}__`);
105677
+ if (img) img.style.setProperty("visibility", "hidden", "important");
105678
+ }
105679
+ }
105680
+ }, videoIds);
105681
+ }
105682
+ async function showVideoElements(page, videoIds) {
105683
+ if (videoIds.length === 0) return;
105684
+ await page.evaluate((ids) => {
105685
+ for (const id of ids) {
105686
+ const el = document.getElementById(id);
105687
+ if (el) {
105688
+ el.style.removeProperty("visibility");
105689
+ el.style.removeProperty("opacity");
105690
+ const img = document.getElementById(`__render_frame_${id}__`);
105691
+ if (img) img.style.removeProperty("visibility");
105692
+ }
105693
+ }
105694
+ }, videoIds);
105695
+ }
105696
+ async function queryElementStacking(page, nativeHdrVideoIds) {
105697
+ const hdrIds = Array.from(nativeHdrVideoIds);
105698
+ return page.evaluate((hdrIdList) => {
105699
+ const hdrSet = new Set(hdrIdList);
105700
+ const elements = document.querySelectorAll("[data-start]");
105701
+ const results = [];
105702
+ function getEffectiveZIndex(node) {
105703
+ let current = node;
105704
+ while (current) {
105705
+ const cs = window.getComputedStyle(current);
105706
+ const pos = cs.position;
105707
+ const z = parseInt(cs.zIndex);
105708
+ if (!Number.isNaN(z) && pos !== "static") return z;
105709
+ current = current.parentElement;
105710
+ }
105711
+ return 0;
105712
+ }
105713
+ for (const el of elements) {
105714
+ const id = el.id;
105715
+ if (!id) continue;
105716
+ const rect = el.getBoundingClientRect();
105717
+ const style = window.getComputedStyle(el);
105718
+ const zIndex = getEffectiveZIndex(el);
105719
+ const opacity = parseFloat(style.opacity) || 1;
105720
+ const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
105721
+ results.push({
105722
+ id,
105723
+ zIndex,
105724
+ x: Math.round(rect.x),
105725
+ y: Math.round(rect.y),
105726
+ width: Math.round(rect.width),
105727
+ height: Math.round(rect.height),
105728
+ opacity,
105729
+ visible,
105730
+ isHdr: hdrSet.has(id)
105731
+ });
105732
+ }
105733
+ return results;
105734
+ }, hdrIds);
105735
+ }
105447
105736
 
105448
105737
  // ../engine/src/services/audioMixer.ts
105449
105738
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
@@ -105930,6 +106219,292 @@ async function mergeWorkerFrames(workDir, tasks, outputDir) {
105930
106219
  return totalFrames;
105931
106220
  }
105932
106221
 
106222
+ // ../engine/src/utils/alphaBlit.ts
106223
+ import { inflateSync } from "zlib";
106224
+ function paeth(a, b, c) {
106225
+ const p = a + b - c;
106226
+ const pa = Math.abs(p - a);
106227
+ const pb = Math.abs(p - b);
106228
+ const pc = Math.abs(p - c);
106229
+ if (pa <= pb && pa <= pc) return a;
106230
+ if (pb <= pc) return b;
106231
+ return c;
106232
+ }
106233
+ function decodePngRaw(buf, caller) {
106234
+ 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) {
106235
+ throw new Error(`${caller}: not a PNG file`);
106236
+ }
106237
+ let pos = 8;
106238
+ let width = 0;
106239
+ let height = 0;
106240
+ let bitDepth = 0;
106241
+ let colorType = 0;
106242
+ let interlace = 0;
106243
+ let sawIhdr = false;
106244
+ const idatChunks = [];
106245
+ while (pos + 12 <= buf.length) {
106246
+ const chunkLen = buf.readUInt32BE(pos);
106247
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
106248
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
106249
+ if (chunkType === "IHDR") {
106250
+ width = chunkData.readUInt32BE(0);
106251
+ height = chunkData.readUInt32BE(4);
106252
+ bitDepth = chunkData[8] ?? 0;
106253
+ colorType = chunkData[9] ?? 0;
106254
+ interlace = chunkData[12] ?? 0;
106255
+ sawIhdr = true;
106256
+ } else if (chunkType === "IDAT") {
106257
+ idatChunks.push(Buffer.from(chunkData));
106258
+ } else if (chunkType === "IEND") {
106259
+ break;
106260
+ }
106261
+ pos += 12 + chunkLen;
106262
+ }
106263
+ if (!sawIhdr) {
106264
+ throw new Error(`${caller}: PNG missing IHDR chunk`);
106265
+ }
106266
+ if (colorType !== 2 && colorType !== 6) {
106267
+ throw new Error(`${caller}: unsupported color type ${colorType} (expected 2=RGB or 6=RGBA)`);
106268
+ }
106269
+ if (interlace !== 0) {
106270
+ throw new Error(
106271
+ `${caller}: Adam7-interlaced PNGs are not supported (interlace method ${interlace})`
106272
+ );
106273
+ }
106274
+ const channels = colorType === 6 ? 4 : 3;
106275
+ const bpp = channels * (bitDepth / 8);
106276
+ const stride = width * bpp;
106277
+ const compressed = Buffer.concat(idatChunks);
106278
+ const decompressed = inflateSync(compressed);
106279
+ const rawPixels = Buffer.allocUnsafe(height * stride);
106280
+ const prevRow = new Uint8Array(stride);
106281
+ const currRow = new Uint8Array(stride);
106282
+ let srcPos = 0;
106283
+ for (let y = 0; y < height; y++) {
106284
+ const filterType = decompressed[srcPos++] ?? 0;
106285
+ const rawRow = decompressed.subarray(srcPos, srcPos + stride);
106286
+ srcPos += stride;
106287
+ switch (filterType) {
106288
+ case 0:
106289
+ currRow.set(rawRow);
106290
+ break;
106291
+ case 1:
106292
+ for (let x = 0; x < stride; x++) {
106293
+ currRow[x] = (rawRow[x] ?? 0) + (x >= bpp ? currRow[x - bpp] ?? 0 : 0) & 255;
106294
+ }
106295
+ break;
106296
+ case 2:
106297
+ for (let x = 0; x < stride; x++) {
106298
+ currRow[x] = (rawRow[x] ?? 0) + (prevRow[x] ?? 0) & 255;
106299
+ }
106300
+ break;
106301
+ case 3:
106302
+ for (let x = 0; x < stride; x++) {
106303
+ const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
106304
+ const up = prevRow[x] ?? 0;
106305
+ currRow[x] = (rawRow[x] ?? 0) + Math.floor((left2 + up) / 2) & 255;
106306
+ }
106307
+ break;
106308
+ case 4:
106309
+ for (let x = 0; x < stride; x++) {
106310
+ const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
106311
+ const up = prevRow[x] ?? 0;
106312
+ const upLeft = x >= bpp ? prevRow[x - bpp] ?? 0 : 0;
106313
+ currRow[x] = (rawRow[x] ?? 0) + paeth(left2, up, upLeft) & 255;
106314
+ }
106315
+ break;
106316
+ default:
106317
+ throw new Error(`${caller}: unknown filter type ${filterType} at row ${y}`);
106318
+ }
106319
+ rawPixels.set(currRow, y * stride);
106320
+ prevRow.set(currRow);
106321
+ }
106322
+ return { width, height, bitDepth, colorType, rawPixels };
106323
+ }
106324
+ function decodePng(buf) {
106325
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePng");
106326
+ if (bitDepth !== 8) {
106327
+ throw new Error(`decodePng: unsupported bit depth ${bitDepth} (expected 8)`);
106328
+ }
106329
+ const output2 = new Uint8Array(width * height * 4);
106330
+ if (colorType === 6) {
106331
+ output2.set(rawPixels);
106332
+ } else {
106333
+ for (let i = 0; i < width * height; i++) {
106334
+ output2[i * 4 + 0] = rawPixels[i * 3 + 0] ?? 0;
106335
+ output2[i * 4 + 1] = rawPixels[i * 3 + 1] ?? 0;
106336
+ output2[i * 4 + 2] = rawPixels[i * 3 + 2] ?? 0;
106337
+ output2[i * 4 + 3] = 255;
106338
+ }
106339
+ }
106340
+ return { width, height, data: output2 };
106341
+ }
106342
+ function decodePngToRgb48le(buf) {
106343
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePngToRgb48le");
106344
+ if (bitDepth !== 16) {
106345
+ throw new Error(`decodePngToRgb48le: unsupported bit depth ${bitDepth} (expected 16)`);
106346
+ }
106347
+ const bpp = colorType === 6 ? 8 : 6;
106348
+ const output2 = Buffer.allocUnsafe(width * height * 6);
106349
+ for (let y = 0; y < height; y++) {
106350
+ const dstBase = y * width * 6;
106351
+ const srcRowBase = y * width * bpp;
106352
+ for (let x = 0; x < width; x++) {
106353
+ const srcBase = srcRowBase + x * bpp;
106354
+ output2[dstBase + x * 6 + 0] = rawPixels[srcBase + 1] ?? 0;
106355
+ output2[dstBase + x * 6 + 1] = rawPixels[srcBase + 0] ?? 0;
106356
+ output2[dstBase + x * 6 + 2] = rawPixels[srcBase + 3] ?? 0;
106357
+ output2[dstBase + x * 6 + 3] = rawPixels[srcBase + 2] ?? 0;
106358
+ output2[dstBase + x * 6 + 4] = rawPixels[srcBase + 5] ?? 0;
106359
+ output2[dstBase + x * 6 + 5] = rawPixels[srcBase + 4] ?? 0;
106360
+ }
106361
+ }
106362
+ return { width, height, data: output2 };
106363
+ }
106364
+ function buildSrgbToHdrLut(transfer) {
106365
+ const lut = new Uint16Array(256);
106366
+ const hlgA = 0.17883277;
106367
+ const hlgB = 1 - 4 * hlgA;
106368
+ const hlgC = 0.5 - hlgA * Math.log(4 * hlgA);
106369
+ const pqM1 = 0.1593017578125;
106370
+ const pqM2 = 78.84375;
106371
+ const pqC1 = 0.8359375;
106372
+ const pqC2 = 18.8515625;
106373
+ const pqC3 = 18.6875;
106374
+ const pqMaxNits = 1e4;
106375
+ const sdrNits = 203;
106376
+ for (let i = 0; i < 256; i++) {
106377
+ const v = i / 255;
106378
+ const linear = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
106379
+ let signal;
106380
+ if (transfer === "hlg") {
106381
+ signal = linear <= 1 / 12 ? Math.sqrt(3 * linear) : hlgA * Math.log(12 * linear - hlgB) + hlgC;
106382
+ } else {
106383
+ const Lp = Math.max(0, linear * sdrNits / pqMaxNits);
106384
+ const Lm1 = Math.pow(Lp, pqM1);
106385
+ signal = Math.pow((pqC1 + pqC2 * Lm1) / (1 + pqC3 * Lm1), pqM2);
106386
+ }
106387
+ lut[i] = Math.min(65535, Math.round(signal * 65535));
106388
+ }
106389
+ return lut;
106390
+ }
106391
+ var SRGB_TO_HLG = buildSrgbToHdrLut("hlg");
106392
+ var SRGB_TO_PQ = buildSrgbToHdrLut("pq");
106393
+ function getSrgbToHdrLut(transfer) {
106394
+ return transfer === "pq" ? SRGB_TO_PQ : SRGB_TO_HLG;
106395
+ }
106396
+ function blitRgba8OverRgb48le(domRgba, canvas, width, height, transfer = "hlg") {
106397
+ const pixelCount = width * height;
106398
+ const lut = getSrgbToHdrLut(transfer);
106399
+ for (let i = 0; i < pixelCount; i++) {
106400
+ const da = domRgba[i * 4 + 3] ?? 0;
106401
+ if (da === 0) {
106402
+ continue;
106403
+ } else if (da === 255) {
106404
+ const r16 = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
106405
+ const g16 = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
106406
+ const b16 = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
106407
+ canvas.writeUInt16LE(r16, i * 6);
106408
+ canvas.writeUInt16LE(g16, i * 6 + 2);
106409
+ canvas.writeUInt16LE(b16, i * 6 + 4);
106410
+ } else {
106411
+ const alpha = da / 255;
106412
+ const invAlpha = 1 - alpha;
106413
+ const hdrR = (canvas[i * 6 + 0] ?? 0) | (canvas[i * 6 + 1] ?? 0) << 8;
106414
+ const hdrG = (canvas[i * 6 + 2] ?? 0) | (canvas[i * 6 + 3] ?? 0) << 8;
106415
+ const hdrB = (canvas[i * 6 + 4] ?? 0) | (canvas[i * 6 + 5] ?? 0) << 8;
106416
+ const domR = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
106417
+ const domG = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
106418
+ const domB = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
106419
+ canvas.writeUInt16LE(Math.round(domR * alpha + hdrR * invAlpha), i * 6);
106420
+ canvas.writeUInt16LE(Math.round(domG * alpha + hdrG * invAlpha), i * 6 + 2);
106421
+ canvas.writeUInt16LE(Math.round(domB * alpha + hdrB * invAlpha), i * 6 + 4);
106422
+ }
106423
+ }
106424
+ }
106425
+ function cornerAlpha(px, py, cx, cy, r) {
106426
+ const dx = px - cx;
106427
+ const dy = py - cy;
106428
+ const dist = Math.sqrt(dx * dx + dy * dy);
106429
+ if (dist > r + 0.5) return 0;
106430
+ if (dist > r - 0.5) return r + 0.5 - dist;
106431
+ return 1;
106432
+ }
106433
+ function roundedRectAlpha(px, py, w, h, radii) {
106434
+ const [tl, tr, br, bl] = radii;
106435
+ if (px < tl && py < tl) return cornerAlpha(px, py, tl, tl, tl);
106436
+ if (px >= w - tr && py < tr) return cornerAlpha(px, py, w - tr, tr, tr);
106437
+ if (px >= w - br && py >= h - br) return cornerAlpha(px, py, w - br, h - br, br);
106438
+ if (px < bl && py >= h - bl) return cornerAlpha(px, py, bl, h - bl, bl);
106439
+ return 1;
106440
+ }
106441
+ function blitRgb48leRegion(canvas, source2, dx, dy, sw, sh, canvasWidth, canvasHeight, opacity, borderRadius) {
106442
+ if (sw <= 0 || sh <= 0) return;
106443
+ const op = opacity ?? 1;
106444
+ const x0 = Math.max(0, dx);
106445
+ const y0 = Math.max(0, dy);
106446
+ const x1 = Math.min(canvasWidth, dx + sw);
106447
+ const y1 = Math.min(canvasHeight, dy + sh);
106448
+ if (x0 >= x1 || y0 >= y1) return;
106449
+ const clippedW = x1 - x0;
106450
+ const srcOffsetX = x0 - dx;
106451
+ const srcOffsetY = y0 - dy;
106452
+ const hasMask = borderRadius !== void 0;
106453
+ if (op >= 0.999 && !hasMask) {
106454
+ for (let y = 0; y < y1 - y0; y++) {
106455
+ const srcRowOff = ((srcOffsetY + y) * sw + srcOffsetX) * 6;
106456
+ const dstRowOff = ((y0 + y) * canvasWidth + x0) * 6;
106457
+ source2.copy(canvas, dstRowOff, srcRowOff, srcRowOff + clippedW * 6);
106458
+ }
106459
+ } else {
106460
+ for (let y = 0; y < y1 - y0; y++) {
106461
+ for (let x = 0; x < clippedW; x++) {
106462
+ let effectiveOp = op;
106463
+ if (hasMask) {
106464
+ const ma = roundedRectAlpha(srcOffsetX + x, srcOffsetY + y, sw, sh, borderRadius);
106465
+ if (ma <= 0) continue;
106466
+ effectiveOp *= ma;
106467
+ }
106468
+ const srcOff = ((srcOffsetY + y) * sw + srcOffsetX + x) * 6;
106469
+ const dstOff = ((y0 + y) * canvasWidth + x0 + x) * 6;
106470
+ if (effectiveOp >= 0.999) {
106471
+ source2.copy(canvas, dstOff, srcOff, srcOff + 6);
106472
+ } else {
106473
+ const invEff = 1 - effectiveOp;
106474
+ const sr = source2.readUInt16LE(srcOff);
106475
+ const sg = source2.readUInt16LE(srcOff + 2);
106476
+ const sb = source2.readUInt16LE(srcOff + 4);
106477
+ const dr = canvas.readUInt16LE(dstOff);
106478
+ const dg = canvas.readUInt16LE(dstOff + 2);
106479
+ const db = canvas.readUInt16LE(dstOff + 4);
106480
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
106481
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
106482
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
106483
+ }
106484
+ }
106485
+ }
106486
+ }
106487
+ }
106488
+
106489
+ // ../engine/src/utils/layerCompositor.ts
106490
+ function groupIntoLayers(elements) {
106491
+ const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
106492
+ const layers = [];
106493
+ for (const el of sorted) {
106494
+ if (el.isHdr) {
106495
+ layers.push({ type: "hdr", element: el });
106496
+ } else {
106497
+ const last2 = layers[layers.length - 1];
106498
+ if (last2 && last2.type === "dom") {
106499
+ last2.elementIds.push(el.id);
106500
+ } else {
106501
+ layers.push({ type: "dom", elementIds: [el.id] });
106502
+ }
106503
+ }
106504
+ }
106505
+ return layers;
106506
+ }
106507
+
105933
106508
  // src/services/renderOrchestrator.ts
105934
106509
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
105935
106510
  import { randomUUID } from "crypto";
@@ -106036,6 +106611,102 @@ var MIME_TYPES = {
106036
106611
  ".ttf": "font/ttf",
106037
106612
  ".otf": "font/otf"
106038
106613
  };
106614
+ var VIRTUAL_TIME_SHIM = String.raw`(function() {
106615
+ if (window.__HF_VIRTUAL_TIME__) return;
106616
+
106617
+ var virtualNowMs = 0;
106618
+ var rafId = 1;
106619
+ var rafQueue = [];
106620
+ var OriginalDate = Date;
106621
+ var originalSetTimeout = window.setTimeout.bind(window);
106622
+ var originalClearTimeout = window.clearTimeout.bind(window);
106623
+ var originalSetInterval = window.setInterval.bind(window);
106624
+ var originalClearInterval = window.clearInterval.bind(window);
106625
+ var originalRequestAnimationFrame = window.requestAnimationFrame
106626
+ ? window.requestAnimationFrame.bind(window)
106627
+ : null;
106628
+ var originalCancelAnimationFrame = window.cancelAnimationFrame
106629
+ ? window.cancelAnimationFrame.bind(window)
106630
+ : null;
106631
+
106632
+ function flushAnimationFrame() {
106633
+ if (!rafQueue.length) return;
106634
+ var current = rafQueue.slice();
106635
+ rafQueue.length = 0;
106636
+ for (var i = 0; i < current.length; i++) {
106637
+ var entry = current[i];
106638
+ if (entry.cancelled) continue;
106639
+ try {
106640
+ entry.callback(virtualNowMs);
106641
+ } catch {}
106642
+ }
106643
+ }
106644
+
106645
+ function VirtualDate() {
106646
+ var args = Array.prototype.slice.call(arguments);
106647
+ if (!(this instanceof VirtualDate)) {
106648
+ return OriginalDate.apply(null, args.length ? args : [virtualNowMs]);
106649
+ }
106650
+ var instance = args.length ? new (Function.prototype.bind.apply(OriginalDate, [null].concat(args)))() : new OriginalDate(virtualNowMs);
106651
+ Object.setPrototypeOf(instance, VirtualDate.prototype);
106652
+ return instance;
106653
+ }
106654
+
106655
+ VirtualDate.prototype = OriginalDate.prototype;
106656
+ Object.setPrototypeOf(VirtualDate, OriginalDate);
106657
+ VirtualDate.now = function() { return virtualNowMs; };
106658
+ VirtualDate.parse = OriginalDate.parse.bind(OriginalDate);
106659
+ VirtualDate.UTC = OriginalDate.UTC.bind(OriginalDate);
106660
+
106661
+ try {
106662
+ Object.defineProperty(window, "Date", {
106663
+ configurable: true,
106664
+ writable: true,
106665
+ value: VirtualDate,
106666
+ });
106667
+ } catch {}
106668
+
106669
+ if (window.performance && typeof window.performance.now === "function") {
106670
+ try {
106671
+ Object.defineProperty(window.performance, "now", {
106672
+ configurable: true,
106673
+ value: function() { return virtualNowMs; },
106674
+ });
106675
+ } catch {}
106676
+ }
106677
+
106678
+ window.requestAnimationFrame = function(callback) {
106679
+ if (typeof callback !== "function") return 0;
106680
+ var entry = { id: rafId++, callback: callback, cancelled: false };
106681
+ rafQueue.push(entry);
106682
+ return entry.id;
106683
+ };
106684
+ window.cancelAnimationFrame = function(id) {
106685
+ for (var i = 0; i < rafQueue.length; i++) {
106686
+ if (rafQueue[i].id === id) {
106687
+ rafQueue[i].cancelled = true;
106688
+ }
106689
+ }
106690
+ };
106691
+
106692
+ window.__HF_VIRTUAL_TIME__ = {
106693
+ originalSetTimeout: originalSetTimeout,
106694
+ originalClearTimeout: originalClearTimeout,
106695
+ originalSetInterval: originalSetInterval,
106696
+ originalClearInterval: originalClearInterval,
106697
+ originalRequestAnimationFrame: originalRequestAnimationFrame,
106698
+ originalCancelAnimationFrame: originalCancelAnimationFrame,
106699
+ seekToTime: function(nextTimeMs) {
106700
+ var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
106701
+ virtualNowMs = safeTimeMs;
106702
+ flushAnimationFrame();
106703
+ return virtualNowMs;
106704
+ },
106705
+ getTime: function() {
106706
+ return virtualNowMs;
106707
+ },
106708
+ };
106709
+ })();`;
106039
106710
  var RENDER_SEEK_MODE = process.env.PRODUCER_RUNTIME_RENDER_SEEK_MODE === "strict-boundary" ? "strict-boundary" : "preview-phase";
106040
106711
  var RENDER_SEEK_DIAGNOSTICS = process.env.PRODUCER_DEBUG_SEEK_DIAGNOSTICS === "true";
106041
106712
  var RENDER_SEEK_STEP = Math.max(
@@ -106047,6 +106718,10 @@ var RENDER_SEEK_OFFSET_FRACTION = Math.max(
106047
106718
  Math.min(0.95, Number(process.env.PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION || 0.5))
106048
106719
  );
106049
106720
  var RENDER_MODE_SCRIPT = `(function() {
106721
+ var __realSetTimeout =
106722
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetTimeout === "function"
106723
+ ? window.__HF_VIRTUAL_TIME__.originalSetTimeout
106724
+ : window.setTimeout.bind(window);
106050
106725
  var __seekMode = ${JSON.stringify(RENDER_SEEK_MODE)};
106051
106726
  var __seekDiagnostics = ${RENDER_SEEK_DIAGNOSTICS ? "true" : "false"};
106052
106727
  var __seekStep = ${RENDER_SEEK_STEP};
@@ -106140,40 +106815,88 @@ var RENDER_MODE_SCRIPT = `(function() {
106140
106815
  window.__renderReady = true;
106141
106816
  return;
106142
106817
  }
106143
- setTimeout(waitForPlayer, 50);
106818
+ __realSetTimeout(waitForPlayer, 50);
106144
106819
  return;
106145
106820
  }
106146
106821
  if (installMediaFallbackPlayer()) {
106147
106822
  return;
106148
106823
  }
106149
- setTimeout(waitForPlayer, 50);
106824
+ __realSetTimeout(waitForPlayer, 50);
106150
106825
  }
106151
106826
  waitForPlayer();
106152
106827
  })();`;
106828
+ var HF_EARLY_STUB = `(function() {
106829
+ if (typeof window === "undefined") return;
106830
+ if (!window.__hf) window.__hf = {};
106831
+ })();`;
106153
106832
  var HF_BRIDGE_SCRIPT = `(function() {
106833
+ var __realSetInterval =
106834
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
106835
+ ? window.__HF_VIRTUAL_TIME__.originalSetInterval
106836
+ : window.setInterval.bind(window);
106837
+ var __realClearInterval =
106838
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalClearInterval === "function"
106839
+ ? window.__HF_VIRTUAL_TIME__.originalClearInterval
106840
+ : window.clearInterval.bind(window);
106154
106841
  function getDeclaredDuration() {
106155
106842
  var root = document.querySelector('[data-composition-id]');
106156
106843
  if (!root) return 0;
106157
106844
  var d = Number(root.getAttribute('data-duration'));
106158
106845
  return Number.isFinite(d) && d > 0 ? d : 0;
106159
106846
  }
106847
+ function seekSameOriginChildFrames(frameWindow, nextTimeMs) {
106848
+ var frames;
106849
+ try {
106850
+ frames = frameWindow.frames;
106851
+ } catch (_error) {
106852
+ return;
106853
+ }
106854
+ if (!frames || typeof frames.length !== "number") return;
106855
+ for (var i = 0; i < frames.length; i++) {
106856
+ var childWindow = null;
106857
+ try {
106858
+ childWindow = frames[i];
106859
+ if (!childWindow || childWindow === frameWindow) continue;
106860
+ if (
106861
+ childWindow.__HF_VIRTUAL_TIME__ &&
106862
+ typeof childWindow.__HF_VIRTUAL_TIME__.seekToTime === "function"
106863
+ ) {
106864
+ childWindow.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106865
+ }
106866
+ } catch (_error) {
106867
+ continue;
106868
+ }
106869
+ seekSameOriginChildFrames(childWindow, nextTimeMs);
106870
+ }
106871
+ }
106160
106872
  function bridge() {
106161
106873
  var p = window.__player;
106162
106874
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
106163
106875
  return false;
106164
106876
  }
106165
- window.__hf = {
106166
- get duration() {
106877
+ var hf = window.__hf || {};
106878
+ Object.defineProperty(hf, "duration", {
106879
+ configurable: true,
106880
+ enumerable: true,
106881
+ get: function() {
106167
106882
  var d = p.getDuration();
106168
106883
  return d > 0 ? d : getDeclaredDuration();
106169
106884
  },
106170
- seek: function(t) { p.renderSeek(t); },
106885
+ });
106886
+ hf.seek = function(t) {
106887
+ p.renderSeek(t);
106888
+ var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
106889
+ if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
106890
+ window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106891
+ }
106892
+ seekSameOriginChildFrames(window, nextTimeMs);
106171
106893
  };
106894
+ window.__hf = hf;
106172
106895
  return true;
106173
106896
  }
106174
106897
  if (bridge()) return;
106175
- var iv = setInterval(function() {
106176
- if (bridge()) clearInterval(iv);
106898
+ var iv = __realSetInterval(function() {
106899
+ if (bridge()) __realClearInterval(iv);
106177
106900
  }, 50);
106178
106901
  })();`;
106179
106902
  function stripEmbeddedRuntimeScripts(html) {
@@ -106235,8 +106958,22 @@ function injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbedded) {
106235
106958
  }
106236
106959
  return html;
106237
106960
  }
106961
+ function injectScriptsAtHeadStart(html, scripts) {
106962
+ if (scripts.length === 0) return html;
106963
+ const headTags = scripts.map((src) => `<script>${src}</script>`).join("\n");
106964
+ if (html.includes("<head")) {
106965
+ return html.replace(/<head\b[^>]*>/i, (match2) => `${match2}
106966
+ ${headTags}`);
106967
+ }
106968
+ if (html.includes("<body")) {
106969
+ return html.replace("<body", () => `${headTags}
106970
+ <body`);
106971
+ }
106972
+ return headTags + "\n" + html;
106973
+ }
106238
106974
  function createFileServer2(options) {
106239
106975
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
106976
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
106240
106977
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
106241
106978
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
106242
106979
  const app = new Hono2();
@@ -106260,7 +106997,11 @@ function createFileServer2(options) {
106260
106997
  if (ext === ".html") {
106261
106998
  const rawHtml = readFileSync6(filePath, "utf-8");
106262
106999
  const isIndex = relativePath === "index.html";
106263
- const html = isIndex ? injectScriptsIntoHtml(rawHtml, headScripts, bodyScripts, stripEmbeddedRuntime) : rawHtml;
107000
+ let html = rawHtml;
107001
+ if (preHeadScripts.length > 0) {
107002
+ html = injectScriptsAtHeadStart(html, preHeadScripts);
107003
+ }
107004
+ html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
106264
107005
  return c.text(html, 200, { "Content-Type": contentType });
106265
107006
  }
106266
107007
  const content = readFileSync6(filePath);
@@ -106708,6 +107449,37 @@ function dedupeElementsById(elements) {
106708
107449
  }
106709
107450
  return Array.from(deduped.values());
106710
107451
  }
107452
+ var INLINE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
107453
+ function stripJsComments(source2) {
107454
+ return source2.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
107455
+ }
107456
+ function detectRenderModeHints(html) {
107457
+ const reasons = [];
107458
+ const { document: document2 } = parseHTML(html);
107459
+ if (document2.querySelector("iframe")) {
107460
+ reasons.push({
107461
+ code: "iframe",
107462
+ message: "Detected <iframe> in the composition DOM. Nested iframe animation is routed through screenshot capture mode for compatibility."
107463
+ });
107464
+ }
107465
+ let scriptMatch;
107466
+ const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
107467
+ while ((scriptMatch = scriptPattern.exec(html)) !== null) {
107468
+ const attrs = scriptMatch[1] || "";
107469
+ if (/\bsrc\s*=/i.test(attrs)) continue;
107470
+ const content = stripJsComments(scriptMatch[2] || "");
107471
+ if (!/requestAnimationFrame\s*\(/.test(content)) continue;
107472
+ reasons.push({
107473
+ code: "requestAnimationFrame",
107474
+ message: "Detected raw requestAnimationFrame() in an inline script. This render is routed through screenshot capture mode with virtual time enabled."
107475
+ });
107476
+ break;
107477
+ }
107478
+ return {
107479
+ recommendScreenshot: reasons.length > 0,
107480
+ reasons
107481
+ };
107482
+ }
106711
107483
  async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
106712
107484
  let filePath = src;
106713
107485
  if (isHttpUrl(src)) {
@@ -107271,6 +108043,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107271
108043
  /(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
107272
108044
  "$1"
107273
108045
  );
108046
+ const renderModeHints = detectRenderModeHints(sanitizedHtml);
107274
108047
  const coalescedHtml = await injectDeterministicFontFaces(
107275
108048
  coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
107276
108049
  );
@@ -107314,7 +108087,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107314
108087
  externalAssets,
107315
108088
  width,
107316
108089
  height,
107317
- staticDuration
108090
+ staticDuration,
108091
+ renderModeHints
107318
108092
  };
107319
108093
  }
107320
108094
  async function discoverMediaFromBrowser(page) {
@@ -107408,7 +108182,8 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
107408
108182
  subCompositions,
107409
108183
  videos,
107410
108184
  audios,
107411
- unresolvedCompositions: remaining
108185
+ unresolvedCompositions: remaining,
108186
+ renderModeHints: compiled.renderModeHints
107412
108187
  };
107413
108188
  }
107414
108189
 
@@ -107458,6 +108233,24 @@ async function safeCleanup(label, fn, log = defaultLogger) {
107458
108233
  });
107459
108234
  }
107460
108235
  }
108236
+ var frameDirMaxIndexCache = /* @__PURE__ */ new Map();
108237
+ var FRAME_FILENAME_RE = /^frame_(\d+)\.png$/;
108238
+ function getMaxFrameIndex(frameDir) {
108239
+ const cached = frameDirMaxIndexCache.get(frameDir);
108240
+ if (cached !== void 0) return cached;
108241
+ let max = 0;
108242
+ try {
108243
+ for (const name of readdirSync6(frameDir)) {
108244
+ const m = FRAME_FILENAME_RE.exec(name);
108245
+ if (!m) continue;
108246
+ const n = Number(m[1]);
108247
+ if (Number.isFinite(n) && n > max) max = n;
108248
+ }
108249
+ } catch {
108250
+ }
108251
+ frameDirMaxIndexCache.set(frameDir, max);
108252
+ return max;
108253
+ }
107461
108254
  var RenderCancelledError = class extends Error {
107462
108255
  reason;
107463
108256
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -107545,11 +108338,20 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
107545
108338
  end: a.end,
107546
108339
  mediaStart: a.mediaStart
107547
108340
  })),
107548
- subCompositions: Array.from(compiled.subCompositions.keys())
108341
+ subCompositions: Array.from(compiled.subCompositions.keys()),
108342
+ renderModeHints: compiled.renderModeHints
107549
108343
  };
107550
108344
  writeFileSync4(join15(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
107551
108345
  }
107552
108346
  }
108347
+ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
108348
+ if (cfg.forceScreenshot || !compiled.renderModeHints.recommendScreenshot) return;
108349
+ cfg.forceScreenshot = true;
108350
+ log.warn("Auto-selected screenshot capture mode for render compatibility", {
108351
+ reasonCodes: compiled.renderModeHints.reasons.map((reason) => reason.code),
108352
+ reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
108353
+ });
108354
+ }
107553
108355
  function createRenderJob(config2) {
107554
108356
  return {
107555
108357
  id: randomUUID(),
@@ -107662,6 +108464,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107662
108464
  let compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"));
107663
108465
  assertNotAborted();
107664
108466
  perfStages.compileOnlyMs = Date.now() - compileStart;
108467
+ applyRenderModeHints(cfg, compiled, log);
107665
108468
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
107666
108469
  log.info("Compiled composition metadata", {
107667
108470
  entryFile,
@@ -107669,7 +108472,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107669
108472
  width: compiled.width,
107670
108473
  height: compiled.height,
107671
108474
  videoCount: compiled.videos.length,
107672
- audioCount: compiled.audios.length
108475
+ audioCount: compiled.audios.length,
108476
+ renderModeHints: compiled.renderModeHints
107673
108477
  });
107674
108478
  const composition = {
107675
108479
  duration: compiled.staticDuration,
@@ -107689,7 +108493,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107689
108493
  fileServer = await createFileServer2({
107690
108494
  projectDir,
107691
108495
  compiledDir: join15(workDir, "compiled"),
107692
- port: 0
108496
+ port: 0,
108497
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
107693
108498
  });
107694
108499
  assertNotAborted();
107695
108500
  const captureOpts = {
@@ -107845,7 +108650,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107845
108650
  }
107846
108651
  }
107847
108652
  }
107848
- } catch {
108653
+ } catch (err) {
108654
+ log.warn("Failed to gather browser diagnostics for zero-duration composition", {
108655
+ error: err instanceof Error ? err.message : String(err)
108656
+ });
107849
108657
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
107850
108658
  }
107851
108659
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -107869,8 +108677,26 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107869
108677
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107870
108678
  let frameLookup = null;
107871
108679
  const compiledDir = join15(workDir, "compiled");
108680
+ let extractionResult = null;
108681
+ const nativeHdrVideoIds = /* @__PURE__ */ new Set();
108682
+ if (composition.videos.length > 0) {
108683
+ await Promise.all(
108684
+ composition.videos.map(async (v) => {
108685
+ let videoPath = v.src;
108686
+ if (!videoPath.startsWith("/")) {
108687
+ const fromCompiled = existsSync15(join15(compiledDir, videoPath)) ? join15(compiledDir, videoPath) : join15(projectDir, videoPath);
108688
+ videoPath = fromCompiled;
108689
+ }
108690
+ if (!existsSync15(videoPath)) return;
108691
+ const meta = await extractVideoMetadata(videoPath);
108692
+ if (isHdrColorSpace(meta.colorSpace)) {
108693
+ nativeHdrVideoIds.add(v.id);
108694
+ }
108695
+ })
108696
+ );
108697
+ }
107872
108698
  if (composition.videos.length > 0) {
107873
- const extractionResult = await extractAllVideoFrames(
108699
+ extractionResult = await extractAllVideoFrames(
107874
108700
  composition.videos,
107875
108701
  projectDir,
107876
108702
  { fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
@@ -107905,6 +108731,23 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107905
108731
  } else {
107906
108732
  perfStages.videoExtractMs = Date.now() - stage2Start;
107907
108733
  }
108734
+ let effectiveHdr;
108735
+ if (frameLookup) {
108736
+ const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
108737
+ const info = analyzeCompositionHdr(colorSpaces);
108738
+ if (info.hasHdr && info.dominantTransfer) {
108739
+ effectiveHdr = { transfer: info.dominantTransfer };
108740
+ }
108741
+ }
108742
+ if (effectiveHdr && outputFormat !== "mp4") {
108743
+ log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
108744
+ effectiveHdr = void 0;
108745
+ }
108746
+ if (effectiveHdr) {
108747
+ log.info(
108748
+ `[Render] HDR source detected \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
108749
+ );
108750
+ }
107908
108751
  const stage3Start = Date.now();
107909
108752
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
107910
108753
  const audioOutputPath = join15(workDir, "audio.aac");
@@ -107932,7 +108775,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107932
108775
  fileServer = await createFileServer2({
107933
108776
  projectDir,
107934
108777
  compiledDir: join15(workDir, "compiled"),
107935
- port: 0
108778
+ port: 0,
108779
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
107936
108780
  });
107937
108781
  assertNotAborted();
107938
108782
  }
@@ -107949,218 +108793,398 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107949
108793
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
107950
108794
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
107951
108795
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
107952
- const preset = getEncoderPreset(job.config.quality, outputFormat);
107953
- const effectiveQuality = job.config.crf ?? preset.quality;
107954
- const effectiveBitrate = job.config.videoBitrate;
107955
- const baseEncoderOpts = {
107956
- fps: job.config.fps,
107957
- width,
107958
- height,
107959
- codec: preset.codec,
107960
- preset: preset.preset,
107961
- quality: effectiveQuality,
107962
- bitrate: effectiveBitrate,
107963
- pixelFormat: preset.pixelFormat,
107964
- useGpu: job.config.useGpu
107965
- };
108796
+ const hasHdrVideo = effectiveHdr && composition.videos.length > 0 && frameLookup;
108797
+ const encoderHdr = hasHdrVideo ? effectiveHdr : void 0;
108798
+ const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
107966
108799
  job.framesRendered = 0;
107967
- let streamingEncoder = null;
107968
- if (enableStreamingEncode) {
107969
- streamingEncoder = await spawnStreamingEncoder(
108800
+ if (hasHdrVideo) {
108801
+ log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
108802
+ const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
108803
+ const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
108804
+ for (const v of composition.videos) {
108805
+ if (!hdrVideoIds.includes(v.id)) continue;
108806
+ let srcPath = v.src;
108807
+ if (!srcPath.startsWith("/")) {
108808
+ const fromCompiled = join15(compiledDir, srcPath);
108809
+ srcPath = existsSync15(fromCompiled) ? fromCompiled : join15(projectDir, srcPath);
108810
+ }
108811
+ hdrVideoSrcPaths.set(v.id, srcPath);
108812
+ }
108813
+ const domSession = await createCaptureSession(
108814
+ fileServer.url,
108815
+ framesDir,
108816
+ captureOptions,
108817
+ createVideoFrameInjector(frameLookup),
108818
+ cfg
108819
+ );
108820
+ await initializeSession(domSession);
108821
+ assertNotAborted();
108822
+ lastBrowserConsole = domSession.browserConsoleBuffer;
108823
+ await initTransparentBackground(domSession.page);
108824
+ const hdrEncoder = await spawnStreamingEncoder(
107970
108825
  videoOnlyPath,
107971
108826
  {
107972
- ...baseEncoderOpts,
107973
- imageFormat: captureOptions.format || "jpeg"
108827
+ fps: job.config.fps,
108828
+ width,
108829
+ height,
108830
+ codec: preset.codec,
108831
+ preset: preset.preset,
108832
+ quality: preset.quality,
108833
+ pixelFormat: preset.pixelFormat,
108834
+ hdr: preset.hdr,
108835
+ rawInputFormat: "rgb48le"
107974
108836
  },
107975
- abortSignal
108837
+ abortSignal,
108838
+ { ffmpegStreamingTimeout: 36e5 }
107976
108839
  );
107977
108840
  assertNotAborted();
107978
- }
107979
- if (enableStreamingEncode && streamingEncoder) {
107980
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
107981
- const currentEncoder = streamingEncoder;
107982
- if (workerCount > 1) {
107983
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
107984
- const onFrameBuffer = async (frameIndex, buffer) => {
107985
- await reorderBuffer.waitForFrame(frameIndex);
107986
- currentEncoder.writeFrame(buffer);
107987
- reorderBuffer.advanceTo(frameIndex + 1);
107988
- };
107989
- await executeParallelCapture(
107990
- fileServer.url,
107991
- workDir,
107992
- tasks,
107993
- captureOptions,
107994
- () => createVideoFrameInjector(frameLookup),
107995
- abortSignal,
107996
- (progress) => {
107997
- job.framesRendered = progress.capturedFrames;
107998
- const frameProgress = progress.capturedFrames / progress.totalFrames;
107999
- const progressPct = 25 + frameProgress * 55;
108000
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108001
- updateJobStatus(
108002
- job,
108003
- "rendering",
108004
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108005
- Math.round(progressPct),
108006
- onProgress
108841
+ const { execSync: execSync2 } = await import("child_process");
108842
+ const hdrFrameDirs = /* @__PURE__ */ new Map();
108843
+ for (const [videoId, srcPath] of hdrVideoSrcPaths) {
108844
+ const video = composition.videos.find((v) => v.id === videoId);
108845
+ if (!video) continue;
108846
+ const frameDir = join15(framesDir, `hdr_${videoId}`);
108847
+ mkdirSync10(frameDir, { recursive: true });
108848
+ const duration = video.end - video.start;
108849
+ try {
108850
+ execSync2(
108851
+ `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")}"`,
108852
+ { maxBuffer: 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
108853
+ );
108854
+ } catch (err) {
108855
+ log.warn("HDR frame pre-extraction failed; loop will fill with black", {
108856
+ videoId,
108857
+ srcPath,
108858
+ error: err instanceof Error ? err.message : String(err)
108859
+ });
108860
+ }
108861
+ hdrFrameDirs.set(videoId, frameDir);
108862
+ }
108863
+ assertNotAborted();
108864
+ try {
108865
+ const beforeCaptureHook = domSession.onBeforeCapture;
108866
+ for (let i = 0; i < job.totalFrames; i++) {
108867
+ assertNotAborted();
108868
+ const time = i / job.config.fps;
108869
+ await domSession.page.evaluate((t) => {
108870
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
108871
+ }, time);
108872
+ if (beforeCaptureHook) {
108873
+ await beforeCaptureHook(domSession.page, time);
108874
+ }
108875
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
108876
+ const layers = groupIntoLayers(stackingInfo);
108877
+ if (i % 30 === 0) {
108878
+ const hdrEl = stackingInfo.find((e) => e.isHdr);
108879
+ const hdrInLayers = layers.some((l) => l.type === "hdr");
108880
+ log.debug("[Render] HDR layer composite frame", {
108881
+ frame: i,
108882
+ time: time.toFixed(2),
108883
+ hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
108884
+ hdrLayerPresent: hdrInLayers,
108885
+ layerCount: layers.length
108886
+ });
108887
+ }
108888
+ const canvas = Buffer.alloc(width * height * 6);
108889
+ for (const layer of layers) {
108890
+ if (layer.type === "hdr") {
108891
+ const el = layer.element;
108892
+ const frameDir = hdrFrameDirs.get(el.id);
108893
+ const video = composition.videos.find((v) => v.id === el.id);
108894
+ if (!frameDir || !video) continue;
108895
+ const videoFrameIndex = Math.round((time - video.start) * job.config.fps) + 1;
108896
+ const maxIndex = getMaxFrameIndex(frameDir);
108897
+ const inBounds = videoFrameIndex >= 1 && (maxIndex === 0 || videoFrameIndex <= maxIndex);
108898
+ const framePath = inBounds ? join15(frameDir, `frame_${String(videoFrameIndex).padStart(4, "0")}.png`) : null;
108899
+ if (framePath !== null && existsSync15(framePath)) {
108900
+ try {
108901
+ const hdrRgb = decodePngToRgb48le(readFileSync9(framePath)).data;
108902
+ blitRgb48leRegion(
108903
+ canvas,
108904
+ hdrRgb,
108905
+ el.x,
108906
+ el.y,
108907
+ el.width,
108908
+ el.height,
108909
+ width,
108910
+ height,
108911
+ el.opacity < 0.999 ? el.opacity : void 0
108912
+ );
108913
+ } catch (err) {
108914
+ log.warn("HDR layer decode/blit failed; skipping layer for frame", {
108915
+ frameIndex: i,
108916
+ videoId: el.id,
108917
+ framePath,
108918
+ error: err instanceof Error ? err.message : String(err)
108919
+ });
108920
+ }
108921
+ }
108922
+ } else {
108923
+ const allElementIds = stackingInfo.map((e) => e.id);
108924
+ const layerIds = new Set(layer.elementIds);
108925
+ const hideIds = allElementIds.filter(
108926
+ (id) => !layerIds.has(id) || nativeHdrVideoIds.has(id)
108007
108927
  );
108928
+ await hideVideoElements(domSession.page, hideIds);
108929
+ const domPng = await captureAlphaPng(domSession.page, width, height);
108930
+ await showVideoElements(domSession.page, hideIds);
108931
+ await domSession.page.evaluate((t) => {
108932
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
108933
+ }, time);
108934
+ try {
108935
+ const { data: domRgba } = decodePng(domPng);
108936
+ const hdrTransfer = effectiveHdr ? effectiveHdr.transfer : "hlg";
108937
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, hdrTransfer);
108938
+ } catch (err) {
108939
+ log.warn("DOM layer decode/blit failed; skipping overlay for frame", {
108940
+ frameIndex: i,
108941
+ layerIds: layer.elementIds,
108942
+ error: err instanceof Error ? err.message : String(err)
108943
+ });
108944
+ }
108008
108945
  }
108009
- },
108010
- onFrameBuffer,
108011
- cfg
108012
- );
108013
- if (probeSession) {
108014
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108015
- await closeCaptureSession(probeSession);
108016
- probeSession = null;
108017
- }
108018
- } else {
108019
- const videoInjector = createVideoFrameInjector(frameLookup);
108020
- const session = probeSession ?? await createCaptureSession(
108021
- fileServer.url,
108022
- framesDir,
108023
- captureOptions,
108024
- videoInjector,
108025
- cfg
108026
- );
108027
- if (probeSession) {
108028
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108029
- probeSession = null;
108030
- }
108031
- try {
108032
- if (!session.isInitialized) {
108033
- await initializeSession(session);
108034
108946
  }
108035
- assertNotAborted();
108036
- lastBrowserConsole = session.browserConsoleBuffer;
108037
- for (let i = 0; i < job.totalFrames; i++) {
108038
- assertNotAborted();
108039
- const time = i / job.config.fps;
108040
- const { buffer } = await captureFrameToBuffer(session, i, time);
108041
- await reorderBuffer.waitForFrame(i);
108042
- currentEncoder.writeFrame(buffer);
108043
- reorderBuffer.advanceTo(i + 1);
108044
- job.framesRendered = i + 1;
108947
+ hdrEncoder.writeFrame(canvas);
108948
+ job.framesRendered = i + 1;
108949
+ if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
108045
108950
  const frameProgress = (i + 1) / job.totalFrames;
108046
- const progress = 25 + frameProgress * 55;
108047
108951
  updateJobStatus(
108048
108952
  job,
108049
108953
  "rendering",
108050
- `Streaming frame ${i + 1}/${job.totalFrames}`,
108051
- Math.round(progress),
108954
+ `HDR composite frame ${i + 1}/${job.totalFrames}`,
108955
+ Math.round(25 + frameProgress * 55),
108052
108956
  onProgress
108053
108957
  );
108054
108958
  }
108055
- } finally {
108056
- lastBrowserConsole = session.browserConsoleBuffer;
108057
- await closeCaptureSession(session);
108058
108959
  }
108960
+ } finally {
108961
+ lastBrowserConsole = domSession.browserConsoleBuffer;
108962
+ await closeCaptureSession(domSession);
108059
108963
  }
108060
- const encodeResult = await currentEncoder.close();
108964
+ const hdrEncodeResult = await hdrEncoder.close();
108061
108965
  assertNotAborted();
108062
- if (!encodeResult.success) {
108063
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108966
+ if (!hdrEncodeResult.success) {
108967
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
108064
108968
  }
108065
108969
  perfStages.captureMs = Date.now() - stage4Start;
108066
- perfStages.encodeMs = encodeResult.durationMs;
108970
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
108067
108971
  } else {
108068
- if (workerCount > 1) {
108069
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108070
- await executeParallelCapture(
108071
- fileServer.url,
108072
- workDir,
108073
- tasks,
108074
- captureOptions,
108075
- () => createVideoFrameInjector(frameLookup),
108076
- abortSignal,
108077
- (progress) => {
108078
- job.framesRendered = progress.capturedFrames;
108079
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108080
- const progressPct = 25 + frameProgress * 45;
108081
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108972
+ let streamingEncoder = null;
108973
+ if (enableStreamingEncode) {
108974
+ streamingEncoder = await spawnStreamingEncoder(
108975
+ videoOnlyPath,
108976
+ {
108977
+ fps: job.config.fps,
108978
+ width,
108979
+ height,
108980
+ codec: preset.codec,
108981
+ preset: preset.preset,
108982
+ quality: preset.quality,
108983
+ pixelFormat: preset.pixelFormat,
108984
+ useGpu: job.config.useGpu,
108985
+ imageFormat: captureOptions.format || "jpeg",
108986
+ hdr: preset.hdr
108987
+ },
108988
+ abortSignal
108989
+ );
108990
+ assertNotAborted();
108991
+ }
108992
+ if (enableStreamingEncode && streamingEncoder) {
108993
+ const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
108994
+ const currentEncoder = streamingEncoder;
108995
+ if (workerCount > 1) {
108996
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108997
+ const onFrameBuffer = async (frameIndex, buffer) => {
108998
+ await reorderBuffer.waitForFrame(frameIndex);
108999
+ currentEncoder.writeFrame(buffer);
109000
+ reorderBuffer.advanceTo(frameIndex + 1);
109001
+ };
109002
+ await executeParallelCapture(
109003
+ fileServer.url,
109004
+ workDir,
109005
+ tasks,
109006
+ captureOptions,
109007
+ () => createVideoFrameInjector(frameLookup),
109008
+ abortSignal,
109009
+ (progress) => {
109010
+ job.framesRendered = progress.capturedFrames;
109011
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
109012
+ const progressPct = 25 + frameProgress * 55;
109013
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
109014
+ updateJobStatus(
109015
+ job,
109016
+ "rendering",
109017
+ `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
109018
+ Math.round(progressPct),
109019
+ onProgress
109020
+ );
109021
+ }
109022
+ },
109023
+ onFrameBuffer,
109024
+ cfg
109025
+ );
109026
+ if (probeSession) {
109027
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
109028
+ await closeCaptureSession(probeSession);
109029
+ probeSession = null;
109030
+ }
109031
+ } else {
109032
+ const videoInjector = createVideoFrameInjector(frameLookup);
109033
+ const session = probeSession ?? await createCaptureSession(
109034
+ fileServer.url,
109035
+ framesDir,
109036
+ captureOptions,
109037
+ videoInjector,
109038
+ cfg
109039
+ );
109040
+ if (probeSession) {
109041
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
109042
+ probeSession = null;
109043
+ }
109044
+ try {
109045
+ if (!session.isInitialized) {
109046
+ await initializeSession(session);
109047
+ }
109048
+ assertNotAborted();
109049
+ lastBrowserConsole = session.browserConsoleBuffer;
109050
+ for (let i = 0; i < job.totalFrames; i++) {
109051
+ assertNotAborted();
109052
+ const time = i / job.config.fps;
109053
+ const { buffer } = await captureFrameToBuffer(session, i, time);
109054
+ await reorderBuffer.waitForFrame(i);
109055
+ currentEncoder.writeFrame(buffer);
109056
+ reorderBuffer.advanceTo(i + 1);
109057
+ job.framesRendered = i + 1;
109058
+ const frameProgress = (i + 1) / job.totalFrames;
109059
+ const progress = 25 + frameProgress * 55;
108082
109060
  updateJobStatus(
108083
109061
  job,
108084
109062
  "rendering",
108085
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108086
- Math.round(progressPct),
109063
+ `Streaming frame ${i + 1}/${job.totalFrames}`,
109064
+ Math.round(progress),
108087
109065
  onProgress
108088
109066
  );
108089
109067
  }
108090
- },
108091
- void 0,
108092
- cfg
108093
- );
108094
- await mergeWorkerFrames(workDir, tasks, framesDir);
108095
- if (probeSession) {
108096
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108097
- await closeCaptureSession(probeSession);
108098
- probeSession = null;
109068
+ } finally {
109069
+ lastBrowserConsole = session.browserConsoleBuffer;
109070
+ await closeCaptureSession(session);
109071
+ }
108099
109072
  }
108100
- } else {
108101
- const videoInjector = createVideoFrameInjector(frameLookup);
108102
- const session = probeSession ?? await createCaptureSession(
108103
- fileServer.url,
108104
- framesDir,
108105
- captureOptions,
108106
- videoInjector,
108107
- cfg
108108
- );
108109
- if (probeSession) {
108110
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108111
- probeSession = null;
109073
+ const encodeResult = await currentEncoder.close();
109074
+ assertNotAborted();
109075
+ if (!encodeResult.success) {
109076
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108112
109077
  }
108113
- try {
108114
- if (!session.isInitialized) {
108115
- await initializeSession(session);
109078
+ perfStages.captureMs = Date.now() - stage4Start;
109079
+ perfStages.encodeMs = encodeResult.durationMs;
109080
+ } else {
109081
+ if (workerCount > 1) {
109082
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
109083
+ await executeParallelCapture(
109084
+ fileServer.url,
109085
+ workDir,
109086
+ tasks,
109087
+ captureOptions,
109088
+ () => createVideoFrameInjector(frameLookup),
109089
+ abortSignal,
109090
+ (progress) => {
109091
+ job.framesRendered = progress.capturedFrames;
109092
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
109093
+ const progressPct = 25 + frameProgress * 45;
109094
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
109095
+ updateJobStatus(
109096
+ job,
109097
+ "rendering",
109098
+ `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
109099
+ Math.round(progressPct),
109100
+ onProgress
109101
+ );
109102
+ }
109103
+ },
109104
+ void 0,
109105
+ cfg
109106
+ );
109107
+ await mergeWorkerFrames(workDir, tasks, framesDir);
109108
+ if (probeSession) {
109109
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
109110
+ await closeCaptureSession(probeSession);
109111
+ probeSession = null;
108116
109112
  }
108117
- assertNotAborted();
108118
- lastBrowserConsole = session.browserConsoleBuffer;
108119
- for (let i = 0; i < job.totalFrames; i++) {
108120
- assertNotAborted();
108121
- const time = i / job.config.fps;
108122
- await captureFrame(session, i, time);
108123
- job.framesRendered = i + 1;
108124
- const frameProgress = (i + 1) / job.totalFrames;
108125
- const progress = 25 + frameProgress * 45;
108126
- updateJobStatus(
108127
- job,
108128
- "rendering",
108129
- `Capturing frame ${i + 1}/${job.totalFrames}`,
108130
- Math.round(progress),
108131
- onProgress
108132
- );
109113
+ } else {
109114
+ const videoInjector = createVideoFrameInjector(frameLookup);
109115
+ const session = probeSession ?? await createCaptureSession(
109116
+ fileServer.url,
109117
+ framesDir,
109118
+ captureOptions,
109119
+ videoInjector,
109120
+ cfg
109121
+ );
109122
+ if (probeSession) {
109123
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
109124
+ probeSession = null;
108133
109125
  }
108134
- } finally {
108135
- lastBrowserConsole = session.browserConsoleBuffer;
108136
- await closeCaptureSession(session);
109126
+ try {
109127
+ if (!session.isInitialized) {
109128
+ await initializeSession(session);
109129
+ }
109130
+ assertNotAborted();
109131
+ lastBrowserConsole = session.browserConsoleBuffer;
109132
+ for (let i = 0; i < job.totalFrames; i++) {
109133
+ assertNotAborted();
109134
+ const time = i / job.config.fps;
109135
+ await captureFrame(session, i, time);
109136
+ job.framesRendered = i + 1;
109137
+ const frameProgress = (i + 1) / job.totalFrames;
109138
+ const progress = 25 + frameProgress * 45;
109139
+ updateJobStatus(
109140
+ job,
109141
+ "rendering",
109142
+ `Capturing frame ${i + 1}/${job.totalFrames}`,
109143
+ Math.round(progress),
109144
+ onProgress
109145
+ );
109146
+ }
109147
+ } finally {
109148
+ lastBrowserConsole = session.browserConsoleBuffer;
109149
+ await closeCaptureSession(session);
109150
+ }
109151
+ }
109152
+ perfStages.captureMs = Date.now() - stage4Start;
109153
+ const stage5Start = Date.now();
109154
+ updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
109155
+ const frameExt = needsAlpha ? "png" : "jpg";
109156
+ const framePattern = `frame_%06d.${frameExt}`;
109157
+ const encoderOpts = {
109158
+ fps: job.config.fps,
109159
+ width,
109160
+ height,
109161
+ codec: preset.codec,
109162
+ preset: preset.preset,
109163
+ quality: preset.quality,
109164
+ pixelFormat: preset.pixelFormat,
109165
+ useGpu: job.config.useGpu,
109166
+ hdr: preset.hdr
109167
+ };
109168
+ const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
109169
+ framesDir,
109170
+ framePattern,
109171
+ videoOnlyPath,
109172
+ encoderOpts,
109173
+ chunkedEncodeSize,
109174
+ abortSignal
109175
+ ) : await encodeFramesFromDir(
109176
+ framesDir,
109177
+ framePattern,
109178
+ videoOnlyPath,
109179
+ encoderOpts,
109180
+ abortSignal
109181
+ );
109182
+ assertNotAborted();
109183
+ if (!encodeResult.success) {
109184
+ throw new Error(`Encoding failed: ${encodeResult.error}`);
108137
109185
  }
109186
+ perfStages.encodeMs = Date.now() - stage5Start;
108138
109187
  }
108139
- perfStages.captureMs = Date.now() - stage4Start;
108140
- const stage5Start = Date.now();
108141
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
108142
- const frameExt = needsAlpha ? "png" : "jpg";
108143
- const framePattern = `frame_%06d.${frameExt}`;
108144
- const encoderOpts = baseEncoderOpts;
108145
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
108146
- framesDir,
108147
- framePattern,
108148
- videoOnlyPath,
108149
- encoderOpts,
108150
- chunkedEncodeSize,
108151
- abortSignal
108152
- ) : await encodeFramesFromDir(
108153
- framesDir,
108154
- framePattern,
108155
- videoOnlyPath,
108156
- encoderOpts,
108157
- abortSignal
108158
- );
108159
- assertNotAborted();
108160
- if (!encodeResult.success) {
108161
- throw new Error(`Encoding failed: ${encodeResult.error}`);
108162
- }
108163
- perfStages.encodeMs = Date.now() - stage5Start;
108164
109188
  }
108165
109189
  if (probeSession !== null) {
108166
109190
  const remainingProbeSession = probeSession;