@hyperframes/producer 0.4.6 → 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);
@@ -101452,7 +101479,7 @@ var ENCODER_PRESETS = {
101452
101479
  standard: { preset: "medium", quality: 18, codec: "h264" },
101453
101480
  high: { preset: "slow", quality: 15, codec: "h264" }
101454
101481
  };
101455
- function getEncoderPreset(quality, format3 = "mp4") {
101482
+ function getEncoderPreset(quality, format3 = "mp4", hdr) {
101456
101483
  const base = ENCODER_PRESETS[quality];
101457
101484
  if (format3 === "webm") {
101458
101485
  return {
@@ -101470,6 +101497,15 @@ function getEncoderPreset(quality, format3 = "mp4") {
101470
101497
  pixelFormat: "yuva444p10le"
101471
101498
  };
101472
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
+ }
101473
101509
  return { ...base, pixelFormat: "yuv420p" };
101474
101510
  }
101475
101511
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -101527,6 +101563,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
101527
101563
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
101528
101564
  }
101529
101565
  }
101566
+ if (codec === "h265") {
101567
+ args.push("-tag:v", "hvc1");
101568
+ }
101530
101569
  } else if (codec === "vp9") {
101531
101570
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
101532
101571
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -101833,31 +101872,79 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
101833
101872
  import { spawn as spawn6 } from "child_process";
101834
101873
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync4 } from "fs";
101835
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
101836
101912
  function createFrameReorderBuffer(startFrame, endFrame) {
101837
- let nextFrame = startFrame;
101838
- let waiters = [];
101839
- const resolveWaiters = () => {
101840
- for (const waiter of waiters.slice()) {
101841
- if (waiter.frame === nextFrame) {
101842
- waiter.resolve();
101843
- waiters = waiters.filter((w) => w !== waiter);
101844
- }
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);
101845
101921
  }
101846
101922
  };
101847
- return {
101848
- waitForFrame: (frame) => new Promise((resolve13) => {
101849
- waiters.push({ frame, resolve: resolve13 });
101850
- resolveWaiters();
101851
- }),
101852
- advanceTo: (frame) => {
101853
- nextFrame = frame;
101854
- resolveWaiters();
101855
- },
101856
- waitForAllDone: () => new Promise((resolve13) => {
101857
- waiters.push({ frame: endFrame, resolve: resolve13 });
101858
- resolveWaiters();
101859
- })
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();
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);
101860
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 };
101861
101948
  }
101862
101949
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101863
101950
  const {
@@ -101870,19 +101957,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101870
101957
  useGpu = false,
101871
101958
  imageFormat = "jpeg"
101872
101959
  } = options;
101873
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
101874
- const args = [
101875
- "-f",
101876
- "image2pipe",
101877
- "-vcodec",
101878
- inputCodec,
101879
- "-framerate",
101880
- String(fps),
101881
- "-i",
101882
- "-",
101883
- "-r",
101884
- String(fps)
101885
- ];
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));
101886
101990
  const shouldUseGpu = useGpu && gpuEncoder !== null;
101887
101991
  if (codec === "h264" || codec === "h265") {
101888
101992
  if (shouldUseGpu) {
@@ -101920,12 +102024,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101920
102024
  if (bitrate) args.push("-b:v", bitrate);
101921
102025
  else args.push("-crf", String(quality));
101922
102026
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
101923
- 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";
101924
102028
  if (preset === "ultrafast") {
101925
102029
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
101926
102030
  } else {
101927
102031
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
101928
102032
  }
102033
+ if (codec === "h265") {
102034
+ args.push("-tag:v", "hvc1");
102035
+ }
101929
102036
  }
101930
102037
  } else if (codec === "vp9") {
101931
102038
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -101941,17 +102048,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
101941
102048
  return [...args, "-y", outputPath];
101942
102049
  }
101943
102050
  if (codec === "h264" || codec === "h265") {
101944
- args.push(
101945
- "-colorspace:v",
101946
- "bt709",
101947
- "-color_primaries:v",
101948
- "bt709",
101949
- "-color_trc:v",
101950
- "bt709",
101951
- "-color_range",
101952
- "tv"
101953
- );
101954
- if (gpuEncoder === "vaapi") {
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") {
101955
102076
  const vfIdx = args.indexOf("-vf");
101956
102077
  if (vfIdx !== -1) {
101957
102078
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -102021,14 +102142,16 @@ Process error: ${err.message}`;
102021
102142
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
102022
102143
  return false;
102023
102144
  }
102024
- return ffmpeg.stdin.write(buffer);
102145
+ const copy = Buffer.from(buffer);
102146
+ return ffmpeg.stdin.write(copy);
102025
102147
  },
102026
102148
  close: async () => {
102027
102149
  clearTimeout(timer2);
102028
102150
  if (signal) signal.removeEventListener("abort", onAbort);
102029
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
102151
+ const stdin = ffmpeg.stdin;
102152
+ if (stdin && !stdin.destroyed) {
102030
102153
  await new Promise((resolve13) => {
102031
- ffmpeg.stdin.end(() => resolve13());
102154
+ stdin.end(() => resolve13());
102032
102155
  });
102033
102156
  }
102034
102157
  await exitPromise;
@@ -102132,6 +102255,10 @@ async function extractVideoMetadata(filePath) {
102132
102255
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
102133
102256
  const fps = avgFps || rFps;
102134
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);
102135
102262
  return {
102136
102263
  durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
102137
102264
  width: videoStream.width || 0,
@@ -102139,7 +102266,8 @@ async function extractVideoMetadata(filePath) {
102139
102266
  fps,
102140
102267
  videoCodec: videoStream.codec_name || "unknown",
102141
102268
  hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
102142
- isVFR
102269
+ isVFR,
102270
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
102143
102271
  };
102144
102272
  })();
102145
102273
  videoMetadataCache.set(filePath, probePromise);
@@ -102347,18 +102475,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102347
102475
  const metadata = await extractVideoMetadata(videoPath);
102348
102476
  const framePattern = `frame_%05d.${format3}`;
102349
102477
  const outputPattern = join8(videoOutputDir, framePattern);
102350
- const args = [
102351
- "-ss",
102352
- String(startTime),
102353
- "-i",
102354
- videoPath,
102355
- "-t",
102356
- String(duration),
102357
- "-vf",
102358
- `fps=${fps}`,
102359
- "-q:v",
102360
- format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
102361
- ];
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");
102362
102492
  if (format3 === "png") args.push("-compression_level", "6");
102363
102493
  args.push("-y", outputPattern);
102364
102494
  return new Promise((resolve13, reject) => {
@@ -102418,30 +102548,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102418
102548
  });
102419
102549
  });
102420
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
+ }
102421
102582
  async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
102422
102583
  const startTime = Date.now();
102423
102584
  const extracted = [];
102424
102585
  const errors = [];
102425
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
+ }
102426
102639
  const results = await Promise.all(
102427
- videos.map(async (video) => {
102640
+ resolvedVideos.map(async ({ video, videoPath }) => {
102428
102641
  if (signal?.aborted) {
102429
102642
  throw new Error("Video frame extraction cancelled");
102430
102643
  }
102431
102644
  try {
102432
- let videoPath = video.src;
102433
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
102434
- const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
102435
- videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
102436
- }
102437
- if (isHttpUrl(videoPath)) {
102438
- const downloadDir = join8(options.outputDir, "_downloads");
102439
- mkdirSync5(downloadDir, { recursive: true });
102440
- videoPath = await downloadToTemp(videoPath, downloadDir);
102441
- }
102442
- if (!existsSync8(videoPath)) {
102443
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
102444
- }
102445
102645
  let videoDuration = video.end - video.start;
102446
102646
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
102447
102647
  const metadata = await extractVideoMetadata(videoPath);
@@ -102676,6 +102876,74 @@ function createVideoFrameInjector(frameLookup, config2) {
102676
102876
  }
102677
102877
  };
102678
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
+ }
102679
102947
 
102680
102948
  // ../engine/src/services/audioMixer.ts
102681
102949
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
@@ -105786,6 +106054,292 @@ var serve = (options, listeningListener) => {
105786
106054
  return server;
105787
106055
  };
105788
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
+
105789
106343
  // src/services/renderOrchestrator.ts
105790
106344
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
105791
106345
  import { randomUUID } from "crypto";
@@ -106106,6 +106660,10 @@ var RENDER_MODE_SCRIPT = `(function() {
106106
106660
  }
106107
106661
  waitForPlayer();
106108
106662
  })();`;
106663
+ var HF_EARLY_STUB = `(function() {
106664
+ if (typeof window === "undefined") return;
106665
+ if (!window.__hf) window.__hf = {};
106666
+ })();`;
106109
106667
  var HF_BRIDGE_SCRIPT = `(function() {
106110
106668
  var __realSetInterval =
106111
106669
  window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
@@ -106151,20 +106709,24 @@ var HF_BRIDGE_SCRIPT = `(function() {
106151
106709
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
106152
106710
  return false;
106153
106711
  }
106154
- window.__hf = {
106155
- get duration() {
106712
+ var hf = window.__hf || {};
106713
+ Object.defineProperty(hf, "duration", {
106714
+ configurable: true,
106715
+ enumerable: true,
106716
+ get: function() {
106156
106717
  var d = p.getDuration();
106157
106718
  return d > 0 ? d : getDeclaredDuration();
106158
106719
  },
106159
- seek: function(t) {
106160
- p.renderSeek(t);
106161
- var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
106162
- if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
106163
- window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106164
- }
106165
- seekSameOriginChildFrames(window, nextTimeMs);
106166
- },
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);
106167
106728
  };
106729
+ window.__hf = hf;
106168
106730
  return true;
106169
106731
  }
106170
106732
  if (bridge()) return;
@@ -106246,7 +106808,7 @@ ${headTags}`);
106246
106808
  }
106247
106809
  function createFileServer2(options) {
106248
106810
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
106249
- const preHeadScripts = options.preHeadScripts ?? [];
106811
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
106250
106812
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
106251
106813
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
106252
106814
  const app = new Hono2();
@@ -107506,6 +108068,24 @@ async function safeCleanup(label, fn, log = defaultLogger) {
107506
108068
  });
107507
108069
  }
107508
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
+ }
107509
108089
  var RenderCancelledError = class extends Error {
107510
108090
  reason;
107511
108091
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -107905,7 +108485,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107905
108485
  }
107906
108486
  }
107907
108487
  }
107908
- } 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
+ });
107909
108492
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
107910
108493
  }
107911
108494
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -107929,8 +108512,26 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107929
108512
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107930
108513
  let frameLookup = null;
107931
108514
  const compiledDir = join15(workDir, "compiled");
108515
+ let extractionResult = null;
108516
+ const nativeHdrVideoIds = /* @__PURE__ */ new Set();
107932
108517
  if (composition.videos.length > 0) {
107933
- const extractionResult = await extractAllVideoFrames(
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
+ }
108533
+ if (composition.videos.length > 0) {
108534
+ extractionResult = await extractAllVideoFrames(
107934
108535
  composition.videos,
107935
108536
  projectDir,
107936
108537
  { fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
@@ -107965,6 +108566,23 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107965
108566
  } else {
107966
108567
  perfStages.videoExtractMs = Date.now() - stage2Start;
107967
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
+ }
107968
108586
  const stage3Start = Date.now();
107969
108587
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
107970
108588
  const audioOutputPath = join15(workDir, "audio.aac");
@@ -108010,218 +108628,398 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108010
108628
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
108011
108629
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
108012
108630
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
108013
- const preset = getEncoderPreset(job.config.quality, outputFormat);
108014
- const effectiveQuality = job.config.crf ?? preset.quality;
108015
- const effectiveBitrate = job.config.videoBitrate;
108016
- const baseEncoderOpts = {
108017
- fps: job.config.fps,
108018
- width,
108019
- height,
108020
- codec: preset.codec,
108021
- preset: preset.preset,
108022
- quality: effectiveQuality,
108023
- bitrate: effectiveBitrate,
108024
- pixelFormat: preset.pixelFormat,
108025
- useGpu: job.config.useGpu
108026
- };
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);
108027
108634
  job.framesRendered = 0;
108028
- let streamingEncoder = null;
108029
- if (enableStreamingEncode) {
108030
- 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(
108031
108660
  videoOnlyPath,
108032
108661
  {
108033
- ...baseEncoderOpts,
108034
- 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"
108035
108671
  },
108036
- abortSignal
108672
+ abortSignal,
108673
+ { ffmpegStreamingTimeout: 36e5 }
108037
108674
  );
108038
108675
  assertNotAborted();
108039
- }
108040
- if (enableStreamingEncode && streamingEncoder) {
108041
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
108042
- const currentEncoder = streamingEncoder;
108043
- if (workerCount > 1) {
108044
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108045
- const onFrameBuffer = async (frameIndex, buffer) => {
108046
- await reorderBuffer.waitForFrame(frameIndex);
108047
- currentEncoder.writeFrame(buffer);
108048
- reorderBuffer.advanceTo(frameIndex + 1);
108049
- };
108050
- await executeParallelCapture(
108051
- fileServer.url,
108052
- workDir,
108053
- tasks,
108054
- captureOptions,
108055
- () => createVideoFrameInjector(frameLookup),
108056
- abortSignal,
108057
- (progress) => {
108058
- job.framesRendered = progress.capturedFrames;
108059
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108060
- const progressPct = 25 + frameProgress * 55;
108061
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108062
- updateJobStatus(
108063
- job,
108064
- "rendering",
108065
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108066
- Math.round(progressPct),
108067
- onProgress
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)
108068
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
+ }
108069
108780
  }
108070
- },
108071
- onFrameBuffer,
108072
- cfg
108073
- );
108074
- if (probeSession) {
108075
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108076
- await closeCaptureSession(probeSession);
108077
- probeSession = null;
108078
- }
108079
- } else {
108080
- const videoInjector = createVideoFrameInjector(frameLookup);
108081
- const session = probeSession ?? await createCaptureSession(
108082
- fileServer.url,
108083
- framesDir,
108084
- captureOptions,
108085
- videoInjector,
108086
- cfg
108087
- );
108088
- if (probeSession) {
108089
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108090
- probeSession = null;
108091
- }
108092
- try {
108093
- if (!session.isInitialized) {
108094
- await initializeSession(session);
108095
108781
  }
108096
- assertNotAborted();
108097
- lastBrowserConsole = session.browserConsoleBuffer;
108098
- for (let i = 0; i < job.totalFrames; i++) {
108099
- assertNotAborted();
108100
- const time = i / job.config.fps;
108101
- const { buffer } = await captureFrameToBuffer(session, i, time);
108102
- await reorderBuffer.waitForFrame(i);
108103
- currentEncoder.writeFrame(buffer);
108104
- reorderBuffer.advanceTo(i + 1);
108105
- job.framesRendered = i + 1;
108782
+ hdrEncoder.writeFrame(canvas);
108783
+ job.framesRendered = i + 1;
108784
+ if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
108106
108785
  const frameProgress = (i + 1) / job.totalFrames;
108107
- const progress = 25 + frameProgress * 55;
108108
108786
  updateJobStatus(
108109
108787
  job,
108110
108788
  "rendering",
108111
- `Streaming frame ${i + 1}/${job.totalFrames}`,
108112
- Math.round(progress),
108789
+ `HDR composite frame ${i + 1}/${job.totalFrames}`,
108790
+ Math.round(25 + frameProgress * 55),
108113
108791
  onProgress
108114
108792
  );
108115
108793
  }
108116
- } finally {
108117
- lastBrowserConsole = session.browserConsoleBuffer;
108118
- await closeCaptureSession(session);
108119
108794
  }
108795
+ } finally {
108796
+ lastBrowserConsole = domSession.browserConsoleBuffer;
108797
+ await closeCaptureSession(domSession);
108120
108798
  }
108121
- const encodeResult = await currentEncoder.close();
108799
+ const hdrEncodeResult = await hdrEncoder.close();
108122
108800
  assertNotAborted();
108123
- if (!encodeResult.success) {
108124
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108801
+ if (!hdrEncodeResult.success) {
108802
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
108125
108803
  }
108126
108804
  perfStages.captureMs = Date.now() - stage4Start;
108127
- perfStages.encodeMs = encodeResult.durationMs;
108805
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
108128
108806
  } else {
108129
- if (workerCount > 1) {
108130
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108131
- await executeParallelCapture(
108132
- fileServer.url,
108133
- workDir,
108134
- tasks,
108135
- captureOptions,
108136
- () => createVideoFrameInjector(frameLookup),
108137
- abortSignal,
108138
- (progress) => {
108139
- job.framesRendered = progress.capturedFrames;
108140
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108141
- const progressPct = 25 + frameProgress * 45;
108142
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
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;
108143
108895
  updateJobStatus(
108144
108896
  job,
108145
108897
  "rendering",
108146
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108147
- Math.round(progressPct),
108898
+ `Streaming frame ${i + 1}/${job.totalFrames}`,
108899
+ Math.round(progress),
108148
108900
  onProgress
108149
108901
  );
108150
108902
  }
108151
- },
108152
- void 0,
108153
- cfg
108154
- );
108155
- await mergeWorkerFrames(workDir, tasks, framesDir);
108156
- if (probeSession) {
108157
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108158
- await closeCaptureSession(probeSession);
108159
- probeSession = null;
108903
+ } finally {
108904
+ lastBrowserConsole = session.browserConsoleBuffer;
108905
+ await closeCaptureSession(session);
108906
+ }
108160
108907
  }
108161
- } else {
108162
- const videoInjector = createVideoFrameInjector(frameLookup);
108163
- const session = probeSession ?? await createCaptureSession(
108164
- fileServer.url,
108165
- framesDir,
108166
- captureOptions,
108167
- videoInjector,
108168
- cfg
108169
- );
108170
- if (probeSession) {
108171
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108172
- probeSession = null;
108908
+ const encodeResult = await currentEncoder.close();
108909
+ assertNotAborted();
108910
+ if (!encodeResult.success) {
108911
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108173
108912
  }
108174
- try {
108175
- if (!session.isInitialized) {
108176
- 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;
108177
108947
  }
108178
- assertNotAborted();
108179
- lastBrowserConsole = session.browserConsoleBuffer;
108180
- for (let i = 0; i < job.totalFrames; i++) {
108181
- assertNotAborted();
108182
- const time = i / job.config.fps;
108183
- await captureFrame(session, i, time);
108184
- job.framesRendered = i + 1;
108185
- const frameProgress = (i + 1) / job.totalFrames;
108186
- const progress = 25 + frameProgress * 45;
108187
- updateJobStatus(
108188
- job,
108189
- "rendering",
108190
- `Capturing frame ${i + 1}/${job.totalFrames}`,
108191
- Math.round(progress),
108192
- onProgress
108193
- );
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;
108194
108960
  }
108195
- } finally {
108196
- lastBrowserConsole = session.browserConsoleBuffer;
108197
- 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}`);
108198
109020
  }
109021
+ perfStages.encodeMs = Date.now() - stage5Start;
108199
109022
  }
108200
- perfStages.captureMs = Date.now() - stage4Start;
108201
- const stage5Start = Date.now();
108202
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
108203
- const frameExt = needsAlpha ? "png" : "jpg";
108204
- const framePattern = `frame_%06d.${frameExt}`;
108205
- const encoderOpts = baseEncoderOpts;
108206
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
108207
- framesDir,
108208
- framePattern,
108209
- videoOnlyPath,
108210
- encoderOpts,
108211
- chunkedEncodeSize,
108212
- abortSignal
108213
- ) : await encodeFramesFromDir(
108214
- framesDir,
108215
- framePattern,
108216
- videoOnlyPath,
108217
- encoderOpts,
108218
- abortSignal
108219
- );
108220
- assertNotAborted();
108221
- if (!encodeResult.success) {
108222
- throw new Error(`Encoding failed: ${encodeResult.error}`);
108223
- }
108224
- perfStages.encodeMs = Date.now() - stage5Start;
108225
109023
  }
108226
109024
  if (probeSession !== null) {
108227
109025
  const remainingProbeSession = probeSession;