@hyperframes/producer 0.8.198 → 0.8.200

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.
@@ -4,13 +4,13 @@
4
4
  import {
5
5
  existsSync as existsSync19,
6
6
  mkdirSync as mkdirSync10,
7
- statSync as statSync8,
7
+ statSync as statSync9,
8
8
  mkdtempSync as mkdtempSync4,
9
9
  writeFileSync as writeFileSync6,
10
- rmSync as rmSync11,
10
+ rmSync as rmSync12,
11
11
  createReadStream as createReadStream3
12
12
  } from "node:fs";
13
- import { resolve as resolve9, dirname as dirname12, join as join29 } from "node:path";
13
+ import { resolve as resolve9, dirname as dirname12, join as join30 } from "node:path";
14
14
  import { tmpdir as tmpdir3 } from "node:os";
15
15
  import { parseArgs } from "node:util";
16
16
  import crypto from "node:crypto";
@@ -26,8 +26,8 @@ import {
26
26
  mkdirSync as mkdirSync9,
27
27
  readFileSync as readFileSync8,
28
28
  readdirSync as readdirSync5,
29
- rmSync as rmSync10,
30
- statSync as statSync6,
29
+ rmSync as rmSync11,
30
+ statSync as statSync7,
31
31
  writeFileSync as writeFileSync5,
32
32
  copyFileSync as copyFileSync4,
33
33
  appendFileSync
@@ -76,7 +76,7 @@ import {
76
76
  augmentProtocolTimeoutError,
77
77
  augmentPageNavigationTimeoutError
78
78
  } from "@hyperframes/engine";
79
- import { join as join25, dirname as dirname10, resolve as resolve7 } from "path";
79
+ import { join as join26, dirname as dirname10, resolve as resolve7 } from "path";
80
80
  import { totalmem as totalmem2 } from "node:os";
81
81
  import { randomUUID } from "crypto";
82
82
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -1910,8 +1910,8 @@ function replanAfterFailure(plan, failure) {
1910
1910
  }
1911
1911
 
1912
1912
  // src/services/render/stages/captureSegmentedStage.ts
1913
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
1914
- import { join as join9 } from "node:path";
1913
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
1914
+ import { join as join10 } from "node:path";
1915
1915
  import {
1916
1916
  captureFrameToBuffer as captureFrameToBuffer2,
1917
1917
  closeCaptureSession as closeCaptureSession2,
@@ -2114,6 +2114,7 @@ function aggregateDrawElement(perfs, de) {
2114
2114
  blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2115
2115
  blankRecaptures: drain?.blankRecaptures ?? 0,
2116
2116
  boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
2117
+ riskFrames: perfs.reduce((sum, p) => sum + (p.deRiskFrames ?? 0), 0),
2117
2118
  ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
2118
2119
  frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
2119
2120
  };
@@ -2295,7 +2296,7 @@ import {
2295
2296
  } from "@hyperframes/engine";
2296
2297
 
2297
2298
  // src/services/hdrCompositor.ts
2298
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2299
+ import { readSync as readSync2 } from "fs";
2299
2300
  import { join as join6 } from "path";
2300
2301
  import {
2301
2302
  captureAlphaPng,
@@ -2348,10 +2349,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
2348
2349
  }
2349
2350
  function closeHdrVideoFrameSource(source, log) {
2350
2351
  try {
2351
- closeSync3(source.fd);
2352
+ source.windows.close();
2352
2353
  } catch (err) {
2353
- log?.warn("Failed to close HDR raw frame file", {
2354
- rawPath: source.rawPath,
2354
+ log?.warn("Failed to close HDR raw frame windows", {
2355
+ dir: source.dir,
2355
2356
  error: err instanceof Error ? err.message : String(err)
2356
2357
  });
2357
2358
  }
@@ -2361,7 +2362,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
2361
2362
  if (frameIndex < 0 || frameCount < 1) return null;
2362
2363
  return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
2363
2364
  }
2364
- function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2365
+ async function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2365
2366
  const frameSource = hdrVideoFrameSources.get(el.id);
2366
2367
  const startTime = hdrStartTimes.get(el.id);
2367
2368
  if (!frameSource || startTime === void 0 || el.opacity <= 0) {
@@ -2377,13 +2378,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
2377
2378
  if (effectiveIndex === null) return;
2378
2379
  const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
2379
2380
  if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
2380
- const frameOffset = effectiveIndex * frameSource.frameSize;
2381
+ if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2382
+ const lease = await frameSource.windows.lease(effectiveIndex);
2383
+ if (!lease) return;
2384
+ try {
2385
+ blitScratchFrameWithoutYielding(
2386
+ canvas,
2387
+ el,
2388
+ frameSource,
2389
+ lease,
2390
+ width,
2391
+ height,
2392
+ log,
2393
+ sourceTransfer,
2394
+ targetTransfer,
2395
+ hdrPerf
2396
+ );
2397
+ } finally {
2398
+ lease.release();
2399
+ }
2400
+ }
2401
+ function blitScratchFrameWithoutYielding(canvas, el, frameSource, lease, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2381
2402
  try {
2382
- if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2383
2403
  const bytesRead = timeHdrPhase(
2384
2404
  hdrPerf,
2385
2405
  "hdrVideoReadDecodeMs",
2386
- () => readSync2(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset)
2406
+ () => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
2387
2407
  );
2388
2408
  if (bytesRead !== frameSource.frameSize) return;
2389
2409
  const hdrRgb = frameSource.scratch;
@@ -2612,7 +2632,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2612
2632
  hdrPerf
2613
2633
  );
2614
2634
  } else {
2615
- blitHdrVideoLayer(
2635
+ await blitHdrVideoLayer(
2616
2636
  canvas,
2617
2637
  layer.element,
2618
2638
  time,
@@ -2656,7 +2676,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2656
2676
  startTime,
2657
2677
  localTime: localTime.toFixed(3),
2658
2678
  hdrFrameNum: frameNum,
2659
- rawPath: frameSource?.rawPath ?? null,
2679
+ dir: frameSource?.dir ?? null,
2660
2680
  frameCount: frameSource?.frameCount ?? null
2661
2681
  });
2662
2682
  }
@@ -2752,18 +2772,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2752
2772
  }
2753
2773
 
2754
2774
  // src/services/render/stages/captureHdrResources.ts
2755
- import {
2756
- closeSync as closeSync4,
2757
- constants as constants2,
2758
- fstatSync as fstatSync3,
2759
- mkdirSync as mkdirSync2,
2760
- mkdtempSync as mkdtempSync2,
2761
- openSync as openSync3,
2762
- readFileSync as readFileSync3,
2763
- rmSync as rmSync4,
2764
- statfsSync
2765
- } from "node:fs";
2766
- import { join as join7 } from "node:path";
2775
+ import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, rmSync as rmSync5, statSync as statSync2, statfsSync } from "node:fs";
2776
+ import { join as join8 } from "node:path";
2767
2777
  import {
2768
2778
  decodePngToRgb48le,
2769
2779
  extractMediaMetadata as extractMediaMetadata2,
@@ -2777,7 +2787,90 @@ import {
2777
2787
  runFfmpeg
2778
2788
  } from "@hyperframes/engine";
2779
2789
  import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2780
- var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
2790
+
2791
+ // src/services/hdrFrameWindows.ts
2792
+ import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
2793
+ import { join as join7 } from "node:path";
2794
+ function createHdrFrameWindows(opts) {
2795
+ const { dir, frameSize, windowFrames, maxResident, extract } = opts;
2796
+ let totalFrames = opts.totalFrames;
2797
+ const windows = /* @__PURE__ */ new Map();
2798
+ const drop = (index, w) => {
2799
+ windows.delete(index);
2800
+ if (w.fd >= 0) closeSync3(w.fd);
2801
+ rmSync4(w.path, { force: true });
2802
+ };
2803
+ const open = (index) => {
2804
+ const known = windows.get(index);
2805
+ if (known) {
2806
+ windows.delete(index);
2807
+ windows.set(index, known);
2808
+ return known;
2809
+ }
2810
+ for (const [i, w2] of windows) {
2811
+ if (windows.size < maxResident) break;
2812
+ if (w2.pins === 0) drop(i, w2);
2813
+ }
2814
+ const firstFrame = index * windowFrames;
2815
+ const w = {
2816
+ firstFrame,
2817
+ path: join7(dir, `window-${index}.rgb48le`),
2818
+ frames: 0,
2819
+ fd: -1,
2820
+ pins: 0,
2821
+ ready: Promise.resolve()
2822
+ };
2823
+ w.ready = extract({
2824
+ firstFrame,
2825
+ frames: Math.min(windowFrames, totalFrames - firstFrame),
2826
+ path: w.path
2827
+ }).then((produced) => {
2828
+ if (produced < 1) return;
2829
+ w.fd = openSync3(w.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2830
+ w.frames = produced;
2831
+ });
2832
+ windows.set(index, w);
2833
+ return w;
2834
+ };
2835
+ return {
2836
+ get totalFrames() {
2837
+ return totalFrames;
2838
+ },
2839
+ async lease(frameIndex) {
2840
+ for (; ; ) {
2841
+ if (totalFrames < 1) return null;
2842
+ const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
2843
+ const key = Math.floor(index / windowFrames);
2844
+ const w = open(key);
2845
+ w.pins += 1;
2846
+ try {
2847
+ await w.ready;
2848
+ } catch (error) {
2849
+ w.pins -= 1;
2850
+ windows.delete(key);
2851
+ throw error;
2852
+ }
2853
+ const local = index - w.firstFrame;
2854
+ if (local < w.frames) {
2855
+ return {
2856
+ fd: w.fd,
2857
+ offset: local * frameSize,
2858
+ release: () => {
2859
+ w.pins -= 1;
2860
+ }
2861
+ };
2862
+ }
2863
+ w.pins -= 1;
2864
+ totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
2865
+ }
2866
+ },
2867
+ close() {
2868
+ for (const [i, w] of [...windows]) drop(i, w);
2869
+ }
2870
+ };
2871
+ }
2872
+
2873
+ // src/services/render/stages/captureHdrResources.ts
2781
2874
  function tempDirSafePrefix(id) {
2782
2875
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
2783
2876
  return safe || "video";
@@ -2822,10 +2915,16 @@ async function probeHdrExtractionDims(args) {
2822
2915
  const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
2823
2916
  for (const el of stacking) {
2824
2917
  if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
2825
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
2918
+ prep.hdrExtractionDims.set(el.id, {
2919
+ width: el.layoutWidth,
2920
+ height: el.layoutHeight
2921
+ });
2826
2922
  }
2827
2923
  if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
2828
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2924
+ prep.hdrImageFitInfo.set(el.id, {
2925
+ fit: el.objectFit,
2926
+ position: el.objectPosition
2927
+ });
2829
2928
  }
2830
2929
  }
2831
2930
  }
@@ -2844,23 +2943,21 @@ async function probeHdrExtractionDims(args) {
2844
2943
  const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
2845
2944
  for (const el of retryStacking) {
2846
2945
  if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
2847
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
2946
+ prep.hdrExtractionDims.set(el.id, {
2947
+ width: el.layoutWidth,
2948
+ height: el.layoutHeight
2949
+ });
2848
2950
  if (!prep.hdrImageFitInfo.has(el.id)) {
2849
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2951
+ prep.hdrImageFitInfo.set(el.id, {
2952
+ fit: el.objectFit,
2953
+ position: el.objectPosition
2954
+ });
2850
2955
  }
2851
2956
  break;
2852
2957
  }
2853
2958
  }
2854
2959
  }
2855
2960
  }
2856
- function estimateHdrExtractionBytes(videos, fps) {
2857
- let total = 0;
2858
- for (const v of videos) {
2859
- const frames = Math.ceil(Math.max(0, v.durationSeconds) * fps);
2860
- total += frames * v.width * v.height * 6;
2861
- }
2862
- return total;
2863
- }
2864
2961
  var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
2865
2962
  var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
2866
2963
  var HDR_EXTRACTION_WARN_BYTES = 1e10;
@@ -2924,24 +3021,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
2924
3021
  }
2925
3022
  return window2;
2926
3023
  }
2927
- function cleanupHdrFrameDirectory(frameDir, rawPath, log) {
3024
+ function cleanupHdrFrameDirectory(frameDir, log) {
2928
3025
  if (process.env.KEEP_TEMP === "1") return;
2929
3026
  try {
2930
- rmSync4(frameDir, { recursive: true, force: true });
3027
+ rmSync5(frameDir, { recursive: true, force: true });
2931
3028
  } catch (err) {
2932
3029
  log?.warn("Failed to clean up HDR raw frame directory", {
2933
3030
  frameDir,
2934
- rawPath,
2935
3031
  error: err instanceof Error ? err.message : String(err)
2936
3032
  });
2937
3033
  }
2938
3034
  }
2939
3035
  function cleanupHdrVideoFrameSource(source, log) {
2940
3036
  closeHdrVideoFrameSource(source, log);
2941
- cleanupHdrFrameDirectory(source.dir, source.rawPath, log);
3037
+ cleanupHdrFrameDirectory(source.dir, log);
2942
3038
  }
2943
- function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2944
- const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
3039
+ function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
2945
3040
  const configuredBudget = resolveHdrExtractionBudgetBytes(
2946
3041
  process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
2947
3042
  );
@@ -2975,14 +3070,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2975
3070
  budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
2976
3071
  };
2977
3072
  }
3073
+ var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
3074
+ function hdrWindowFrames(frameSize, windowBytes) {
3075
+ return Math.max(8, Math.floor(windowBytes / frameSize));
3076
+ }
3077
+ async function probeFirstTick(srcPath, mediaStart, fps, runFfmpegImpl, abortSignal) {
3078
+ const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
3079
+ const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
3080
+ signal: abortSignal
3081
+ });
3082
+ const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
3083
+ return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
3084
+ }
2978
3085
  async function extractHdrVideoFrames(args) {
2979
3086
  const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
3087
+ const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
3088
+ const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
2980
3089
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
2981
3090
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
2982
3091
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3092
+ const fps = fpsToNumber4(job.config.fps);
2983
3093
  const out = /* @__PURE__ */ new Map();
2984
3094
  mkdirSync2(framesDir, { recursive: true });
2985
- const plannedVideos = [];
3095
+ let scratchBytes = 0;
2986
3096
  const extractionWindows = /* @__PURE__ */ new Map();
2987
3097
  for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
2988
3098
  const video = composition.videos.find((v) => v.id === videoId);
@@ -3003,16 +3113,13 @@ async function extractHdrVideoFrames(args) {
3003
3113
  videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
3004
3114
  });
3005
3115
  prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
3006
- plannedVideos.push({
3007
- durationSeconds: window2.durationSeconds,
3008
- width: dims.width,
3009
- height: dims.height
3010
- });
3116
+ const frameSize = dims.width * dims.height * 6;
3117
+ const frames = window2.finalFrameOnly ? 1 : Math.ceil(window2.durationSeconds * fps);
3118
+ scratchBytes += Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
3011
3119
  }
3012
3120
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
3013
3121
  framesDir,
3014
- plannedVideos,
3015
- fpsToNumber4(job.config.fps),
3122
+ scratchBytes,
3016
3123
  log
3017
3124
  );
3018
3125
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3024,79 +3131,79 @@ async function extractHdrVideoFrames(args) {
3024
3131
  if (!video || !planned) continue;
3025
3132
  const { window: window2 } = planned;
3026
3133
  mkdirSync2(framesDir, { recursive: true });
3027
- const frameDir = mkdtempSync2(join7(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3134
+ const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3028
3135
  createdFrameDirs.add(frameDir);
3029
3136
  const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
3030
- const rawPath = join7(frameDir, "frames.rgb48le");
3031
- const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3032
- const extractionStart = String(extractionMediaStart);
3033
- const ffmpegArgs = [];
3034
- if (window2.finalFrameOnly) {
3035
- ffmpegArgs.push("-i", srcPath, "-ss", extractionStart, "-frames:v", "1");
3036
- } else {
3037
- ffmpegArgs.push(
3038
- "-ss",
3039
- extractionStart,
3040
- "-i",
3041
- srcPath,
3042
- "-t",
3043
- String(window2.durationSeconds)
3044
- );
3045
- }
3046
- if (!window2.finalFrameOnly) {
3047
- ffmpegArgs.push("-r", fpsToFfmpegArg(job.config.fps));
3048
- }
3049
- ffmpegArgs.push(
3050
- "-vf",
3051
- `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
3052
- "-pix_fmt",
3053
- "rgb48le",
3054
- "-f",
3055
- "rawvideo",
3056
- "-y",
3057
- rawPath
3058
- );
3059
- const result = await runFfmpegImpl(ffmpegArgs, { signal: abortSignal });
3060
- if (!result.success) {
3061
- hdrDiagnostics.videoExtractionFailures += 1;
3062
- log.error("HDR frame pre-extraction failed; aborting render", {
3063
- videoId,
3064
- srcPath,
3065
- stderr: result.stderr.slice(-400)
3066
- });
3067
- throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3068
- error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3069
- failureReason: result.failureReason
3070
- });
3071
- }
3072
3137
  const frameSize = dims.width * dims.height * 6;
3073
- const fd = openSync3(rawPath, constants2.O_RDONLY | NO_FOLLOW_FLAG);
3074
- let handedOff = false;
3075
- try {
3076
- const frameCount = Math.floor(fstatSync3(fd).size / frameSize);
3077
- if (frameCount < 1) {
3078
- hdrDiagnostics.videoExtractionFailures += 1;
3079
- throw new Error(
3080
- `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
3138
+ const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3139
+ const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
3140
+ const firstTick = window2.finalFrameOnly ? 0 : await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
3141
+ const totalFrames = window2.finalFrameOnly ? 1 : Math.max(1, Math.ceil(window2.durationSeconds * fps - firstTick - 1e-9)) + Number(video.loop);
3142
+ const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
3143
+ const windows = createHdrFrameWindows({
3144
+ dir: frameDir,
3145
+ frameSize,
3146
+ totalFrames,
3147
+ windowFrames,
3148
+ maxResident,
3149
+ extract: async (spec) => {
3150
+ const ffmpegArgs = window2.finalFrameOnly ? (
3151
+ // Decode before seeking for the one-frame path. Input-side seeking can
3152
+ // return zero frames for valid unindexed/negative-base transports.
3153
+ ["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
3154
+ ) : hdrWindowFfmpegArgs({
3155
+ srcPath,
3156
+ mediaStart: extractionMediaStart,
3157
+ fps,
3158
+ fpsArg: fpsToFfmpegArg(job.config.fps),
3159
+ firstTick,
3160
+ firstFrame: spec.firstFrame,
3161
+ frames: spec.frames,
3162
+ scale
3163
+ });
3164
+ const result = await runFfmpegImpl(
3165
+ [...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
3166
+ { signal: abortSignal }
3081
3167
  );
3168
+ if (!result.success) {
3169
+ hdrDiagnostics.videoExtractionFailures += 1;
3170
+ log.error("HDR frame extraction failed; aborting render", {
3171
+ videoId,
3172
+ srcPath,
3173
+ firstFrame: spec.firstFrame,
3174
+ stderr: result.stderr.slice(-400)
3175
+ });
3176
+ throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3177
+ error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3178
+ failureReason: result.failureReason
3179
+ });
3180
+ }
3181
+ return Math.min(Math.floor(statSync2(spec.path).size / frameSize), spec.frames);
3082
3182
  }
3083
- out.set(videoId, {
3084
- dir: frameDir,
3085
- rawPath,
3086
- fd,
3087
- width: dims.width,
3088
- height: dims.height,
3089
- frameSize,
3090
- frameCount,
3091
- scratch: Buffer.allocUnsafe(frameSize),
3092
- loop: video.loop,
3093
- mediaStart: extractionMediaStart,
3094
- videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3095
- });
3096
- handedOff = true;
3097
- } finally {
3098
- if (!handedOff) closeSync4(fd);
3183
+ });
3184
+ const first = await windows.lease(0);
3185
+ if (!first) {
3186
+ hdrDiagnostics.videoExtractionFailures += 1;
3187
+ throw new Error(
3188
+ `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
3189
+ );
3099
3190
  }
3191
+ first.release();
3192
+ if (video.loop) (await windows.lease(totalFrames - 1))?.release();
3193
+ out.set(videoId, {
3194
+ dir: frameDir,
3195
+ windows,
3196
+ width: dims.width,
3197
+ height: dims.height,
3198
+ frameSize,
3199
+ get frameCount() {
3200
+ return windows.totalFrames;
3201
+ },
3202
+ scratch: Buffer.allocUnsafe(frameSize),
3203
+ loop: video.loop,
3204
+ mediaStart: extractionMediaStart,
3205
+ videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3206
+ });
3100
3207
  }
3101
3208
  return { sources: out, estimatedBytes, releaseReservation };
3102
3209
  } catch (error) {
@@ -3105,12 +3212,23 @@ async function extractHdrVideoFrames(args) {
3105
3212
  createdFrameDirs.delete(source.dir);
3106
3213
  }
3107
3214
  for (const frameDir of createdFrameDirs) {
3108
- cleanupHdrFrameDirectory(frameDir, void 0, log);
3215
+ cleanupHdrFrameDirectory(frameDir, log);
3109
3216
  }
3110
3217
  releaseReservation();
3111
3218
  throw error;
3112
3219
  }
3113
3220
  }
3221
+ function hdrWindowFfmpegArgs(a) {
3222
+ const unit = Math.ceil(a.fps);
3223
+ const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
3224
+ const trim = a.firstFrame - seekFrame;
3225
+ const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
3226
+ if (trim > 0) filters.push(`trim=start_frame=${trim}`);
3227
+ return [
3228
+ ...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
3229
+ ...["-vf", filters.join(","), "-frames:v", String(a.frames)]
3230
+ ];
3231
+ }
3114
3232
  function decodeHdrImageBuffers(args) {
3115
3233
  const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
3116
3234
  const out = /* @__PURE__ */ new Map();
@@ -3130,7 +3248,11 @@ function decodeHdrImageBuffers(args) {
3130
3248
  fit,
3131
3249
  fitInfo?.position
3132
3250
  );
3133
- out.set(imageId, { data: resampled, width: layout.width, height: layout.height });
3251
+ out.set(imageId, {
3252
+ data: resampled,
3253
+ width: layout.width,
3254
+ height: layout.height
3255
+ });
3134
3256
  } else {
3135
3257
  out.set(imageId, {
3136
3258
  data: Buffer.from(decoded.data),
@@ -3242,7 +3364,7 @@ async function captureSceneIntoBuffer(a) {
3242
3364
  hdrPerf
3243
3365
  );
3244
3366
  } else {
3245
- blitHdrVideoLayer(
3367
+ await blitHdrVideoLayer(
3246
3368
  sceneBuf,
3247
3369
  el,
3248
3370
  time,
@@ -3444,7 +3566,7 @@ function isTargetLossError(error) {
3444
3566
  // src/services/render/stages/captureStreamingStage.ts
3445
3567
  import { mkdtemp, writeFile } from "node:fs/promises";
3446
3568
  import { tmpdir } from "node:os";
3447
- import { join as join8 } from "node:path";
3569
+ import { join as join9 } from "node:path";
3448
3570
  import {
3449
3571
  DrawElementVerificationError,
3450
3572
  captureFrameToBuffer,
@@ -3606,11 +3728,11 @@ function createDrainFrameGuard(args) {
3606
3728
  return buf;
3607
3729
  }
3608
3730
  if (db < verifyMinDb) {
3609
- const dumpDir = await mkdtemp(join8(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3731
+ const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3610
3732
  if (dumpDir) {
3611
3733
  await Promise.all([
3612
- writeFile(join8(dumpDir, `frame-${idx}-de.jpg`), buf),
3613
- writeFile(join8(dumpDir, `frame-${idx}-truth.jpg`), truth)
3734
+ writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3735
+ writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3614
3736
  ]).catch(() => {
3615
3737
  });
3616
3738
  }
@@ -3629,11 +3751,21 @@ function createDrainFrameGuard(args) {
3629
3751
  return buf;
3630
3752
  };
3631
3753
  }
3632
- async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
3754
+ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
3633
3755
  let prev = null;
3634
3756
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
3635
3757
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3636
- const guardFrame = (idx, buf) => guard(session, idx, buf);
3758
+ const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3759
+ const captureRisk = async (idx) => {
3760
+ await risk.open();
3761
+ lastProgressAt = Date.now();
3762
+ return raceAgainstStall(
3763
+ risk.capture(idx, frameTime(idx)),
3764
+ stallTimeoutMs - (Date.now() - lastProgressAt),
3765
+ { session, frameIndex: idx, totalFrames, stallTimeoutMs },
3766
+ abortSignal
3767
+ );
3768
+ };
3637
3769
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3638
3770
  let lastProgressAt = Date.now();
3639
3771
  const captureWithStallGuard = (idx, promise) => raceAgainstStall(
@@ -3645,7 +3777,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3645
3777
  const drainPrev = async () => {
3646
3778
  if (!prev) return;
3647
3779
  assertNotAborted();
3648
- const buf = await guardFrame(prev.idx, await prev.encodeResult);
3780
+ const buf = await guardFrame(prev, await prev.encodeResult);
3649
3781
  await reorderBuffer.waitForFrame(prev.idx);
3650
3782
  ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
3651
3783
  reorderBuffer.advanceTo(prev.idx + 1);
@@ -3665,7 +3797,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3665
3797
  const drainBatch = async (batch) => {
3666
3798
  for (const item of batch) {
3667
3799
  assertNotAborted();
3668
- const buf = await guardFrame(item.idx, await item.encodeResult);
3800
+ const buf = await guardFrame(item, await item.encodeResult);
3669
3801
  await reorderBuffer.waitForFrame(item.idx);
3670
3802
  ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
3671
3803
  reorderBuffer.advanceTo(item.idx + 1);
@@ -3683,7 +3815,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3683
3815
  };
3684
3816
  if (batchN > 1 && !session.onBeforeCapture) {
3685
3817
  const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
3686
- const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
3818
+ const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
3687
3819
  let prevBatch = [];
3688
3820
  let i = 0;
3689
3821
  while (i < totalFrames) {
@@ -3700,6 +3832,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3700
3832
  );
3701
3833
  await drainBatch(prevBatch);
3702
3834
  prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
3835
+ } else if (risk?.frames.has(i)) {
3836
+ const buffer = await captureRisk(i);
3837
+ await drainBatch(prevBatch);
3838
+ prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
3839
+ i++;
3703
3840
  } else {
3704
3841
  const { encodeResult } = await captureWithStallGuard(
3705
3842
  i,
@@ -3715,6 +3852,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3715
3852
  }
3716
3853
  for (let i = 0; i < totalFrames; i++) {
3717
3854
  assertNotAborted();
3855
+ if (risk?.frames.has(i)) {
3856
+ const buffer = await captureRisk(i);
3857
+ await drainPrev();
3858
+ prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
3859
+ continue;
3860
+ }
3718
3861
  const time = frameTime(i);
3719
3862
  const { encodeResult } = await captureWithStallGuard(
3720
3863
  i,
@@ -3961,6 +4104,24 @@ async function runCaptureStreamingStage(input) {
3961
4104
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
3962
4105
  probeSession = null;
3963
4106
  }
4107
+ let riskSession = null;
4108
+ const openRiskSession = async () => {
4109
+ const opened = await createCaptureSession(
4110
+ fileServer.url,
4111
+ framesDir,
4112
+ buildCaptureOptions(),
4113
+ createRenderVideoFrameInjector(),
4114
+ {
4115
+ ...captureCfg,
4116
+ useDrawElement: false,
4117
+ staticFrameDedup: false,
4118
+ enableBrowserPool: false
4119
+ }
4120
+ );
4121
+ riskSession = opened;
4122
+ await initializeSession(opened);
4123
+ return opened;
4124
+ };
3964
4125
  try {
3965
4126
  if (!session.isInitialized) {
3966
4127
  await initializeSession(session);
@@ -3975,6 +4136,7 @@ async function runCaptureStreamingStage(input) {
3975
4136
  blankDeterministicAccepts: 0,
3976
4137
  blankRecaptures: 0
3977
4138
  };
4139
+ const riskFrames = session.deRiskFrames;
3978
4140
  await runWorkerEncodePipelineLoop(
3979
4141
  session,
3980
4142
  totalFrames,
@@ -3985,7 +4147,16 @@ async function runCaptureStreamingStage(input) {
3985
4147
  onProgress,
3986
4148
  log,
3987
4149
  deDrainStats,
3988
- abortSignal
4150
+ abortSignal,
4151
+ riskFrames && riskFrames.size > 0 ? {
4152
+ frames: riskFrames,
4153
+ // Opened on the first risk frame, in a browser of its own: pages sharing a
4154
+ // browser share a compositor, which starves the drawElement page's paint events.
4155
+ open: async () => {
4156
+ riskSession ??= await openRiskSession();
4157
+ },
4158
+ capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
4159
+ } : void 0
3989
4160
  );
3990
4161
  } else {
3991
4162
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
@@ -4023,6 +4194,7 @@ async function runCaptureStreamingStage(input) {
4023
4194
  } finally {
4024
4195
  lastBrowserConsole = session.browserConsoleBuffer;
4025
4196
  await closeCaptureSession(session);
4197
+ if (riskSession) await closeCaptureSession(riskSession);
4026
4198
  }
4027
4199
  }
4028
4200
  const encodeFrom = job.progress;
@@ -4059,7 +4231,7 @@ async function runCaptureStreamingStage(input) {
4059
4231
 
4060
4232
  // src/services/render/stages/captureSegmentedStage.ts
4061
4233
  function segmentOutputPath(segmentDir, index) {
4062
- return join9(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4234
+ return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4063
4235
  }
4064
4236
  function segmentEncoderOptions(base, segment) {
4065
4237
  return {
@@ -4135,7 +4307,7 @@ async function captureSegment(ctx, segment) {
4135
4307
  path: segmentPath,
4136
4308
  // Recorded so resume can prove the file on disk is the one that
4137
4309
  // finished; absent only in unit tests, whose fake encoder writes none.
4138
- bytes: existsSync6(segmentPath) ? statSync2(segmentPath).size : 0
4310
+ bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4139
4311
  });
4140
4312
  return encodeResult.durationMs;
4141
4313
  } finally {
@@ -4153,7 +4325,7 @@ var DEFAULT_DEPS = {
4153
4325
  // is invisible to a test that swaps the engine module after this file has
4154
4326
  // been evaluated (bun updates live bindings, not copies).
4155
4327
  closeSession: (session) => closeCaptureSession2(session),
4156
- removeFile: (path) => rmSync5(path, { force: true })
4328
+ removeFile: (path) => rmSync6(path, { force: true })
4157
4329
  };
4158
4330
  function defaultSessionFactory(input) {
4159
4331
  let probeSession = input.probeSession;
@@ -4182,7 +4354,7 @@ function openSegmentedSessionReuse(input, session) {
4182
4354
  async function openSegmentedSession(input) {
4183
4355
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4184
4356
  const videoInjector = input.createRenderVideoFrameInjector();
4185
- const captureOptions = input.buildCaptureOptions();
4357
+ const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4186
4358
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4187
4359
  parallel: (input.workerCount ?? 1) > 1
4188
4360
  });
@@ -4320,7 +4492,7 @@ async function runCaptureSegmentedStage(input) {
4320
4492
  const { log, dedupPerfs, assertNotAborted } = input;
4321
4493
  const deps = { ...DEFAULT_DEPS, ...input.deps };
4322
4494
  const segments = planSegments(input.totalFrames, input.segmentFrames);
4323
- const segmentDir = input.segmentDir ?? join9(input.workDir, "segments");
4495
+ const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
4324
4496
  mkdirSync3(segmentDir, { recursive: true });
4325
4497
  const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
4326
4498
  const pending = segments.filter((s) => !skip.has(s.index));
@@ -4391,10 +4563,83 @@ async function runCaptureSegmentedStage(input) {
4391
4563
  };
4392
4564
  }
4393
4565
 
4566
+ // src/services/render/renderEtaGuard.ts
4567
+ var RENDER_TOO_SLOW = "RENDER_TOO_SLOW";
4568
+ var ETA_MARGIN = 1.2;
4569
+ var MIN_SAMPLE_MS = 6e4;
4570
+ var MIN_SAMPLE_FRAMES = 3;
4571
+ var POLL_MS = 5e3;
4572
+ var RenderTooSlowError = class extends Error {
4573
+ code = RENDER_TOO_SLOW;
4574
+ // The producer server reads these off the error for the wire envelope.
4575
+ // fallow-ignore-next-line unused-class-member
4576
+ owner = "user";
4577
+ // fallow-ignore-next-line unused-class-member
4578
+ retryable = false;
4579
+ publicMetadata;
4580
+ constructor(stage, input) {
4581
+ const { framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs } = input;
4582
+ super(
4583
+ `render too slow: stage ${stage}, ${framesCompleted}/${totalFrames} frames, ${Math.round(msPerFrame)} ms/frame, ETA ${etaMs} ms exceeds maxRenderMs ${maxRenderMs}`
4584
+ );
4585
+ this.name = "RenderTooSlowError";
4586
+ this.publicMetadata = { stage, framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs };
4587
+ }
4588
+ };
4589
+ function checkRenderEta(stage, input) {
4590
+ const { maxRenderMs, elapsedMs, framesCompleted, totalFrames, msPerFrame } = input;
4591
+ if (!maxRenderMs || maxRenderMs <= 0 || !(msPerFrame > 0)) return void 0;
4592
+ const etaMs = Math.round(elapsedMs + (totalFrames - framesCompleted) * msPerFrame);
4593
+ if (etaMs <= maxRenderMs * ETA_MARGIN) return void 0;
4594
+ return new RenderTooSlowError(stage, { ...input, maxRenderMs, etaMs });
4595
+ }
4596
+ function createRateSampler() {
4597
+ let base;
4598
+ let lastFrames = 0;
4599
+ return {
4600
+ observe(done, at) {
4601
+ if (done === 0 || done < lastFrames) base = void 0;
4602
+ lastFrames = done;
4603
+ if (done === 0) return void 0;
4604
+ base ??= { at, frames: done };
4605
+ const sampleMs = at - base.at;
4606
+ const sampleFrames = done - base.frames;
4607
+ if (sampleMs < MIN_SAMPLE_MS || sampleFrames < MIN_SAMPLE_FRAMES) return void 0;
4608
+ return sampleMs / sampleFrames;
4609
+ }
4610
+ };
4611
+ }
4612
+ function startRenderEtaGuard(options) {
4613
+ const { maxRenderMs, job, totalFrames, pipelineStartMs, onTooSlow, now = Date.now } = options;
4614
+ if (!maxRenderMs || maxRenderMs <= 0) return () => {
4615
+ };
4616
+ const sampler = createRateSampler();
4617
+ const stop = () => clearInterval(timer);
4618
+ const timer = setInterval(() => {
4619
+ const done = job.framesRendered ?? 0;
4620
+ if (done >= totalFrames) return stop();
4621
+ const at = now();
4622
+ const msPerFrame = sampler.observe(done, at);
4623
+ if (msPerFrame === void 0) return;
4624
+ const error = checkRenderEta("capture", {
4625
+ maxRenderMs,
4626
+ elapsedMs: at - pipelineStartMs,
4627
+ framesCompleted: done,
4628
+ totalFrames,
4629
+ msPerFrame
4630
+ });
4631
+ if (!error) return;
4632
+ stop();
4633
+ onTooSlow(error);
4634
+ }, options.pollMs ?? POLL_MS);
4635
+ timer.unref?.();
4636
+ return stop;
4637
+ }
4638
+
4394
4639
  // src/services/render/segmentManifest.ts
4395
4640
  import { createHash as createHash2 } from "node:crypto";
4396
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4397
- import { join as join10 } from "node:path";
4641
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
4642
+ import { join as join11 } from "node:path";
4398
4643
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4399
4644
  var MANIFEST_FILENAME = "segments.json";
4400
4645
  function computeSegmentPlanHash(input) {
@@ -4420,7 +4665,7 @@ function computeSegmentPlanHash(input) {
4420
4665
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4421
4666
  }
4422
4667
  function segmentDirFor(projectRendersDir, planHash) {
4423
- return join10(projectRendersDir, ".hf-segments", planHash);
4668
+ return join11(projectRendersDir, ".hf-segments", planHash);
4424
4669
  }
4425
4670
  function hasNumber(value, key) {
4426
4671
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -4438,7 +4683,7 @@ function isManifest(value) {
4438
4683
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
4439
4684
  }
4440
4685
  function readSegmentManifest(segmentDir) {
4441
- const path = join10(segmentDir, MANIFEST_FILENAME);
4686
+ const path = join11(segmentDir, MANIFEST_FILENAME);
4442
4687
  if (!existsSync7(path)) return null;
4443
4688
  try {
4444
4689
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -4449,9 +4694,9 @@ function readSegmentManifest(segmentDir) {
4449
4694
  }
4450
4695
  function writeSegmentManifest(segmentDir, manifest) {
4451
4696
  mkdirSync4(segmentDir, { recursive: true });
4452
- const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4697
+ const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4453
4698
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4454
- renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4699
+ renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
4455
4700
  }
4456
4701
  async function probeSegmentFrameCount(path, probe) {
4457
4702
  try {
@@ -4469,7 +4714,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
4469
4714
  if (manifest.planHash !== expectedHash) return ok;
4470
4715
  for (const entry of manifest.completed) {
4471
4716
  if (!existsSync7(entry.path)) continue;
4472
- const size = statSync3(entry.path).size;
4717
+ const size = statSync4(entry.path).size;
4473
4718
  if (size <= 0 || size !== entry.bytes) continue;
4474
4719
  const frames = await probeFrames(entry.path);
4475
4720
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -4563,7 +4808,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4563
4808
  }
4564
4809
 
4565
4810
  // src/services/render/captureCost.ts
4566
- import { join as join11 } from "node:path";
4811
+ import { join as join12 } from "node:path";
4567
4812
  import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4568
4813
  import {
4569
4814
  calculateOptimalWorkers,
@@ -4808,7 +5053,7 @@ async function runCaptureCalibration(input) {
4808
5053
  });
4809
5054
  let calibration;
4810
5055
  try {
4811
- calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
5056
+ calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
4812
5057
  } catch (error) {
4813
5058
  const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
4814
5059
  if (!shouldFallback) {
@@ -4841,7 +5086,7 @@ async function runCaptureCalibration(input) {
4841
5086
  const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
4842
5087
  try {
4843
5088
  calibration = await runOneCalibration(
4844
- join11(workDir, "capture-calibration-screenshot"),
5089
+ join12(workDir, "capture-calibration-screenshot"),
4845
5090
  screenshotCfg
4846
5091
  );
4847
5092
  } catch (fallbackError) {
@@ -5273,14 +5518,14 @@ function countAuthoredTimedClips(html) {
5273
5518
  }
5274
5519
 
5275
5520
  // src/services/render/stages/compileStage.ts
5276
- import { join as join14 } from "node:path";
5521
+ import { join as join15 } from "node:path";
5277
5522
  import { suggestMatchingPreset } from "@hyperframes/core";
5278
5523
 
5279
5524
  // src/services/htmlCompiler.ts
5280
5525
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5281
5526
  import { createHash as createHash5 } from "node:crypto";
5282
5527
  import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
5283
- import { join as join13, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5528
+ import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5284
5529
  import { parseHTML as parseHTML4 } from "linkedom";
5285
5530
  import {
5286
5531
  COMPOSITION_SOURCE_URL,
@@ -5507,8 +5752,8 @@ import {
5507
5752
 
5508
5753
  // src/services/animatedGifPrep.ts
5509
5754
  import { createHash as createHash4 } from "node:crypto";
5510
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
5511
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join12, resolve as resolve5 } from "node:path";
5755
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
5756
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
5512
5757
  import { parseHTML as parseHTML3 } from "linkedom";
5513
5758
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5514
5759
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -5557,7 +5802,7 @@ function resolveGifSourcePath(src, options) {
5557
5802
  }
5558
5803
  function isUsableFile(path) {
5559
5804
  try {
5560
- const stat = statSync4(path);
5805
+ const stat = statSync5(path);
5561
5806
  return stat.isFile() && stat.size > 0;
5562
5807
  } catch {
5563
5808
  return false;
@@ -5692,10 +5937,10 @@ async function ensurePreparedWebm(input) {
5692
5937
  if (!isUsableFile(input.cachePath)) {
5693
5938
  renameWithRetry3(tmpPath, input.cachePath);
5694
5939
  } else {
5695
- rmSync6(tmpPath, { force: true });
5940
+ rmSync7(tmpPath, { force: true });
5696
5941
  }
5697
5942
  } catch (error) {
5698
- rmSync6(tmpPath, { force: true });
5943
+ rmSync7(tmpPath, { force: true });
5699
5944
  throw error;
5700
5945
  }
5701
5946
  }
@@ -5751,7 +5996,7 @@ function replaceImageWithVideo(input) {
5751
5996
  return video;
5752
5997
  }
5753
5998
  async function prepareAnimatedGifInputs(html, options) {
5754
- const outputDir = options.outputDir ?? join12(options.downloadDir, PREPARED_GIF_SUBDIR);
5999
+ const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
5755
6000
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
5756
6001
  const cacheDir = options.cacheDir ?? outputDir;
5757
6002
  const { document: document2 } = parseHTML3(html);
@@ -5773,8 +6018,8 @@ async function prepareAnimatedGifInputs(html, options) {
5773
6018
  const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
5774
6019
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
5775
6020
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
5776
- const cachePath = join12(cacheDir, filename);
5777
- const outputPath = join12(outputDir, filename);
6021
+ const cachePath = join13(cacheDir, filename);
6022
+ const outputPath = join13(outputDir, filename);
5778
6023
  const outputSrc = `${outputSrcPrefix}/${filename}`;
5779
6024
  await ensurePreparedWebm({
5780
6025
  sourcePath,
@@ -6125,10 +6370,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
6125
6370
  function detectThreeDTransformUsage(html) {
6126
6371
  return THREE_D_CONTEXT_PATTERN.test(html);
6127
6372
  }
6128
- var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
6129
- function detectMixBlendModeUsage(html) {
6130
- return MIX_BLEND_MODE_PATTERN.test(html);
6131
- }
6132
6373
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6133
6374
  function detectAncestorBackgroundImage(html) {
6134
6375
  const { document: document2 } = parseHTML4(html);
@@ -6188,7 +6429,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6188
6429
  }
6189
6430
  } else {
6190
6431
  filePath = decodedUrlPath(src);
6191
- if (!filePath.startsWith("/")) filePath = join13(baseDir, filePath);
6432
+ if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6192
6433
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6193
6434
  }
6194
6435
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6808,7 +7049,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
6808
7049
  return downloadAndRewriteUrls(
6809
7050
  urlSet,
6810
7051
  html,
6811
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7052
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6812
7053
  "Remote media download failed",
6813
7054
  "Localized remote media source(s)"
6814
7055
  );
@@ -6823,7 +7064,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
6823
7064
  return downloadAndRewriteUrls(
6824
7065
  urlSet,
6825
7066
  html,
6826
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7067
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6827
7068
  "Remote image download failed",
6828
7069
  "Localized remote image source(s)"
6829
7070
  );
@@ -6839,7 +7080,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
6839
7080
  return downloadAndRewriteUrls(
6840
7081
  urlSet,
6841
7082
  html,
6842
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7083
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6843
7084
  "Remote background-image download failed",
6844
7085
  "Localized remote background-image(s)",
6845
7086
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -6974,7 +7215,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
6974
7215
  return downloadAndRewriteUrls(
6975
7216
  urlSet,
6976
7217
  processed,
6977
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7218
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6978
7219
  "Remote font download failed",
6979
7220
  "Localized remote font face(s)",
6980
7221
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7141,7 +7382,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7141
7382
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
7142
7383
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
7143
7384
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
7144
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
7145
7385
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
7146
7386
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
7147
7387
  injectTextRenderingRule(
@@ -7255,7 +7495,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7255
7495
  renderModeHints,
7256
7496
  hasShaderTransitions,
7257
7497
  usesThreeDTransforms,
7258
- usesMixBlendMode,
7259
7498
  hasAncestorBackgroundImage
7260
7499
  };
7261
7500
  }
@@ -7641,7 +7880,7 @@ async function runCompileStage(input) {
7641
7880
  abortSignal
7642
7881
  } = input;
7643
7882
  const compileStart = Date.now();
7644
- const compiled = await compileForRender(projectDir, htmlPath, join14(workDir, "downloads"), {
7883
+ const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
7645
7884
  log,
7646
7885
  failClosedFontFetch: failClosedFontFetch === true,
7647
7886
  allowSystemFontCapture,
@@ -7649,7 +7888,7 @@ async function runCompileStage(input) {
7649
7888
  variables: input.variables,
7650
7889
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7651
7890
  pageMotionBlur: input.pageMotionBlur,
7652
- animatedGifCacheDir: cfg.extractCacheDir ? join14(cfg.extractCacheDir, "animated-gif") : void 0,
7891
+ animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
7653
7892
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7654
7893
  });
7655
7894
  assertNotAborted();
@@ -7662,13 +7901,6 @@ async function runCompileStage(input) {
7662
7901
  "[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7663
7902
  );
7664
7903
  }
7665
- if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
7666
- cfg.useDrawElement = false;
7667
- deCompileGate = "mix_blend_mode";
7668
- log.info(
7669
- "[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7670
- );
7671
- }
7672
7904
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
7673
7905
  cfg.useDrawElement = false;
7674
7906
  deCompileGate = "ancestor_background_image";
@@ -7712,7 +7944,7 @@ async function runCompileStage(input) {
7712
7944
  };
7713
7945
  await preflightCompositionAssetMediaTypes({
7714
7946
  projectDir,
7715
- compiledDir: join14(workDir, "compiled"),
7947
+ compiledDir: join15(workDir, "compiled"),
7716
7948
  composition,
7717
7949
  signal: abortSignal
7718
7950
  });
@@ -7757,7 +7989,7 @@ async function runCompileStage(input) {
7757
7989
  }
7758
7990
 
7759
7991
  // src/services/render/stages/probeStage.ts
7760
- import { join as join15 } from "node:path";
7992
+ import { join as join16 } from "node:path";
7761
7993
  import { parseHTML as parseHTML5 } from "linkedom";
7762
7994
  import {
7763
7995
  closeCaptureSession as closeCaptureSession4,
@@ -7949,7 +8181,7 @@ async function runProbeStage(input) {
7949
8181
  });
7950
8182
  fileServer = await createFileServer({
7951
8183
  projectDir,
7952
- compiledDir: join15(workDir, "compiled"),
8184
+ compiledDir: join16(workDir, "compiled"),
7953
8185
  port: 0,
7954
8186
  preHeadScripts: [VIRTUAL_TIME_SHIM],
7955
8187
  fps: job.config.fps
@@ -7977,7 +8209,7 @@ async function runProbeStage(input) {
7977
8209
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
7978
8210
  probeSession = await createCaptureSession4(
7979
8211
  fileServer.url,
7980
- join15(workDir, "probe"),
8212
+ join16(workDir, "probe"),
7981
8213
  captureOpts,
7982
8214
  null,
7983
8215
  probeCfg
@@ -8062,7 +8294,7 @@ async function runProbeStage(input) {
8062
8294
  });
8063
8295
  probeSession = await createCaptureSession4(
8064
8296
  fileServer.url,
8065
- join15(workDir, "probe-screenshot"),
8297
+ join16(workDir, "probe-screenshot"),
8066
8298
  captureOpts,
8067
8299
  null,
8068
8300
  { ...probeCfg, forceScreenshot: true }
@@ -8098,7 +8330,7 @@ async function runProbeStage(input) {
8098
8330
  compiled,
8099
8331
  resolutions,
8100
8332
  projectDir,
8101
- join15(workDir, "downloads")
8333
+ join16(workDir, "downloads")
8102
8334
  );
8103
8335
  assertNotAborted();
8104
8336
  composition.videos = compiled.videos;
@@ -8289,7 +8521,7 @@ async function runProbeStage(input) {
8289
8521
  }
8290
8522
  await preflightCompositionAssetMediaTypes({
8291
8523
  projectDir,
8292
- compiledDir: join15(workDir, "compiled"),
8524
+ compiledDir: join16(workDir, "compiled"),
8293
8525
  composition,
8294
8526
  signal: abortSignal
8295
8527
  });
@@ -8423,7 +8655,7 @@ function validateRenderDuration(input) {
8423
8655
 
8424
8656
  // src/services/render/stages/extractVideosStage.ts
8425
8657
  import { existsSync as existsSync11 } from "node:fs";
8426
- import { join as join16 } from "node:path";
8658
+ import { join as join17 } from "node:path";
8427
8659
  import {
8428
8660
  classifyVideoExtractionError,
8429
8661
  createFrameLookupTable,
@@ -8738,14 +8970,14 @@ async function runExtractVideosStage(input) {
8738
8970
  // because short boundary counts can differ by one frame.
8739
8971
  {
8740
8972
  fps: job.config.fps,
8741
- outputDir: join16(compiledDir, "__hyperframes_video_frames"),
8973
+ outputDir: join17(compiledDir, "__hyperframes_video_frames"),
8742
8974
  format: job.config.videoFrameFormat ?? "auto",
8743
8975
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
8744
8976
  timelineEnd: composition.duration,
8745
8977
  maxTransientRetries: extractionPolicy.maxTransientRetries,
8746
8978
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
8747
8979
  deferRangeExtraction,
8748
- contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)],
8980
+ contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
8749
8981
  urlByAbsentLocalPath
8750
8982
  },
8751
8983
  abortSignal,
@@ -8812,8 +9044,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8812
9044
  }
8813
9045
 
8814
9046
  // src/services/render/stages/audioStage.ts
8815
- import { join as join18 } from "node:path";
8816
- import { rmSync as rmSync8 } from "node:fs";
9047
+ import { join as join19 } from "node:path";
9048
+ import { rmSync as rmSync9 } from "node:fs";
8817
9049
  import {
8818
9050
  MIXED_AUDIO_FILENAME,
8819
9051
  audioFailureDiagnostics,
@@ -8822,8 +9054,8 @@ import {
8822
9054
 
8823
9055
  // src/services/render/audioPadTrim.ts
8824
9056
  import { spawn } from "node:child_process";
8825
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync7 } from "node:fs";
8826
- import { dirname as dirname7, join as join17 } from "node:path";
9057
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
9058
+ import { dirname as dirname7, join as join18 } from "node:path";
8827
9059
  import {
8828
9060
  DEFAULT_CONFIG,
8829
9061
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9026,7 +9258,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9026
9258
  error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
9027
9259
  };
9028
9260
  } finally {
9029
- for (const path of plan.cleanupPaths) rmSync7(path, { force: true });
9261
+ for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
9030
9262
  }
9031
9263
  let loweredDb = 0;
9032
9264
  if (probeTruePeak) {
@@ -9063,19 +9295,21 @@ async function constrainAacTruePeak(input) {
9063
9295
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
9064
9296
  return enforceAacTruePeak({
9065
9297
  audioPath: input.audioPath,
9298
+ sourcePath: input.sourcePath,
9066
9299
  targetDurationSeconds: input.targetDurationSeconds,
9067
9300
  signal: input.signal,
9068
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
9069
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9301
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
9302
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9070
9303
  });
9071
9304
  }
9072
9305
  async function enforceAacTruePeak(input) {
9073
9306
  let scratchDir;
9074
9307
  try {
9075
- scratchDir = mkdtempSync3(join17(dirname7(input.audioPath), ".true-peak-"));
9076
- const correctedPath = join17(scratchDir, "audio.m4a");
9308
+ scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9309
+ const correctedPath = join18(scratchDir, "audio.m4a");
9077
9310
  const measurements = [];
9078
9311
  let attenuationDb = 0;
9312
+ let sourcePeakDbfs;
9079
9313
  let measuredPath = input.audioPath;
9080
9314
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
9081
9315
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -9092,7 +9326,11 @@ async function enforceAacTruePeak(input) {
9092
9326
  }
9093
9327
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
9094
9328
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
9095
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
9329
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
9330
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
9331
+ }
9332
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
9333
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
9096
9334
  const result = await input.encode(
9097
9335
  input.audioPath,
9098
9336
  correctedPath,
@@ -9124,7 +9362,7 @@ async function enforceAacTruePeak(input) {
9124
9362
  )}`
9125
9363
  };
9126
9364
  } finally {
9127
- if (scratchDir) rmSync7(scratchDir, { recursive: true, force: true });
9365
+ if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9128
9366
  }
9129
9367
  }
9130
9368
  function failResult(outputPath, target, source, error) {
@@ -9271,7 +9509,7 @@ function assertAudioNotInterrupted(failures) {
9271
9509
  }
9272
9510
  async function runAudioStage(input) {
9273
9511
  const stageStart = Date.now();
9274
- const pcmSourcePath = join18(input.workDir, "audio-source.wav");
9512
+ const pcmSourcePath = join19(input.workDir, "audio-source.wav");
9275
9513
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9276
9514
  try {
9277
9515
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9291,7 +9529,7 @@ async function runAudioStage(input) {
9291
9529
  }
9292
9530
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9293
9531
  } finally {
9294
- rmSync8(pcmSourcePath, { force: true });
9532
+ rmSync9(pcmSourcePath, { force: true });
9295
9533
  }
9296
9534
  }
9297
9535
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9309,7 +9547,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9309
9547
  assertNotAborted,
9310
9548
  log
9311
9549
  } = input;
9312
- const audioOutputPath = join18(workDir, MIXED_AUDIO_FILENAME);
9550
+ const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
9313
9551
  let hasAudio = false;
9314
9552
  let audioError;
9315
9553
  let audioFailures;
@@ -9320,7 +9558,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9320
9558
  audioResult = await processCompositionAudio(
9321
9559
  audios,
9322
9560
  projectDir,
9323
- join18(workDir, "audio-work"),
9561
+ join19(workDir, "audio-work"),
9324
9562
  audioOutputPath,
9325
9563
  duration,
9326
9564
  abortSignal,
@@ -9389,7 +9627,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9389
9627
 
9390
9628
  // src/services/render/stages/captureStage.ts
9391
9629
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9392
- import { join as join19 } from "node:path";
9630
+ import { join as join20 } from "node:path";
9393
9631
  import {
9394
9632
  classifyCaptureFailure as classifyCaptureFailure2,
9395
9633
  frameFileExtension,
@@ -9462,13 +9700,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9462
9700
  if (!existsSync12(workDir)) return total;
9463
9701
  for (const attempt of readdirSync3(workDir)) {
9464
9702
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9465
- total += sampleDirectoryBytes(join19(workDir, attempt));
9703
+ total += sampleDirectoryBytes(join20(workDir, attempt));
9466
9704
  continue;
9467
9705
  }
9468
9706
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9469
- const attemptDir = join19(workDir, attempt);
9707
+ const attemptDir = join20(workDir, attempt);
9470
9708
  for (const name of readdirSync3(attemptDir)) {
9471
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join19(attemptDir, name));
9709
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
9472
9710
  }
9473
9711
  }
9474
9712
  return total;
@@ -9528,7 +9766,7 @@ async function runCaptureStage(input) {
9528
9766
  );
9529
9767
  }
9530
9768
  }
9531
- const captureOptions = buildCaptureOptions();
9769
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
9532
9770
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
9533
9771
  const checkDiskProjection = createDiskCaptureProjection({
9534
9772
  framesDir,
@@ -9755,7 +9993,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
9755
9993
 
9756
9994
  // src/services/render/stages/captureHdrStage.ts
9757
9995
  import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
9758
- import { join as join23 } from "node:path";
9996
+ import { join as join24 } from "node:path";
9759
9997
  import {
9760
9998
  closeCaptureSession as closeCaptureSession7,
9761
9999
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -9827,7 +10065,7 @@ function createHdrImageTransferCache(options = {}) {
9827
10065
  }
9828
10066
 
9829
10067
  // src/services/render/stages/captureHdrSequentialLoop.ts
9830
- import { join as join20 } from "node:path";
10068
+ import { join as join21 } from "node:path";
9831
10069
  import {
9832
10070
  TRANSITIONS,
9833
10071
  crossfade
@@ -9952,7 +10190,7 @@ async function runSequentialLayeredFrameLoop(input) {
9952
10190
  );
9953
10191
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
9954
10192
  writeFileExclusiveSync(
9955
- join20(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10193
+ join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
9956
10194
  normalCanvas
9957
10195
  );
9958
10196
  }
@@ -9984,7 +10222,7 @@ async function runSequentialLayeredFrameLoop(input) {
9984
10222
  }
9985
10223
 
9986
10224
  // src/services/render/stages/captureHdrHybridLoop.ts
9987
- import { join as join22 } from "node:path";
10225
+ import { join as join23 } from "node:path";
9988
10226
  import {
9989
10227
  TRANSITIONS as TRANSITIONS2,
9990
10228
  closeCaptureSession as closeCaptureSession6,
@@ -9998,7 +10236,7 @@ import {
9998
10236
  // src/services/shaderTransitionWorkerPool.ts
9999
10237
  import { Worker } from "node:worker_threads";
10000
10238
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10001
- import { dirname as dirname8, join as join21 } from "node:path";
10239
+ import { dirname as dirname8, join as join22 } from "node:path";
10002
10240
  import { createRequire } from "node:module";
10003
10241
  import { existsSync as existsSync13 } from "node:fs";
10004
10242
  import { cpus as cpus2 } from "node:os";
@@ -10012,9 +10250,9 @@ function resolveWorkerEntry(explicit) {
10012
10250
  return { path: override, isTs };
10013
10251
  }
10014
10252
  const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10015
- const jsPath = join21(moduleDir, "shaderTransitionWorker.js");
10253
+ const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10016
10254
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10017
- const tsPath = join21(moduleDir, "shaderTransitionWorker.ts");
10255
+ const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10018
10256
  return { path: tsPath, isTs: true };
10019
10257
  }
10020
10258
  function buildExecArgv(entryIsTs) {
@@ -10382,7 +10620,7 @@ async function runHybridLayeredFrameLoop(input) {
10382
10620
  );
10383
10621
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10384
10622
  writeFileExclusiveSync(
10385
- join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10623
+ join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10386
10624
  canvas
10387
10625
  );
10388
10626
  }
@@ -10551,7 +10789,8 @@ async function runCaptureHdrStage(input) {
10551
10789
  width,
10552
10790
  height,
10553
10791
  abortSignal,
10554
- hdrDiagnostics
10792
+ hdrDiagnostics,
10793
+ maxResidentWindows: Math.max(1, workerCount) + 1
10555
10794
  });
10556
10795
  releaseHdrExtractionReservation = extracted.releaseReservation;
10557
10796
  for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
@@ -10569,7 +10808,7 @@ async function runCaptureHdrStage(input) {
10569
10808
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10570
10809
  }
10571
10810
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10572
- const debugDumpDir = debugDumpEnabled ? join23(framesDir, "debug-composite") : null;
10811
+ const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
10573
10812
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10574
10813
  mkdirSync7(debugDumpDir, { recursive: true });
10575
10814
  }
@@ -10729,12 +10968,12 @@ import {
10729
10968
  mkdirSync as mkdirSync8,
10730
10969
  readdirSync as readdirSync4,
10731
10970
  readFileSync as readFileSync7,
10732
- rmSync as rmSync9,
10733
- statSync as statSync5,
10971
+ rmSync as rmSync10,
10972
+ statSync as statSync6,
10734
10973
  writeFileSync as writeFileSync4
10735
10974
  } from "node:fs";
10736
10975
  import { readFile as readFile2 } from "node:fs/promises";
10737
- import { dirname as dirname9, join as join24 } from "node:path";
10976
+ import { dirname as dirname9, join as join25 } from "node:path";
10738
10977
  import {
10739
10978
  encodeFramesChunkedConcat,
10740
10979
  encodeFramesFromDir,
@@ -10859,7 +11098,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10859
11098
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
10860
11099
  if (failure) return failure;
10861
11100
  }
10862
- const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
11101
+ const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
10863
11102
  return {
10864
11103
  success: true,
10865
11104
  outputPath,
@@ -10868,7 +11107,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10868
11107
  fileSize
10869
11108
  };
10870
11109
  } finally {
10871
- rmSync9(input.palettePath, { force: true });
11110
+ rmSync10(input.palettePath, { force: true });
10872
11111
  }
10873
11112
  }
10874
11113
  function startEncodeProgress(job, stage, onProgress) {
@@ -10943,7 +11182,7 @@ function encodeWhileCapturing(input, totalFrames) {
10943
11182
  await new Promise((resolve10) => wake = resolve10);
10944
11183
  }
10945
11184
  if (signal.aborted) return;
10946
- const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
11185
+ const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
10947
11186
  const frame = await readLandedFrame(landed.get(next), mergedPath);
10948
11187
  landed.delete(next);
10949
11188
  if (!await stream.writeFrame(frame)) {
@@ -11026,11 +11265,11 @@ async function runEncodeStage(input) {
11026
11265
  );
11027
11266
  }
11028
11267
  captured.forEach((name, i) => {
11029
- const dst = join24(outputPath, formatExportFrameName(i, "png"));
11030
- copyFileSync3(join24(framesDir, name), dst);
11268
+ const dst = join25(outputPath, formatExportFrameName(i, "png"));
11269
+ copyFileSync3(join25(framesDir, name), dst);
11031
11270
  });
11032
11271
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11033
- copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
11272
+ copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11034
11273
  log.info(
11035
11274
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11036
11275
  );
@@ -11050,7 +11289,7 @@ async function runEncodeStage(input) {
11050
11289
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11051
11290
  fps: job.config.fps,
11052
11291
  loop,
11053
- palettePath: join24(dirname9(videoOnlyPath), "gif-palette.png"),
11292
+ palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11054
11293
  preserveAlpha: needsAlpha,
11055
11294
  signal: abortSignal,
11056
11295
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11263,9 +11502,9 @@ function sampleDirectoryBytes(dir) {
11263
11502
  continue;
11264
11503
  }
11265
11504
  for (const name of entries) {
11266
- const full = join25(current, name);
11505
+ const full = join26(current, name);
11267
11506
  try {
11268
- const st = statSync6(full);
11507
+ const st = statSync7(full);
11269
11508
  if (st.isDirectory()) {
11270
11509
  stack.push(full);
11271
11510
  } else if (st.isFile()) {
@@ -11412,8 +11651,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11412
11651
  const ranges = [];
11413
11652
  let rangeStart = null;
11414
11653
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11415
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11416
- const missing = !existsSync16(framePath) || statSync6(framePath).size <= 8;
11654
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11655
+ const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
11417
11656
  if (missing && rangeStart === null) {
11418
11657
  rangeStart = frameIndex;
11419
11658
  } else if (!missing && rangeStart !== null) {
@@ -11435,7 +11674,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
11435
11674
  workerId,
11436
11675
  startFrame: rangeStart + range.startFrame,
11437
11676
  endFrame: rangeStart + range.endFrame,
11438
- outputDir: join25(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11677
+ outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11439
11678
  outputFrameOffset: rangeStart
11440
11679
  }));
11441
11680
  batches.push(batch);
@@ -11463,8 +11702,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11463
11702
  return Math.max(1, Math.floor(currentWorkers / 2));
11464
11703
  }
11465
11704
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11466
- if (platform2 === "win32" || platform2 === "darwin") return join25(systemTempDir, "hf-render-");
11467
- return join25(dirname10(outputPath), `work-${jobId}-`);
11705
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11706
+ return join26(dirname10(outputPath), `work-${jobId}-`);
11468
11707
  }
11469
11708
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11470
11709
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11496,7 +11735,7 @@ The composition is too large for the available memory. To reduce memory pressure
11496
11735
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11497
11736
  let captured = 0;
11498
11737
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11499
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11738
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11500
11739
  if (existsSync16(framePath)) captured++;
11501
11740
  }
11502
11741
  return captured;
@@ -11522,7 +11761,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11522
11761
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11523
11762
  });
11524
11763
  pendingTransientRetry = false;
11525
- const attemptWorkDir = join25(options.workDir, `capture-attempt-${attempt}`);
11764
+ const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
11526
11765
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11527
11766
  missingRanges,
11528
11767
  currentWorkers,
@@ -11979,7 +12218,10 @@ function resolveDeShortBand(args) {
11979
12218
  return args.bandOpen ? "applied" : "skipped_elements";
11980
12219
  }
11981
12220
  function shouldPreferSingleWorkerDrawElement(args) {
11982
- return args.workerCount > 1 && typeof args.requestedWorkers !== "number" && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12221
+ return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12222
+ }
12223
+ function probeNeedsScreenshotFrames(session) {
12224
+ return (session?.deRiskFrames?.size ?? 0) > 0;
11983
12225
  }
11984
12226
  function isProbeDeGated(session) {
11985
12227
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
@@ -12115,17 +12357,17 @@ function deVerifyFallbackTelemetry(err) {
12115
12357
  }
12116
12358
  function resolveRenderDebugDir() {
12117
12359
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12118
- return join25(producerRoot, ".debug");
12360
+ return join26(producerRoot, ".debug");
12119
12361
  }
12120
12362
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12121
12363
  validateHlsRenderConfig(job.config);
12122
12364
  const debugDir = resolveRenderDebugDir();
12123
12365
  const outputDir = dirname10(outputPath);
12124
12366
  if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12125
- const workDir = job.config.debug ? join25(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12367
+ const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12126
12368
  const pipelineStart = Date.now();
12127
12369
  const baseLog = job.config.logger ?? defaultLogger;
12128
- const logPath = job.config.debug ? join25(workDir, "render.log") : null;
12370
+ const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12129
12371
  const execution = new RenderExecutionContext({
12130
12372
  request: { renderJobId: job.id, projectDir, outputPath },
12131
12373
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12139,7 +12381,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12139
12381
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12140
12382
  return;
12141
12383
  }
12142
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12384
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12143
12385
  });
12144
12386
  try {
12145
12387
  await executeRenderPipeline({
@@ -12170,7 +12412,9 @@ async function executeRenderPipeline(input) {
12170
12412
  const log = execution.logger;
12171
12413
  const eventPublisher = execution.events;
12172
12414
  const onProgress = execution.onProgress;
12173
- const executionSignal = execution.signal;
12415
+ const tooSlow = new AbortController();
12416
+ let tooSlowError;
12417
+ const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
12174
12418
  let fileServer = null;
12175
12419
  let probeSession = null;
12176
12420
  let lastBrowserConsole = [];
@@ -12183,7 +12427,7 @@ async function executeRenderPipeline(input) {
12183
12427
  imageDecodeFailures: 0
12184
12428
  };
12185
12429
  let hdrPerf;
12186
- const perfOutputPath = join25(workDir, "perf-summary.json");
12430
+ const perfOutputPath = join26(workDir, "perf-summary.json");
12187
12431
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12188
12432
  const observability = new RenderObservabilityRecorder({
12189
12433
  pipelineStartMs: pipelineStart,
@@ -12285,15 +12529,15 @@ async function executeRenderPipeline(input) {
12285
12529
  requestedWorkers: job.config.workers ?? "auto"
12286
12530
  });
12287
12531
  const entryFile = job.config.entryFile || "index.html";
12288
- let htmlPath = join25(projectDir, entryFile);
12532
+ let htmlPath = join26(projectDir, entryFile);
12289
12533
  if (!existsSync16(htmlPath)) {
12290
12534
  throw new Error(`Entry file not found: ${htmlPath}`);
12291
12535
  }
12292
12536
  assertNotAborted();
12293
12537
  const rawEntry = readFileSync8(htmlPath, "utf-8");
12294
12538
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12295
- const wrapperPath = join25(workDir, "standalone-entry.html");
12296
- const projectIndexPath = join25(projectDir, "index.html");
12539
+ const wrapperPath = join26(workDir, "standalone-entry.html");
12540
+ const projectIndexPath = join26(projectDir, "index.html");
12297
12541
  if (!existsSync16(projectIndexPath)) {
12298
12542
  throw new Error(
12299
12543
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -12456,7 +12700,7 @@ async function executeRenderPipeline(input) {
12456
12700
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12457
12701
  code: "extract_video"
12458
12702
  });
12459
- const compiledDir = join25(workDir, "compiled");
12703
+ const compiledDir = join26(workDir, "compiled");
12460
12704
  const extractResult = await observeRenderStage(
12461
12705
  observability,
12462
12706
  "video_extract",
@@ -12590,7 +12834,7 @@ async function executeRenderPipeline(input) {
12590
12834
  try {
12591
12835
  fileServer = await createFileServer({
12592
12836
  projectDir,
12593
- compiledDir: join25(workDir, "compiled"),
12837
+ compiledDir: join26(workDir, "compiled"),
12594
12838
  port: 0,
12595
12839
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12596
12840
  fps: job.config.fps
@@ -12608,7 +12852,7 @@ async function executeRenderPipeline(input) {
12608
12852
  if (!activeFileServer) {
12609
12853
  throw new Error("File server failed to initialize before frame capture");
12610
12854
  }
12611
- const framesDir = join25(workDir, "captured-frames");
12855
+ const framesDir = join26(workDir, "captured-frames");
12612
12856
  if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
12613
12857
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
12614
12858
  code: "start_browsers"
@@ -12657,6 +12901,7 @@ async function executeRenderPipeline(input) {
12657
12901
  // Probe-resolved duration: drawElement self-verification derives its
12658
12902
  // sample frame indices from this so they land inside the drained range.
12659
12903
  compositionDurationSeconds: job.duration,
12904
+ deFrameFallback: true,
12660
12905
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
12661
12906
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
12662
12907
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -12712,7 +12957,6 @@ async function executeRenderPipeline(input) {
12712
12957
  const WOULD_RESOLVE_MULTI_WORKER = 2;
12713
12958
  const deInversionArgs = {
12714
12959
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12715
- requestedWorkers: job.config.workers,
12716
12960
  useDrawElement: cfg.useDrawElement,
12717
12961
  deCompileGate,
12718
12962
  forceScreenshot: captureForceScreenshot,
@@ -12758,7 +13002,7 @@ async function executeRenderPipeline(input) {
12758
13002
  minFrames: deParallelMinFrames,
12759
13003
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12760
13004
  supersampling: deviceScaleFactor !== 1,
12761
- probeDeGated: isProbeDeGated(probeSession),
13005
+ probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
12762
13006
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12763
13007
  routerEnabled: deParallelRouterEnabled,
12764
13008
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12800,6 +13044,8 @@ async function executeRenderPipeline(input) {
12800
13044
  deInversionEligible = false;
12801
13045
  deParallelRouterEligible = false;
12802
13046
  deShortBand = void 0;
13047
+ } else if (probeNeedsScreenshotFrames(probeSession)) {
13048
+ deParallelRouterEligible = false;
12803
13049
  }
12804
13050
  }
12805
13051
  const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
@@ -12918,7 +13164,7 @@ async function executeRenderPipeline(input) {
12918
13164
  } else if (deInversionEligible && workerCount > 1) {
12919
13165
  deWorkerInversion = "inverted";
12920
13166
  log.info(
12921
- `[Render] Fast capture: single-worker drawElement streaming preferred over ${workerCount}-worker screenshot capture (${totalFrames} frames >= ${deInversionEffectiveMinFrames}; verified path, measured faster at every worker count). Set HF_DE_SINGLE_MIN_FRAMES=0 or --workers N to override.`
13167
+ `[Render] Fast capture: single-worker drawElement streaming preferred over ${workerCount}-worker screenshot capture (${totalFrames} frames >= ${deInversionEffectiveMinFrames}; verified path, measured faster at every worker count). Set HF_DE_SINGLE_MIN_FRAMES=0 to override.`
12922
13168
  );
12923
13169
  workerCount = 1;
12924
13170
  }
@@ -12962,6 +13208,27 @@ async function executeRenderPipeline(input) {
12962
13208
  deWorkerInversion: deWorkerInversion ?? "none",
12963
13209
  deParallelRouter: deParallelRouter ?? "none"
12964
13210
  });
13211
+ const calibratedTooSlow = checkRenderEta("capture_calibration", {
13212
+ maxRenderMs: job.config.maxRenderMs,
13213
+ elapsedMs: Date.now() - pipelineStart,
13214
+ framesCompleted: 0,
13215
+ totalFrames,
13216
+ msPerFrame: (captureCalibration?.estimate.p95Ms ?? 0) / workerCount
13217
+ });
13218
+ if (calibratedTooSlow) throw calibratedTooSlow;
13219
+ execution.defer(
13220
+ "stop render ETA guard",
13221
+ startRenderEtaGuard({
13222
+ maxRenderMs: job.config.maxRenderMs,
13223
+ job,
13224
+ totalFrames,
13225
+ pipelineStartMs: pipelineStart,
13226
+ onTooSlow: (error) => {
13227
+ tooSlowError = error;
13228
+ tooSlow.abort(error);
13229
+ }
13230
+ })
13231
+ );
12965
13232
  let useStreamingEncode = shouldUseStreamingEncode(
12966
13233
  cfg,
12967
13234
  outputFormat,
@@ -12970,7 +13237,8 @@ async function executeRenderPipeline(input) {
12970
13237
  deParallelStreamForced
12971
13238
  );
12972
13239
  const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
12973
- if (shouldClampDefaultDrawElement({
13240
+ const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
13241
+ if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
12974
13242
  useDrawElement: cfg.useDrawElement,
12975
13243
  fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
12976
13244
  useStreamingEncode,
@@ -13055,7 +13323,7 @@ async function executeRenderPipeline(input) {
13055
13323
  hls: ".mp4"
13056
13324
  };
13057
13325
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13058
- const videoOnlyPath = join25(workDir, `video-only${videoExt}`);
13326
+ const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13059
13327
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13060
13328
  if (usePageSideCompositingForTransitions) {
13061
13329
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13301,7 +13569,7 @@ async function executeRenderPipeline(input) {
13301
13569
  pageMotionBlur: job.config.pageMotionBlur,
13302
13570
  captureQuality: captureOptions.quality
13303
13571
  });
13304
- const segmentDir = segmentDirFor(join25(projectDir, "renders"), segmentPlanHash);
13572
+ const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
13305
13573
  let completedSegments = /* @__PURE__ */ new Set();
13306
13574
  if (job.config.resumeSegments === true) {
13307
13575
  const existing = readSegmentManifest(segmentDir);
@@ -13316,10 +13584,10 @@ async function executeRenderPipeline(input) {
13316
13584
  });
13317
13585
  }
13318
13586
  if (completedSegments.size === 0) {
13319
- rmSync10(segmentDir, { recursive: true, force: true });
13587
+ rmSync11(segmentDir, { recursive: true, force: true });
13320
13588
  }
13321
13589
  } else {
13322
- rmSync10(segmentDir, { recursive: true, force: true });
13590
+ rmSync11(segmentDir, { recursive: true, force: true });
13323
13591
  }
13324
13592
  const segmentManifest = readSegmentManifest(segmentDir) ?? {
13325
13593
  version: 1,
@@ -13400,7 +13668,7 @@ async function executeRenderPipeline(input) {
13400
13668
  );
13401
13669
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13402
13670
  if (job.config.keepSegments !== true) {
13403
- rmSync10(segmentDir, { recursive: true, force: true });
13671
+ rmSync11(segmentDir, { recursive: true, force: true });
13404
13672
  }
13405
13673
  } else {
13406
13674
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -13669,7 +13937,7 @@ async function executeRenderPipeline(input) {
13669
13937
  "capture_disk",
13670
13938
  "drawElement capture failed; retrying with a fresh screenshot session"
13671
13939
  );
13672
- rmSync10(framesDir, { recursive: true, force: true });
13940
+ rmSync11(framesDir, { recursive: true, force: true });
13673
13941
  mkdirSync9(framesDir, { recursive: true });
13674
13942
  resetCaptureAttemptProgress(job);
13675
13943
  dedupPerfs.length = 0;
@@ -13856,7 +14124,7 @@ async function executeRenderPipeline(input) {
13856
14124
  }
13857
14125
  if (job.config.debug) {
13858
14126
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
13859
- const debugOutput = join25(workDir, `output${videoExt}`);
14127
+ const debugOutput = join26(workDir, `output${videoExt}`);
13860
14128
  copyFileSync4(stagedOutputPath, debugOutput);
13861
14129
  }
13862
14130
  }
@@ -13867,8 +14135,9 @@ async function executeRenderPipeline(input) {
13867
14135
  job.outputPath = outputPath;
13868
14136
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
13869
14137
  await eventPublisher.flush();
13870
- } catch (error) {
13871
- if (error instanceof RenderCancelledError || executionSignal?.aborted) {
14138
+ } catch (caught) {
14139
+ const error = tooSlowError ?? caught;
14140
+ if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
13872
14141
  job.error = error instanceof Error ? error.message : "render_cancelled";
13873
14142
  updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
13874
14143
  await eventPublisher.flush();
@@ -13962,13 +14231,13 @@ async function executeRenderPipeline(input) {
13962
14231
  import {
13963
14232
  existsSync as existsSync17,
13964
14233
  readFileSync as readFileSync9,
13965
- statSync as statSync7,
14234
+ statSync as statSync8,
13966
14235
  openSync as openSync4,
13967
- fstatSync as fstatSync4,
13968
- closeSync as closeSync5,
14236
+ fstatSync as fstatSync3,
14237
+ closeSync as closeSync4,
13969
14238
  constants as constants3
13970
14239
  } from "node:fs";
13971
- import { resolve as resolve8, join as join26, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
14240
+ import { resolve as resolve8, join as join27, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
13972
14241
  import { lintHyperframeHtml } from "@hyperframes/lint";
13973
14242
  function isStringRecord(value) {
13974
14243
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -14002,19 +14271,19 @@ function readEntryHtml(filePath) {
14002
14271
  } catch (error) {
14003
14272
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
14004
14273
  return null;
14005
- if (!statSync7(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14274
+ if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14006
14275
  throw error;
14007
14276
  }
14008
14277
  try {
14009
- if (!fstatSync4(fd).isFile()) return null;
14278
+ if (!fstatSync3(fd).isFile()) return null;
14010
14279
  return readFileSync9(fd, "utf-8");
14011
14280
  } finally {
14012
- closeSync5(fd);
14281
+ closeSync4(fd);
14013
14282
  }
14014
14283
  }
14015
14284
  function readProjectEntryFile(projectDir, preferredEntryFile) {
14016
14285
  const absProjectDir = resolve8(projectDir);
14017
- if (!existsSync17(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
14286
+ if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14018
14287
  return { error: `Project directory not found: ${absProjectDir}` };
14019
14288
  }
14020
14289
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -14036,7 +14305,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
14036
14305
  }
14037
14306
  }
14038
14307
  return {
14039
- error: `No HTML entry file found in project directory: ${join26(absProjectDir, preferredEntryFile || "index.html")}`
14308
+ error: `No HTML entry file found in project directory: ${join27(absProjectDir, preferredEntryFile || "index.html")}`
14040
14309
  };
14041
14310
  }
14042
14311
  function prepareHyperframeLintBody(body) {
@@ -14082,7 +14351,7 @@ async function runHyperframeLint(prepared) {
14082
14351
  // src/services/healthWorker.ts
14083
14352
  import { Worker as Worker2 } from "node:worker_threads";
14084
14353
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14085
- import { dirname as dirname11, join as join27 } from "node:path";
14354
+ import { dirname as dirname11, join as join28 } from "node:path";
14086
14355
  import { existsSync as existsSync18 } from "node:fs";
14087
14356
  var DEFAULT_HEALTH_PORT = 9848;
14088
14357
  async function startHealthWorker(options = {}) {
@@ -14158,7 +14427,7 @@ function defaultLogger2() {
14158
14427
  }
14159
14428
  function resolveWorkerEntry2() {
14160
14429
  const here = dirname11(fileURLToPath5(import.meta.url));
14161
- const candidates = [join27(here, "healthWorkerThread.js"), join27(here, "healthWorkerThread.ts")];
14430
+ const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
14162
14431
  for (const candidate of candidates) {
14163
14432
  if (existsSync18(candidate)) return candidate;
14164
14433
  }
@@ -14194,7 +14463,7 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
14194
14463
 
14195
14464
  // src/services/distributed/plan.ts
14196
14465
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
14197
- import { extname as extname4, isAbsolute as isAbsolute4, join as join28, relative as relative4, sep as sep3 } from "node:path";
14466
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
14198
14467
  import {
14199
14468
  fpsToNumber as fpsToNumber10,
14200
14469
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -14298,9 +14567,9 @@ var PLAN_ROOT_CATEGORY = {
14298
14567
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
14299
14568
  var FREEZE_OWNED_PLAN_FILES = [
14300
14569
  "plan.json",
14301
- join28("meta", "composition.json"),
14302
- join28("meta", "encoder.json"),
14303
- join28("meta", "chunks.json")
14570
+ join29("meta", "composition.json"),
14571
+ join29("meta", "encoder.json"),
14572
+ join29("meta", "chunks.json")
14304
14573
  ];
14305
14574
 
14306
14575
  // src/services/distributed/renderConfigValidation.ts
@@ -14441,6 +14710,7 @@ function assertRequestOptionScalars(options) {
14441
14710
  assertOptionalInteger(options, "gifLoop");
14442
14711
  assertOptionalInteger(options, "hlsSegmentSeconds", 1);
14443
14712
  assertOptionalInteger(options, "workers", 1);
14713
+ assertOptionalInteger(options, "maxRenderMs");
14444
14714
  assertOptionalInteger(options, "crf");
14445
14715
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
14446
14716
  assertOptionalBoolean(options, field);
@@ -14533,7 +14803,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
14533
14803
  "INVALID_VIDEO_METADATA",
14534
14804
  "VIDEO_SOURCE_UNRENDERABLE",
14535
14805
  "VIDEO_EXTRACTION_FAILED",
14536
- "ENCODER_INTERRUPTED"
14806
+ "ENCODER_INTERRUPTED",
14807
+ "RENDER_TOO_SLOW"
14537
14808
  ]);
14538
14809
  function extractSafeRenderErrorCode(error) {
14539
14810
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -14592,6 +14863,7 @@ function parseRenderOptions(body) {
14592
14863
  const fps = parseServerFps(body.fps);
14593
14864
  const quality = parseServerQuality(body.quality);
14594
14865
  const workers = typeof body.workers === "number" ? body.workers : void 0;
14866
+ const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
14595
14867
  const useGpu = body.gpu === true;
14596
14868
  const debug = body.debug === true;
14597
14869
  const strictness = body.bestEffort === false ? "strict" : "best-effort";
@@ -14607,6 +14879,7 @@ function parseRenderOptions(body) {
14607
14879
  fps,
14608
14880
  quality,
14609
14881
  workers,
14882
+ maxRenderMs,
14610
14883
  crf,
14611
14884
  videoBitrate,
14612
14885
  useGpu,
@@ -14640,6 +14913,7 @@ function buildRenderJobConfig(input, outputPath, log) {
14640
14913
  quality: input.quality,
14641
14914
  format: input.format ?? "mp4",
14642
14915
  workers: input.workers,
14916
+ maxRenderMs: input.maxRenderMs,
14643
14917
  crf: input.crf,
14644
14918
  videoBitrate: input.videoBitrate,
14645
14919
  useGpu: input.useGpu,
@@ -14666,6 +14940,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
14666
14940
  function validateRenderOverrides(body) {
14667
14941
  const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
14668
14942
  if (!encoder.ok) return encoder.error;
14943
+ if (body.maxRenderMs !== void 0 && (typeof body.maxRenderMs !== "number" || !Number.isInteger(body.maxRenderMs) || body.maxRenderMs < 0)) {
14944
+ return "maxRenderMs must be a non-negative integer number of milliseconds";
14945
+ }
14669
14946
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
14670
14947
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
14671
14948
  }
@@ -14698,7 +14975,7 @@ function prepareProjectDirectory(projectDir, options) {
14698
14975
  const candidate = nonEmptyString(projectDir);
14699
14976
  if (!candidate) return null;
14700
14977
  const absProjectDir = resolve9(candidate);
14701
- if (!existsSync19(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14978
+ if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
14702
14979
  return { error: `Project directory not found: ${absProjectDir}` };
14703
14980
  }
14704
14981
  const entry = options.entryFile || "index.html";
@@ -14723,8 +15000,8 @@ async function resolveInlineRenderHtml(body) {
14723
15000
  }
14724
15001
  function materializeInlineProject(html, options) {
14725
15002
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
14726
- const tempProjectDir = mkdtempSync4(join29(tempRoot, "producer-project-"));
14727
- writeFileSync6(join29(tempProjectDir, "index.html"), html, "utf-8");
15003
+ const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
15004
+ writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
14728
15005
  return {
14729
15006
  prepared: {
14730
15007
  input: { projectDir: tempProjectDir, ...options },
@@ -14785,7 +15062,7 @@ function createArtifactStore(ttlMs) {
14785
15062
  function cleanupTempDir(dir, log) {
14786
15063
  if (!dir) return;
14787
15064
  try {
14788
- rmSync11(dir, { recursive: true, force: true });
15065
+ rmSync12(dir, { recursive: true, force: true });
14789
15066
  } catch (error) {
14790
15067
  log.warn("Failed to cleanup temp project dir", {
14791
15068
  cleanupProjectDir: dir,
@@ -14794,7 +15071,7 @@ function cleanupTempDir(dir, log) {
14794
15071
  }
14795
15072
  }
14796
15073
  function outputFileSize(path) {
14797
- return existsSync19(path) ? statSync8(path).size : 0;
15074
+ return existsSync19(path) ? statSync9(path).size : 0;
14798
15075
  }
14799
15076
  function createBlockingProgressReporter(log, requestId) {
14800
15077
  let lastLoggedPct = -10;
@@ -15094,7 +15371,7 @@ function createRenderHandlers(options = {}) {
15094
15371
  store.delete(token);
15095
15372
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
15096
15373
  }
15097
- const stats = statSync8(artifact.path);
15374
+ const stats = statSync9(artifact.path);
15098
15375
  return new Response(createReadStream3(artifact.path), {
15099
15376
  headers: {
15100
15377
  "content-type": "video/mp4",