@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.
@@ -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);
@@ -104241,7 +104268,7 @@ var ENCODER_PRESETS = {
104241
104268
  standard: { preset: "medium", quality: 18, codec: "h264" },
104242
104269
  high: { preset: "slow", quality: 15, codec: "h264" }
104243
104270
  };
104244
- function getEncoderPreset(quality, format3 = "mp4") {
104271
+ function getEncoderPreset(quality, format3 = "mp4", hdr) {
104245
104272
  const base = ENCODER_PRESETS[quality];
104246
104273
  if (format3 === "webm") {
104247
104274
  return {
@@ -104259,6 +104286,15 @@ function getEncoderPreset(quality, format3 = "mp4") {
104259
104286
  pixelFormat: "yuva444p10le"
104260
104287
  };
104261
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
+ }
104262
104298
  return { ...base, pixelFormat: "yuv420p" };
104263
104299
  }
104264
104300
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -104316,6 +104352,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
104316
104352
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
104317
104353
  }
104318
104354
  }
104355
+ if (codec === "h265") {
104356
+ args.push("-tag:v", "hvc1");
104357
+ }
104319
104358
  } else if (codec === "vp9") {
104320
104359
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
104321
104360
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -104622,31 +104661,79 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
104622
104661
  import { spawn as spawn6 } from "child_process";
104623
104662
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync4 } from "fs";
104624
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
104625
104701
  function createFrameReorderBuffer(startFrame, endFrame) {
104626
- let nextFrame = startFrame;
104627
- let waiters = [];
104628
- const resolveWaiters = () => {
104629
- for (const waiter of waiters.slice()) {
104630
- if (waiter.frame === nextFrame) {
104631
- waiter.resolve();
104632
- waiters = waiters.filter((w) => w !== waiter);
104633
- }
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);
104634
104710
  }
104635
104711
  };
104636
- return {
104637
- waitForFrame: (frame) => new Promise((resolve13) => {
104638
- waiters.push({ frame, resolve: resolve13 });
104639
- resolveWaiters();
104640
- }),
104641
- advanceTo: (frame) => {
104642
- nextFrame = frame;
104643
- resolveWaiters();
104644
- },
104645
- waitForAllDone: () => new Promise((resolve13) => {
104646
- waiters.push({ frame: endFrame, resolve: resolve13 });
104647
- resolveWaiters();
104648
- })
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();
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);
104649
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 };
104650
104737
  }
104651
104738
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104652
104739
  const {
@@ -104659,19 +104746,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104659
104746
  useGpu = false,
104660
104747
  imageFormat = "jpeg"
104661
104748
  } = options;
104662
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
104663
- const args = [
104664
- "-f",
104665
- "image2pipe",
104666
- "-vcodec",
104667
- inputCodec,
104668
- "-framerate",
104669
- String(fps),
104670
- "-i",
104671
- "-",
104672
- "-r",
104673
- String(fps)
104674
- ];
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));
104675
104779
  const shouldUseGpu = useGpu && gpuEncoder !== null;
104676
104780
  if (codec === "h264" || codec === "h265") {
104677
104781
  if (shouldUseGpu) {
@@ -104709,12 +104813,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104709
104813
  if (bitrate) args.push("-b:v", bitrate);
104710
104814
  else args.push("-crf", String(quality));
104711
104815
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
104712
- 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";
104713
104817
  if (preset === "ultrafast") {
104714
104818
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
104715
104819
  } else {
104716
104820
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
104717
104821
  }
104822
+ if (codec === "h265") {
104823
+ args.push("-tag:v", "hvc1");
104824
+ }
104718
104825
  }
104719
104826
  } else if (codec === "vp9") {
104720
104827
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -104730,17 +104837,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
104730
104837
  return [...args, "-y", outputPath];
104731
104838
  }
104732
104839
  if (codec === "h264" || codec === "h265") {
104733
- args.push(
104734
- "-colorspace:v",
104735
- "bt709",
104736
- "-color_primaries:v",
104737
- "bt709",
104738
- "-color_trc:v",
104739
- "bt709",
104740
- "-color_range",
104741
- "tv"
104742
- );
104743
- 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") {
104744
104865
  const vfIdx = args.indexOf("-vf");
104745
104866
  if (vfIdx !== -1) {
104746
104867
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -104810,14 +104931,16 @@ Process error: ${err.message}`;
104810
104931
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
104811
104932
  return false;
104812
104933
  }
104813
- return ffmpeg.stdin.write(buffer);
104934
+ const copy = Buffer.from(buffer);
104935
+ return ffmpeg.stdin.write(copy);
104814
104936
  },
104815
104937
  close: async () => {
104816
104938
  clearTimeout(timer2);
104817
104939
  if (signal) signal.removeEventListener("abort", onAbort);
104818
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
104940
+ const stdin = ffmpeg.stdin;
104941
+ if (stdin && !stdin.destroyed) {
104819
104942
  await new Promise((resolve13) => {
104820
- ffmpeg.stdin.end(() => resolve13());
104943
+ stdin.end(() => resolve13());
104821
104944
  });
104822
104945
  }
104823
104946
  await exitPromise;
@@ -104921,6 +105044,10 @@ async function extractVideoMetadata(filePath) {
104921
105044
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
104922
105045
  const fps = avgFps || rFps;
104923
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);
104924
105051
  return {
104925
105052
  durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
104926
105053
  width: videoStream.width || 0,
@@ -104928,7 +105055,8 @@ async function extractVideoMetadata(filePath) {
104928
105055
  fps,
104929
105056
  videoCodec: videoStream.codec_name || "unknown",
104930
105057
  hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
104931
- isVFR
105058
+ isVFR,
105059
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
104932
105060
  };
104933
105061
  })();
104934
105062
  videoMetadataCache.set(filePath, probePromise);
@@ -105136,18 +105264,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
105136
105264
  const metadata = await extractVideoMetadata(videoPath);
105137
105265
  const framePattern = `frame_%05d.${format3}`;
105138
105266
  const outputPattern = join8(videoOutputDir, framePattern);
105139
- const args = [
105140
- "-ss",
105141
- String(startTime),
105142
- "-i",
105143
- videoPath,
105144
- "-t",
105145
- String(duration),
105146
- "-vf",
105147
- `fps=${fps}`,
105148
- "-q:v",
105149
- format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
105150
- ];
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");
105151
105281
  if (format3 === "png") args.push("-compression_level", "6");
105152
105282
  args.push("-y", outputPattern);
105153
105283
  return new Promise((resolve13, reject) => {
@@ -105207,30 +105337,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
105207
105337
  });
105208
105338
  });
105209
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
+ }
105210
105371
  async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
105211
105372
  const startTime = Date.now();
105212
105373
  const extracted = [];
105213
105374
  const errors = [];
105214
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
+ }
105215
105428
  const results = await Promise.all(
105216
- videos.map(async (video) => {
105429
+ resolvedVideos.map(async ({ video, videoPath }) => {
105217
105430
  if (signal?.aborted) {
105218
105431
  throw new Error("Video frame extraction cancelled");
105219
105432
  }
105220
105433
  try {
105221
- let videoPath = video.src;
105222
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
105223
- const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
105224
- videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
105225
- }
105226
- if (isHttpUrl(videoPath)) {
105227
- const downloadDir = join8(options.outputDir, "_downloads");
105228
- mkdirSync5(downloadDir, { recursive: true });
105229
- videoPath = await downloadToTemp(videoPath, downloadDir);
105230
- }
105231
- if (!existsSync8(videoPath)) {
105232
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
105233
- }
105234
105434
  let videoDuration = video.end - video.start;
105235
105435
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
105236
105436
  const metadata = await extractVideoMetadata(videoPath);
@@ -105465,6 +105665,74 @@ function createVideoFrameInjector(frameLookup, config2) {
105465
105665
  }
105466
105666
  };
105467
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
+ }
105468
105736
 
105469
105737
  // ../engine/src/services/audioMixer.ts
105470
105738
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
@@ -105951,6 +106219,292 @@ async function mergeWorkerFrames(workDir, tasks, outputDir) {
105951
106219
  return totalFrames;
105952
106220
  }
105953
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
+
105954
106508
  // src/services/renderOrchestrator.ts
105955
106509
  import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
105956
106510
  import { randomUUID } from "crypto";
@@ -106271,6 +106825,10 @@ var RENDER_MODE_SCRIPT = `(function() {
106271
106825
  }
106272
106826
  waitForPlayer();
106273
106827
  })();`;
106828
+ var HF_EARLY_STUB = `(function() {
106829
+ if (typeof window === "undefined") return;
106830
+ if (!window.__hf) window.__hf = {};
106831
+ })();`;
106274
106832
  var HF_BRIDGE_SCRIPT = `(function() {
106275
106833
  var __realSetInterval =
106276
106834
  window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
@@ -106316,20 +106874,24 @@ var HF_BRIDGE_SCRIPT = `(function() {
106316
106874
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
106317
106875
  return false;
106318
106876
  }
106319
- window.__hf = {
106320
- get duration() {
106877
+ var hf = window.__hf || {};
106878
+ Object.defineProperty(hf, "duration", {
106879
+ configurable: true,
106880
+ enumerable: true,
106881
+ get: function() {
106321
106882
  var d = p.getDuration();
106322
106883
  return d > 0 ? d : getDeclaredDuration();
106323
106884
  },
106324
- seek: function(t) {
106325
- p.renderSeek(t);
106326
- var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
106327
- if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
106328
- window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106329
- }
106330
- seekSameOriginChildFrames(window, nextTimeMs);
106331
- },
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);
106332
106893
  };
106894
+ window.__hf = hf;
106333
106895
  return true;
106334
106896
  }
106335
106897
  if (bridge()) return;
@@ -106411,7 +106973,7 @@ ${headTags}`);
106411
106973
  }
106412
106974
  function createFileServer2(options) {
106413
106975
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
106414
- const preHeadScripts = options.preHeadScripts ?? [];
106976
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
106415
106977
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
106416
106978
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
106417
106979
  const app = new Hono2();
@@ -107671,6 +108233,24 @@ async function safeCleanup(label, fn, log = defaultLogger) {
107671
108233
  });
107672
108234
  }
107673
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
+ }
107674
108254
  var RenderCancelledError = class extends Error {
107675
108255
  reason;
107676
108256
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -108070,7 +108650,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108070
108650
  }
108071
108651
  }
108072
108652
  }
108073
- } 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
+ });
108074
108657
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
108075
108658
  }
108076
108659
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -108094,8 +108677,26 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108094
108677
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
108095
108678
  let frameLookup = null;
108096
108679
  const compiledDir = join15(workDir, "compiled");
108680
+ let extractionResult = null;
108681
+ const nativeHdrVideoIds = /* @__PURE__ */ new Set();
108097
108682
  if (composition.videos.length > 0) {
108098
- const extractionResult = await extractAllVideoFrames(
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
+ }
108698
+ if (composition.videos.length > 0) {
108699
+ extractionResult = await extractAllVideoFrames(
108099
108700
  composition.videos,
108100
108701
  projectDir,
108101
108702
  { fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
@@ -108130,6 +108731,23 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108130
108731
  } else {
108131
108732
  perfStages.videoExtractMs = Date.now() - stage2Start;
108132
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
+ }
108133
108751
  const stage3Start = Date.now();
108134
108752
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
108135
108753
  const audioOutputPath = join15(workDir, "audio.aac");
@@ -108175,218 +108793,398 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108175
108793
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
108176
108794
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
108177
108795
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
108178
- const preset = getEncoderPreset(job.config.quality, outputFormat);
108179
- const effectiveQuality = job.config.crf ?? preset.quality;
108180
- const effectiveBitrate = job.config.videoBitrate;
108181
- const baseEncoderOpts = {
108182
- fps: job.config.fps,
108183
- width,
108184
- height,
108185
- codec: preset.codec,
108186
- preset: preset.preset,
108187
- quality: effectiveQuality,
108188
- bitrate: effectiveBitrate,
108189
- pixelFormat: preset.pixelFormat,
108190
- useGpu: job.config.useGpu
108191
- };
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);
108192
108799
  job.framesRendered = 0;
108193
- let streamingEncoder = null;
108194
- if (enableStreamingEncode) {
108195
- 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(
108196
108825
  videoOnlyPath,
108197
108826
  {
108198
- ...baseEncoderOpts,
108199
- 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"
108200
108836
  },
108201
- abortSignal
108837
+ abortSignal,
108838
+ { ffmpegStreamingTimeout: 36e5 }
108202
108839
  );
108203
108840
  assertNotAborted();
108204
- }
108205
- if (enableStreamingEncode && streamingEncoder) {
108206
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
108207
- const currentEncoder = streamingEncoder;
108208
- if (workerCount > 1) {
108209
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108210
- const onFrameBuffer = async (frameIndex, buffer) => {
108211
- await reorderBuffer.waitForFrame(frameIndex);
108212
- currentEncoder.writeFrame(buffer);
108213
- reorderBuffer.advanceTo(frameIndex + 1);
108214
- };
108215
- await executeParallelCapture(
108216
- fileServer.url,
108217
- workDir,
108218
- tasks,
108219
- captureOptions,
108220
- () => createVideoFrameInjector(frameLookup),
108221
- abortSignal,
108222
- (progress) => {
108223
- job.framesRendered = progress.capturedFrames;
108224
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108225
- const progressPct = 25 + frameProgress * 55;
108226
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
108227
- updateJobStatus(
108228
- job,
108229
- "rendering",
108230
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108231
- Math.round(progressPct),
108232
- 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)
108233
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
+ }
108234
108945
  }
108235
- },
108236
- onFrameBuffer,
108237
- cfg
108238
- );
108239
- if (probeSession) {
108240
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108241
- await closeCaptureSession(probeSession);
108242
- probeSession = null;
108243
- }
108244
- } else {
108245
- const videoInjector = createVideoFrameInjector(frameLookup);
108246
- const session = probeSession ?? await createCaptureSession(
108247
- fileServer.url,
108248
- framesDir,
108249
- captureOptions,
108250
- videoInjector,
108251
- cfg
108252
- );
108253
- if (probeSession) {
108254
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108255
- probeSession = null;
108256
- }
108257
- try {
108258
- if (!session.isInitialized) {
108259
- await initializeSession(session);
108260
108946
  }
108261
- assertNotAborted();
108262
- lastBrowserConsole = session.browserConsoleBuffer;
108263
- for (let i = 0; i < job.totalFrames; i++) {
108264
- assertNotAborted();
108265
- const time = i / job.config.fps;
108266
- const { buffer } = await captureFrameToBuffer(session, i, time);
108267
- await reorderBuffer.waitForFrame(i);
108268
- currentEncoder.writeFrame(buffer);
108269
- reorderBuffer.advanceTo(i + 1);
108270
- job.framesRendered = i + 1;
108947
+ hdrEncoder.writeFrame(canvas);
108948
+ job.framesRendered = i + 1;
108949
+ if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
108271
108950
  const frameProgress = (i + 1) / job.totalFrames;
108272
- const progress = 25 + frameProgress * 55;
108273
108951
  updateJobStatus(
108274
108952
  job,
108275
108953
  "rendering",
108276
- `Streaming frame ${i + 1}/${job.totalFrames}`,
108277
- Math.round(progress),
108954
+ `HDR composite frame ${i + 1}/${job.totalFrames}`,
108955
+ Math.round(25 + frameProgress * 55),
108278
108956
  onProgress
108279
108957
  );
108280
108958
  }
108281
- } finally {
108282
- lastBrowserConsole = session.browserConsoleBuffer;
108283
- await closeCaptureSession(session);
108284
108959
  }
108960
+ } finally {
108961
+ lastBrowserConsole = domSession.browserConsoleBuffer;
108962
+ await closeCaptureSession(domSession);
108285
108963
  }
108286
- const encodeResult = await currentEncoder.close();
108964
+ const hdrEncodeResult = await hdrEncoder.close();
108287
108965
  assertNotAborted();
108288
- if (!encodeResult.success) {
108289
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108966
+ if (!hdrEncodeResult.success) {
108967
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
108290
108968
  }
108291
108969
  perfStages.captureMs = Date.now() - stage4Start;
108292
- perfStages.encodeMs = encodeResult.durationMs;
108970
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
108293
108971
  } else {
108294
- if (workerCount > 1) {
108295
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
108296
- await executeParallelCapture(
108297
- fileServer.url,
108298
- workDir,
108299
- tasks,
108300
- captureOptions,
108301
- () => createVideoFrameInjector(frameLookup),
108302
- abortSignal,
108303
- (progress) => {
108304
- job.framesRendered = progress.capturedFrames;
108305
- const frameProgress = progress.capturedFrames / progress.totalFrames;
108306
- const progressPct = 25 + frameProgress * 45;
108307
- 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;
108308
109060
  updateJobStatus(
108309
109061
  job,
108310
109062
  "rendering",
108311
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
108312
- Math.round(progressPct),
109063
+ `Streaming frame ${i + 1}/${job.totalFrames}`,
109064
+ Math.round(progress),
108313
109065
  onProgress
108314
109066
  );
108315
109067
  }
108316
- },
108317
- void 0,
108318
- cfg
108319
- );
108320
- await mergeWorkerFrames(workDir, tasks, framesDir);
108321
- if (probeSession) {
108322
- lastBrowserConsole = probeSession.browserConsoleBuffer;
108323
- await closeCaptureSession(probeSession);
108324
- probeSession = null;
109068
+ } finally {
109069
+ lastBrowserConsole = session.browserConsoleBuffer;
109070
+ await closeCaptureSession(session);
109071
+ }
108325
109072
  }
108326
- } else {
108327
- const videoInjector = createVideoFrameInjector(frameLookup);
108328
- const session = probeSession ?? await createCaptureSession(
108329
- fileServer.url,
108330
- framesDir,
108331
- captureOptions,
108332
- videoInjector,
108333
- cfg
108334
- );
108335
- if (probeSession) {
108336
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
108337
- probeSession = null;
109073
+ const encodeResult = await currentEncoder.close();
109074
+ assertNotAborted();
109075
+ if (!encodeResult.success) {
109076
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
108338
109077
  }
108339
- try {
108340
- if (!session.isInitialized) {
108341
- 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;
108342
109112
  }
108343
- assertNotAborted();
108344
- lastBrowserConsole = session.browserConsoleBuffer;
108345
- for (let i = 0; i < job.totalFrames; i++) {
108346
- assertNotAborted();
108347
- const time = i / job.config.fps;
108348
- await captureFrame(session, i, time);
108349
- job.framesRendered = i + 1;
108350
- const frameProgress = (i + 1) / job.totalFrames;
108351
- const progress = 25 + frameProgress * 45;
108352
- updateJobStatus(
108353
- job,
108354
- "rendering",
108355
- `Capturing frame ${i + 1}/${job.totalFrames}`,
108356
- Math.round(progress),
108357
- onProgress
108358
- );
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;
108359
109125
  }
108360
- } finally {
108361
- lastBrowserConsole = session.browserConsoleBuffer;
108362
- 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}`);
108363
109185
  }
109186
+ perfStages.encodeMs = Date.now() - stage5Start;
108364
109187
  }
108365
- perfStages.captureMs = Date.now() - stage4Start;
108366
- const stage5Start = Date.now();
108367
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
108368
- const frameExt = needsAlpha ? "png" : "jpg";
108369
- const framePattern = `frame_%06d.${frameExt}`;
108370
- const encoderOpts = baseEncoderOpts;
108371
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
108372
- framesDir,
108373
- framePattern,
108374
- videoOnlyPath,
108375
- encoderOpts,
108376
- chunkedEncodeSize,
108377
- abortSignal
108378
- ) : await encodeFramesFromDir(
108379
- framesDir,
108380
- framePattern,
108381
- videoOnlyPath,
108382
- encoderOpts,
108383
- abortSignal
108384
- );
108385
- assertNotAborted();
108386
- if (!encodeResult.success) {
108387
- throw new Error(`Encoding failed: ${encodeResult.error}`);
108388
- }
108389
- perfStages.encodeMs = Date.now() - stage5Start;
108390
109188
  }
108391
109189
  if (probeSession !== null) {
108392
109190
  const remainingProbeSession = probeSession;