@hyperframes/producer 0.8.198 → 0.8.199

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,
@@ -2295,7 +2295,7 @@ import {
2295
2295
  } from "@hyperframes/engine";
2296
2296
 
2297
2297
  // src/services/hdrCompositor.ts
2298
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2298
+ import { readSync as readSync2 } from "fs";
2299
2299
  import { join as join6 } from "path";
2300
2300
  import {
2301
2301
  captureAlphaPng,
@@ -2348,10 +2348,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
2348
2348
  }
2349
2349
  function closeHdrVideoFrameSource(source, log) {
2350
2350
  try {
2351
- closeSync3(source.fd);
2351
+ source.windows.close();
2352
2352
  } catch (err) {
2353
- log?.warn("Failed to close HDR raw frame file", {
2354
- rawPath: source.rawPath,
2353
+ log?.warn("Failed to close HDR raw frame windows", {
2354
+ dir: source.dir,
2355
2355
  error: err instanceof Error ? err.message : String(err)
2356
2356
  });
2357
2357
  }
@@ -2361,7 +2361,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
2361
2361
  if (frameIndex < 0 || frameCount < 1) return null;
2362
2362
  return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
2363
2363
  }
2364
- function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2364
+ async function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2365
2365
  const frameSource = hdrVideoFrameSources.get(el.id);
2366
2366
  const startTime = hdrStartTimes.get(el.id);
2367
2367
  if (!frameSource || startTime === void 0 || el.opacity <= 0) {
@@ -2377,13 +2377,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
2377
2377
  if (effectiveIndex === null) return;
2378
2378
  const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
2379
2379
  if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
2380
- const frameOffset = effectiveIndex * frameSource.frameSize;
2380
+ if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2381
+ const lease = await frameSource.windows.lease(effectiveIndex);
2382
+ if (!lease) return;
2383
+ try {
2384
+ blitScratchFrameWithoutYielding(
2385
+ canvas,
2386
+ el,
2387
+ frameSource,
2388
+ lease,
2389
+ width,
2390
+ height,
2391
+ log,
2392
+ sourceTransfer,
2393
+ targetTransfer,
2394
+ hdrPerf
2395
+ );
2396
+ } finally {
2397
+ lease.release();
2398
+ }
2399
+ }
2400
+ function blitScratchFrameWithoutYielding(canvas, el, frameSource, lease, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2381
2401
  try {
2382
- if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2383
2402
  const bytesRead = timeHdrPhase(
2384
2403
  hdrPerf,
2385
2404
  "hdrVideoReadDecodeMs",
2386
- () => readSync2(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset)
2405
+ () => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
2387
2406
  );
2388
2407
  if (bytesRead !== frameSource.frameSize) return;
2389
2408
  const hdrRgb = frameSource.scratch;
@@ -2612,7 +2631,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2612
2631
  hdrPerf
2613
2632
  );
2614
2633
  } else {
2615
- blitHdrVideoLayer(
2634
+ await blitHdrVideoLayer(
2616
2635
  canvas,
2617
2636
  layer.element,
2618
2637
  time,
@@ -2656,7 +2675,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2656
2675
  startTime,
2657
2676
  localTime: localTime.toFixed(3),
2658
2677
  hdrFrameNum: frameNum,
2659
- rawPath: frameSource?.rawPath ?? null,
2678
+ dir: frameSource?.dir ?? null,
2660
2679
  frameCount: frameSource?.frameCount ?? null
2661
2680
  });
2662
2681
  }
@@ -2752,18 +2771,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2752
2771
  }
2753
2772
 
2754
2773
  // 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";
2774
+ import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, rmSync as rmSync5, statSync as statSync2, statfsSync } from "node:fs";
2775
+ import { join as join8 } from "node:path";
2767
2776
  import {
2768
2777
  decodePngToRgb48le,
2769
2778
  extractMediaMetadata as extractMediaMetadata2,
@@ -2777,7 +2786,90 @@ import {
2777
2786
  runFfmpeg
2778
2787
  } from "@hyperframes/engine";
2779
2788
  import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2780
- var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
2789
+
2790
+ // src/services/hdrFrameWindows.ts
2791
+ import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
2792
+ import { join as join7 } from "node:path";
2793
+ function createHdrFrameWindows(opts) {
2794
+ const { dir, frameSize, windowFrames, maxResident, extract } = opts;
2795
+ let totalFrames = opts.totalFrames;
2796
+ const windows = /* @__PURE__ */ new Map();
2797
+ const drop = (index, w) => {
2798
+ windows.delete(index);
2799
+ if (w.fd >= 0) closeSync3(w.fd);
2800
+ rmSync4(w.path, { force: true });
2801
+ };
2802
+ const open = (index) => {
2803
+ const known = windows.get(index);
2804
+ if (known) {
2805
+ windows.delete(index);
2806
+ windows.set(index, known);
2807
+ return known;
2808
+ }
2809
+ for (const [i, w2] of windows) {
2810
+ if (windows.size < maxResident) break;
2811
+ if (w2.pins === 0) drop(i, w2);
2812
+ }
2813
+ const firstFrame = index * windowFrames;
2814
+ const w = {
2815
+ firstFrame,
2816
+ path: join7(dir, `window-${index}.rgb48le`),
2817
+ frames: 0,
2818
+ fd: -1,
2819
+ pins: 0,
2820
+ ready: Promise.resolve()
2821
+ };
2822
+ w.ready = extract({
2823
+ firstFrame,
2824
+ frames: Math.min(windowFrames, totalFrames - firstFrame),
2825
+ path: w.path
2826
+ }).then((produced) => {
2827
+ if (produced < 1) return;
2828
+ w.fd = openSync3(w.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2829
+ w.frames = produced;
2830
+ });
2831
+ windows.set(index, w);
2832
+ return w;
2833
+ };
2834
+ return {
2835
+ get totalFrames() {
2836
+ return totalFrames;
2837
+ },
2838
+ async lease(frameIndex) {
2839
+ for (; ; ) {
2840
+ if (totalFrames < 1) return null;
2841
+ const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
2842
+ const key = Math.floor(index / windowFrames);
2843
+ const w = open(key);
2844
+ w.pins += 1;
2845
+ try {
2846
+ await w.ready;
2847
+ } catch (error) {
2848
+ w.pins -= 1;
2849
+ windows.delete(key);
2850
+ throw error;
2851
+ }
2852
+ const local = index - w.firstFrame;
2853
+ if (local < w.frames) {
2854
+ return {
2855
+ fd: w.fd,
2856
+ offset: local * frameSize,
2857
+ release: () => {
2858
+ w.pins -= 1;
2859
+ }
2860
+ };
2861
+ }
2862
+ w.pins -= 1;
2863
+ totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
2864
+ }
2865
+ },
2866
+ close() {
2867
+ for (const [i, w] of [...windows]) drop(i, w);
2868
+ }
2869
+ };
2870
+ }
2871
+
2872
+ // src/services/render/stages/captureHdrResources.ts
2781
2873
  function tempDirSafePrefix(id) {
2782
2874
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
2783
2875
  return safe || "video";
@@ -2822,10 +2914,16 @@ async function probeHdrExtractionDims(args) {
2822
2914
  const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
2823
2915
  for (const el of stacking) {
2824
2916
  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 });
2917
+ prep.hdrExtractionDims.set(el.id, {
2918
+ width: el.layoutWidth,
2919
+ height: el.layoutHeight
2920
+ });
2826
2921
  }
2827
2922
  if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
2828
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2923
+ prep.hdrImageFitInfo.set(el.id, {
2924
+ fit: el.objectFit,
2925
+ position: el.objectPosition
2926
+ });
2829
2927
  }
2830
2928
  }
2831
2929
  }
@@ -2844,23 +2942,21 @@ async function probeHdrExtractionDims(args) {
2844
2942
  const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
2845
2943
  for (const el of retryStacking) {
2846
2944
  if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
2847
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
2945
+ prep.hdrExtractionDims.set(el.id, {
2946
+ width: el.layoutWidth,
2947
+ height: el.layoutHeight
2948
+ });
2848
2949
  if (!prep.hdrImageFitInfo.has(el.id)) {
2849
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2950
+ prep.hdrImageFitInfo.set(el.id, {
2951
+ fit: el.objectFit,
2952
+ position: el.objectPosition
2953
+ });
2850
2954
  }
2851
2955
  break;
2852
2956
  }
2853
2957
  }
2854
2958
  }
2855
2959
  }
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
2960
  var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
2865
2961
  var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
2866
2962
  var HDR_EXTRACTION_WARN_BYTES = 1e10;
@@ -2924,24 +3020,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
2924
3020
  }
2925
3021
  return window2;
2926
3022
  }
2927
- function cleanupHdrFrameDirectory(frameDir, rawPath, log) {
3023
+ function cleanupHdrFrameDirectory(frameDir, log) {
2928
3024
  if (process.env.KEEP_TEMP === "1") return;
2929
3025
  try {
2930
- rmSync4(frameDir, { recursive: true, force: true });
3026
+ rmSync5(frameDir, { recursive: true, force: true });
2931
3027
  } catch (err) {
2932
3028
  log?.warn("Failed to clean up HDR raw frame directory", {
2933
3029
  frameDir,
2934
- rawPath,
2935
3030
  error: err instanceof Error ? err.message : String(err)
2936
3031
  });
2937
3032
  }
2938
3033
  }
2939
3034
  function cleanupHdrVideoFrameSource(source, log) {
2940
3035
  closeHdrVideoFrameSource(source, log);
2941
- cleanupHdrFrameDirectory(source.dir, source.rawPath, log);
3036
+ cleanupHdrFrameDirectory(source.dir, log);
2942
3037
  }
2943
- function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2944
- const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
3038
+ function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
2945
3039
  const configuredBudget = resolveHdrExtractionBudgetBytes(
2946
3040
  process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
2947
3041
  );
@@ -2975,14 +3069,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2975
3069
  budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
2976
3070
  };
2977
3071
  }
3072
+ var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
3073
+ function hdrWindowFrames(frameSize, windowBytes) {
3074
+ return Math.max(8, Math.floor(windowBytes / frameSize));
3075
+ }
3076
+ async function probeFirstTick(srcPath, mediaStart, fps, runFfmpegImpl, abortSignal) {
3077
+ const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
3078
+ const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
3079
+ signal: abortSignal
3080
+ });
3081
+ const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
3082
+ return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
3083
+ }
2978
3084
  async function extractHdrVideoFrames(args) {
2979
3085
  const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
3086
+ const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
3087
+ const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
2980
3088
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
2981
3089
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
2982
3090
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3091
+ const fps = fpsToNumber4(job.config.fps);
2983
3092
  const out = /* @__PURE__ */ new Map();
2984
3093
  mkdirSync2(framesDir, { recursive: true });
2985
- const plannedVideos = [];
3094
+ let scratchBytes = 0;
2986
3095
  const extractionWindows = /* @__PURE__ */ new Map();
2987
3096
  for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
2988
3097
  const video = composition.videos.find((v) => v.id === videoId);
@@ -3003,16 +3112,13 @@ async function extractHdrVideoFrames(args) {
3003
3112
  videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
3004
3113
  });
3005
3114
  prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
3006
- plannedVideos.push({
3007
- durationSeconds: window2.durationSeconds,
3008
- width: dims.width,
3009
- height: dims.height
3010
- });
3115
+ const frameSize = dims.width * dims.height * 6;
3116
+ const frames = window2.finalFrameOnly ? 1 : Math.ceil(window2.durationSeconds * fps);
3117
+ scratchBytes += Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
3011
3118
  }
3012
3119
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
3013
3120
  framesDir,
3014
- plannedVideos,
3015
- fpsToNumber4(job.config.fps),
3121
+ scratchBytes,
3016
3122
  log
3017
3123
  );
3018
3124
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3024,79 +3130,79 @@ async function extractHdrVideoFrames(args) {
3024
3130
  if (!video || !planned) continue;
3025
3131
  const { window: window2 } = planned;
3026
3132
  mkdirSync2(framesDir, { recursive: true });
3027
- const frameDir = mkdtempSync2(join7(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3133
+ const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3028
3134
  createdFrameDirs.add(frameDir);
3029
3135
  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
3136
  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.`
3137
+ const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3138
+ const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
3139
+ const firstTick = window2.finalFrameOnly ? 0 : await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
3140
+ const totalFrames = window2.finalFrameOnly ? 1 : Math.max(1, Math.ceil(window2.durationSeconds * fps - firstTick - 1e-9)) + Number(video.loop);
3141
+ const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
3142
+ const windows = createHdrFrameWindows({
3143
+ dir: frameDir,
3144
+ frameSize,
3145
+ totalFrames,
3146
+ windowFrames,
3147
+ maxResident,
3148
+ extract: async (spec) => {
3149
+ const ffmpegArgs = window2.finalFrameOnly ? (
3150
+ // Decode before seeking for the one-frame path. Input-side seeking can
3151
+ // return zero frames for valid unindexed/negative-base transports.
3152
+ ["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
3153
+ ) : hdrWindowFfmpegArgs({
3154
+ srcPath,
3155
+ mediaStart: extractionMediaStart,
3156
+ fps,
3157
+ fpsArg: fpsToFfmpegArg(job.config.fps),
3158
+ firstTick,
3159
+ firstFrame: spec.firstFrame,
3160
+ frames: spec.frames,
3161
+ scale
3162
+ });
3163
+ const result = await runFfmpegImpl(
3164
+ [...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
3165
+ { signal: abortSignal }
3081
3166
  );
3167
+ if (!result.success) {
3168
+ hdrDiagnostics.videoExtractionFailures += 1;
3169
+ log.error("HDR frame extraction failed; aborting render", {
3170
+ videoId,
3171
+ srcPath,
3172
+ firstFrame: spec.firstFrame,
3173
+ stderr: result.stderr.slice(-400)
3174
+ });
3175
+ throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3176
+ error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3177
+ failureReason: result.failureReason
3178
+ });
3179
+ }
3180
+ return Math.min(Math.floor(statSync2(spec.path).size / frameSize), spec.frames);
3082
3181
  }
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);
3182
+ });
3183
+ const first = await windows.lease(0);
3184
+ if (!first) {
3185
+ hdrDiagnostics.videoExtractionFailures += 1;
3186
+ throw new Error(
3187
+ `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
3188
+ );
3099
3189
  }
3190
+ first.release();
3191
+ if (video.loop) (await windows.lease(totalFrames - 1))?.release();
3192
+ out.set(videoId, {
3193
+ dir: frameDir,
3194
+ windows,
3195
+ width: dims.width,
3196
+ height: dims.height,
3197
+ frameSize,
3198
+ get frameCount() {
3199
+ return windows.totalFrames;
3200
+ },
3201
+ scratch: Buffer.allocUnsafe(frameSize),
3202
+ loop: video.loop,
3203
+ mediaStart: extractionMediaStart,
3204
+ videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3205
+ });
3100
3206
  }
3101
3207
  return { sources: out, estimatedBytes, releaseReservation };
3102
3208
  } catch (error) {
@@ -3105,12 +3211,23 @@ async function extractHdrVideoFrames(args) {
3105
3211
  createdFrameDirs.delete(source.dir);
3106
3212
  }
3107
3213
  for (const frameDir of createdFrameDirs) {
3108
- cleanupHdrFrameDirectory(frameDir, void 0, log);
3214
+ cleanupHdrFrameDirectory(frameDir, log);
3109
3215
  }
3110
3216
  releaseReservation();
3111
3217
  throw error;
3112
3218
  }
3113
3219
  }
3220
+ function hdrWindowFfmpegArgs(a) {
3221
+ const unit = Math.ceil(a.fps);
3222
+ const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
3223
+ const trim = a.firstFrame - seekFrame;
3224
+ const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
3225
+ if (trim > 0) filters.push(`trim=start_frame=${trim}`);
3226
+ return [
3227
+ ...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
3228
+ ...["-vf", filters.join(","), "-frames:v", String(a.frames)]
3229
+ ];
3230
+ }
3114
3231
  function decodeHdrImageBuffers(args) {
3115
3232
  const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
3116
3233
  const out = /* @__PURE__ */ new Map();
@@ -3130,7 +3247,11 @@ function decodeHdrImageBuffers(args) {
3130
3247
  fit,
3131
3248
  fitInfo?.position
3132
3249
  );
3133
- out.set(imageId, { data: resampled, width: layout.width, height: layout.height });
3250
+ out.set(imageId, {
3251
+ data: resampled,
3252
+ width: layout.width,
3253
+ height: layout.height
3254
+ });
3134
3255
  } else {
3135
3256
  out.set(imageId, {
3136
3257
  data: Buffer.from(decoded.data),
@@ -3242,7 +3363,7 @@ async function captureSceneIntoBuffer(a) {
3242
3363
  hdrPerf
3243
3364
  );
3244
3365
  } else {
3245
- blitHdrVideoLayer(
3366
+ await blitHdrVideoLayer(
3246
3367
  sceneBuf,
3247
3368
  el,
3248
3369
  time,
@@ -3444,7 +3565,7 @@ function isTargetLossError(error) {
3444
3565
  // src/services/render/stages/captureStreamingStage.ts
3445
3566
  import { mkdtemp, writeFile } from "node:fs/promises";
3446
3567
  import { tmpdir } from "node:os";
3447
- import { join as join8 } from "node:path";
3568
+ import { join as join9 } from "node:path";
3448
3569
  import {
3449
3570
  DrawElementVerificationError,
3450
3571
  captureFrameToBuffer,
@@ -3606,11 +3727,11 @@ function createDrainFrameGuard(args) {
3606
3727
  return buf;
3607
3728
  }
3608
3729
  if (db < verifyMinDb) {
3609
- const dumpDir = await mkdtemp(join8(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3730
+ const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3610
3731
  if (dumpDir) {
3611
3732
  await Promise.all([
3612
- writeFile(join8(dumpDir, `frame-${idx}-de.jpg`), buf),
3613
- writeFile(join8(dumpDir, `frame-${idx}-truth.jpg`), truth)
3733
+ writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3734
+ writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3614
3735
  ]).catch(() => {
3615
3736
  });
3616
3737
  }
@@ -4059,7 +4180,7 @@ async function runCaptureStreamingStage(input) {
4059
4180
 
4060
4181
  // src/services/render/stages/captureSegmentedStage.ts
4061
4182
  function segmentOutputPath(segmentDir, index) {
4062
- return join9(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4183
+ return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4063
4184
  }
4064
4185
  function segmentEncoderOptions(base, segment) {
4065
4186
  return {
@@ -4135,7 +4256,7 @@ async function captureSegment(ctx, segment) {
4135
4256
  path: segmentPath,
4136
4257
  // Recorded so resume can prove the file on disk is the one that
4137
4258
  // finished; absent only in unit tests, whose fake encoder writes none.
4138
- bytes: existsSync6(segmentPath) ? statSync2(segmentPath).size : 0
4259
+ bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4139
4260
  });
4140
4261
  return encodeResult.durationMs;
4141
4262
  } finally {
@@ -4153,7 +4274,7 @@ var DEFAULT_DEPS = {
4153
4274
  // is invisible to a test that swaps the engine module after this file has
4154
4275
  // been evaluated (bun updates live bindings, not copies).
4155
4276
  closeSession: (session) => closeCaptureSession2(session),
4156
- removeFile: (path) => rmSync5(path, { force: true })
4277
+ removeFile: (path) => rmSync6(path, { force: true })
4157
4278
  };
4158
4279
  function defaultSessionFactory(input) {
4159
4280
  let probeSession = input.probeSession;
@@ -4320,7 +4441,7 @@ async function runCaptureSegmentedStage(input) {
4320
4441
  const { log, dedupPerfs, assertNotAborted } = input;
4321
4442
  const deps = { ...DEFAULT_DEPS, ...input.deps };
4322
4443
  const segments = planSegments(input.totalFrames, input.segmentFrames);
4323
- const segmentDir = input.segmentDir ?? join9(input.workDir, "segments");
4444
+ const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
4324
4445
  mkdirSync3(segmentDir, { recursive: true });
4325
4446
  const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
4326
4447
  const pending = segments.filter((s) => !skip.has(s.index));
@@ -4391,10 +4512,83 @@ async function runCaptureSegmentedStage(input) {
4391
4512
  };
4392
4513
  }
4393
4514
 
4515
+ // src/services/render/renderEtaGuard.ts
4516
+ var RENDER_TOO_SLOW = "RENDER_TOO_SLOW";
4517
+ var ETA_MARGIN = 1.2;
4518
+ var MIN_SAMPLE_MS = 6e4;
4519
+ var MIN_SAMPLE_FRAMES = 3;
4520
+ var POLL_MS = 5e3;
4521
+ var RenderTooSlowError = class extends Error {
4522
+ code = RENDER_TOO_SLOW;
4523
+ // The producer server reads these off the error for the wire envelope.
4524
+ // fallow-ignore-next-line unused-class-member
4525
+ owner = "user";
4526
+ // fallow-ignore-next-line unused-class-member
4527
+ retryable = false;
4528
+ publicMetadata;
4529
+ constructor(stage, input) {
4530
+ const { framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs } = input;
4531
+ super(
4532
+ `render too slow: stage ${stage}, ${framesCompleted}/${totalFrames} frames, ${Math.round(msPerFrame)} ms/frame, ETA ${etaMs} ms exceeds maxRenderMs ${maxRenderMs}`
4533
+ );
4534
+ this.name = "RenderTooSlowError";
4535
+ this.publicMetadata = { stage, framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs };
4536
+ }
4537
+ };
4538
+ function checkRenderEta(stage, input) {
4539
+ const { maxRenderMs, elapsedMs, framesCompleted, totalFrames, msPerFrame } = input;
4540
+ if (!maxRenderMs || maxRenderMs <= 0 || !(msPerFrame > 0)) return void 0;
4541
+ const etaMs = Math.round(elapsedMs + (totalFrames - framesCompleted) * msPerFrame);
4542
+ if (etaMs <= maxRenderMs * ETA_MARGIN) return void 0;
4543
+ return new RenderTooSlowError(stage, { ...input, maxRenderMs, etaMs });
4544
+ }
4545
+ function createRateSampler() {
4546
+ let base;
4547
+ let lastFrames = 0;
4548
+ return {
4549
+ observe(done, at) {
4550
+ if (done === 0 || done < lastFrames) base = void 0;
4551
+ lastFrames = done;
4552
+ if (done === 0) return void 0;
4553
+ base ??= { at, frames: done };
4554
+ const sampleMs = at - base.at;
4555
+ const sampleFrames = done - base.frames;
4556
+ if (sampleMs < MIN_SAMPLE_MS || sampleFrames < MIN_SAMPLE_FRAMES) return void 0;
4557
+ return sampleMs / sampleFrames;
4558
+ }
4559
+ };
4560
+ }
4561
+ function startRenderEtaGuard(options) {
4562
+ const { maxRenderMs, job, totalFrames, pipelineStartMs, onTooSlow, now = Date.now } = options;
4563
+ if (!maxRenderMs || maxRenderMs <= 0) return () => {
4564
+ };
4565
+ const sampler = createRateSampler();
4566
+ const stop = () => clearInterval(timer);
4567
+ const timer = setInterval(() => {
4568
+ const done = job.framesRendered ?? 0;
4569
+ if (done >= totalFrames) return stop();
4570
+ const at = now();
4571
+ const msPerFrame = sampler.observe(done, at);
4572
+ if (msPerFrame === void 0) return;
4573
+ const error = checkRenderEta("capture", {
4574
+ maxRenderMs,
4575
+ elapsedMs: at - pipelineStartMs,
4576
+ framesCompleted: done,
4577
+ totalFrames,
4578
+ msPerFrame
4579
+ });
4580
+ if (!error) return;
4581
+ stop();
4582
+ onTooSlow(error);
4583
+ }, options.pollMs ?? POLL_MS);
4584
+ timer.unref?.();
4585
+ return stop;
4586
+ }
4587
+
4394
4588
  // src/services/render/segmentManifest.ts
4395
4589
  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";
4590
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
4591
+ import { join as join11 } from "node:path";
4398
4592
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4399
4593
  var MANIFEST_FILENAME = "segments.json";
4400
4594
  function computeSegmentPlanHash(input) {
@@ -4420,7 +4614,7 @@ function computeSegmentPlanHash(input) {
4420
4614
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4421
4615
  }
4422
4616
  function segmentDirFor(projectRendersDir, planHash) {
4423
- return join10(projectRendersDir, ".hf-segments", planHash);
4617
+ return join11(projectRendersDir, ".hf-segments", planHash);
4424
4618
  }
4425
4619
  function hasNumber(value, key) {
4426
4620
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -4438,7 +4632,7 @@ function isManifest(value) {
4438
4632
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
4439
4633
  }
4440
4634
  function readSegmentManifest(segmentDir) {
4441
- const path = join10(segmentDir, MANIFEST_FILENAME);
4635
+ const path = join11(segmentDir, MANIFEST_FILENAME);
4442
4636
  if (!existsSync7(path)) return null;
4443
4637
  try {
4444
4638
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -4449,9 +4643,9 @@ function readSegmentManifest(segmentDir) {
4449
4643
  }
4450
4644
  function writeSegmentManifest(segmentDir, manifest) {
4451
4645
  mkdirSync4(segmentDir, { recursive: true });
4452
- const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4646
+ const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4453
4647
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4454
- renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4648
+ renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
4455
4649
  }
4456
4650
  async function probeSegmentFrameCount(path, probe) {
4457
4651
  try {
@@ -4469,7 +4663,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
4469
4663
  if (manifest.planHash !== expectedHash) return ok;
4470
4664
  for (const entry of manifest.completed) {
4471
4665
  if (!existsSync7(entry.path)) continue;
4472
- const size = statSync3(entry.path).size;
4666
+ const size = statSync4(entry.path).size;
4473
4667
  if (size <= 0 || size !== entry.bytes) continue;
4474
4668
  const frames = await probeFrames(entry.path);
4475
4669
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -4563,7 +4757,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4563
4757
  }
4564
4758
 
4565
4759
  // src/services/render/captureCost.ts
4566
- import { join as join11 } from "node:path";
4760
+ import { join as join12 } from "node:path";
4567
4761
  import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4568
4762
  import {
4569
4763
  calculateOptimalWorkers,
@@ -4808,7 +5002,7 @@ async function runCaptureCalibration(input) {
4808
5002
  });
4809
5003
  let calibration;
4810
5004
  try {
4811
- calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
5005
+ calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
4812
5006
  } catch (error) {
4813
5007
  const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
4814
5008
  if (!shouldFallback) {
@@ -4841,7 +5035,7 @@ async function runCaptureCalibration(input) {
4841
5035
  const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
4842
5036
  try {
4843
5037
  calibration = await runOneCalibration(
4844
- join11(workDir, "capture-calibration-screenshot"),
5038
+ join12(workDir, "capture-calibration-screenshot"),
4845
5039
  screenshotCfg
4846
5040
  );
4847
5041
  } catch (fallbackError) {
@@ -5273,14 +5467,14 @@ function countAuthoredTimedClips(html) {
5273
5467
  }
5274
5468
 
5275
5469
  // src/services/render/stages/compileStage.ts
5276
- import { join as join14 } from "node:path";
5470
+ import { join as join15 } from "node:path";
5277
5471
  import { suggestMatchingPreset } from "@hyperframes/core";
5278
5472
 
5279
5473
  // src/services/htmlCompiler.ts
5280
5474
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5281
5475
  import { createHash as createHash5 } from "node:crypto";
5282
5476
  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";
5477
+ import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5284
5478
  import { parseHTML as parseHTML4 } from "linkedom";
5285
5479
  import {
5286
5480
  COMPOSITION_SOURCE_URL,
@@ -5507,8 +5701,8 @@ import {
5507
5701
 
5508
5702
  // src/services/animatedGifPrep.ts
5509
5703
  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";
5704
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
5705
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
5512
5706
  import { parseHTML as parseHTML3 } from "linkedom";
5513
5707
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5514
5708
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -5557,7 +5751,7 @@ function resolveGifSourcePath(src, options) {
5557
5751
  }
5558
5752
  function isUsableFile(path) {
5559
5753
  try {
5560
- const stat = statSync4(path);
5754
+ const stat = statSync5(path);
5561
5755
  return stat.isFile() && stat.size > 0;
5562
5756
  } catch {
5563
5757
  return false;
@@ -5692,10 +5886,10 @@ async function ensurePreparedWebm(input) {
5692
5886
  if (!isUsableFile(input.cachePath)) {
5693
5887
  renameWithRetry3(tmpPath, input.cachePath);
5694
5888
  } else {
5695
- rmSync6(tmpPath, { force: true });
5889
+ rmSync7(tmpPath, { force: true });
5696
5890
  }
5697
5891
  } catch (error) {
5698
- rmSync6(tmpPath, { force: true });
5892
+ rmSync7(tmpPath, { force: true });
5699
5893
  throw error;
5700
5894
  }
5701
5895
  }
@@ -5751,7 +5945,7 @@ function replaceImageWithVideo(input) {
5751
5945
  return video;
5752
5946
  }
5753
5947
  async function prepareAnimatedGifInputs(html, options) {
5754
- const outputDir = options.outputDir ?? join12(options.downloadDir, PREPARED_GIF_SUBDIR);
5948
+ const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
5755
5949
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
5756
5950
  const cacheDir = options.cacheDir ?? outputDir;
5757
5951
  const { document: document2 } = parseHTML3(html);
@@ -5773,8 +5967,8 @@ async function prepareAnimatedGifInputs(html, options) {
5773
5967
  const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
5774
5968
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
5775
5969
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
5776
- const cachePath = join12(cacheDir, filename);
5777
- const outputPath = join12(outputDir, filename);
5970
+ const cachePath = join13(cacheDir, filename);
5971
+ const outputPath = join13(outputDir, filename);
5778
5972
  const outputSrc = `${outputSrcPrefix}/${filename}`;
5779
5973
  await ensurePreparedWebm({
5780
5974
  sourcePath,
@@ -6188,7 +6382,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6188
6382
  }
6189
6383
  } else {
6190
6384
  filePath = decodedUrlPath(src);
6191
- if (!filePath.startsWith("/")) filePath = join13(baseDir, filePath);
6385
+ if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6192
6386
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6193
6387
  }
6194
6388
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6808,7 +7002,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
6808
7002
  return downloadAndRewriteUrls(
6809
7003
  urlSet,
6810
7004
  html,
6811
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7005
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6812
7006
  "Remote media download failed",
6813
7007
  "Localized remote media source(s)"
6814
7008
  );
@@ -6823,7 +7017,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
6823
7017
  return downloadAndRewriteUrls(
6824
7018
  urlSet,
6825
7019
  html,
6826
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7020
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6827
7021
  "Remote image download failed",
6828
7022
  "Localized remote image source(s)"
6829
7023
  );
@@ -6839,7 +7033,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
6839
7033
  return downloadAndRewriteUrls(
6840
7034
  urlSet,
6841
7035
  html,
6842
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7036
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6843
7037
  "Remote background-image download failed",
6844
7038
  "Localized remote background-image(s)",
6845
7039
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -6974,7 +7168,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
6974
7168
  return downloadAndRewriteUrls(
6975
7169
  urlSet,
6976
7170
  processed,
6977
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7171
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6978
7172
  "Remote font download failed",
6979
7173
  "Localized remote font face(s)",
6980
7174
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7641,7 +7835,7 @@ async function runCompileStage(input) {
7641
7835
  abortSignal
7642
7836
  } = input;
7643
7837
  const compileStart = Date.now();
7644
- const compiled = await compileForRender(projectDir, htmlPath, join14(workDir, "downloads"), {
7838
+ const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
7645
7839
  log,
7646
7840
  failClosedFontFetch: failClosedFontFetch === true,
7647
7841
  allowSystemFontCapture,
@@ -7649,7 +7843,7 @@ async function runCompileStage(input) {
7649
7843
  variables: input.variables,
7650
7844
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7651
7845
  pageMotionBlur: input.pageMotionBlur,
7652
- animatedGifCacheDir: cfg.extractCacheDir ? join14(cfg.extractCacheDir, "animated-gif") : void 0,
7846
+ animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
7653
7847
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7654
7848
  });
7655
7849
  assertNotAborted();
@@ -7712,7 +7906,7 @@ async function runCompileStage(input) {
7712
7906
  };
7713
7907
  await preflightCompositionAssetMediaTypes({
7714
7908
  projectDir,
7715
- compiledDir: join14(workDir, "compiled"),
7909
+ compiledDir: join15(workDir, "compiled"),
7716
7910
  composition,
7717
7911
  signal: abortSignal
7718
7912
  });
@@ -7757,7 +7951,7 @@ async function runCompileStage(input) {
7757
7951
  }
7758
7952
 
7759
7953
  // src/services/render/stages/probeStage.ts
7760
- import { join as join15 } from "node:path";
7954
+ import { join as join16 } from "node:path";
7761
7955
  import { parseHTML as parseHTML5 } from "linkedom";
7762
7956
  import {
7763
7957
  closeCaptureSession as closeCaptureSession4,
@@ -7949,7 +8143,7 @@ async function runProbeStage(input) {
7949
8143
  });
7950
8144
  fileServer = await createFileServer({
7951
8145
  projectDir,
7952
- compiledDir: join15(workDir, "compiled"),
8146
+ compiledDir: join16(workDir, "compiled"),
7953
8147
  port: 0,
7954
8148
  preHeadScripts: [VIRTUAL_TIME_SHIM],
7955
8149
  fps: job.config.fps
@@ -7977,7 +8171,7 @@ async function runProbeStage(input) {
7977
8171
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
7978
8172
  probeSession = await createCaptureSession4(
7979
8173
  fileServer.url,
7980
- join15(workDir, "probe"),
8174
+ join16(workDir, "probe"),
7981
8175
  captureOpts,
7982
8176
  null,
7983
8177
  probeCfg
@@ -8062,7 +8256,7 @@ async function runProbeStage(input) {
8062
8256
  });
8063
8257
  probeSession = await createCaptureSession4(
8064
8258
  fileServer.url,
8065
- join15(workDir, "probe-screenshot"),
8259
+ join16(workDir, "probe-screenshot"),
8066
8260
  captureOpts,
8067
8261
  null,
8068
8262
  { ...probeCfg, forceScreenshot: true }
@@ -8098,7 +8292,7 @@ async function runProbeStage(input) {
8098
8292
  compiled,
8099
8293
  resolutions,
8100
8294
  projectDir,
8101
- join15(workDir, "downloads")
8295
+ join16(workDir, "downloads")
8102
8296
  );
8103
8297
  assertNotAborted();
8104
8298
  composition.videos = compiled.videos;
@@ -8289,7 +8483,7 @@ async function runProbeStage(input) {
8289
8483
  }
8290
8484
  await preflightCompositionAssetMediaTypes({
8291
8485
  projectDir,
8292
- compiledDir: join15(workDir, "compiled"),
8486
+ compiledDir: join16(workDir, "compiled"),
8293
8487
  composition,
8294
8488
  signal: abortSignal
8295
8489
  });
@@ -8423,7 +8617,7 @@ function validateRenderDuration(input) {
8423
8617
 
8424
8618
  // src/services/render/stages/extractVideosStage.ts
8425
8619
  import { existsSync as existsSync11 } from "node:fs";
8426
- import { join as join16 } from "node:path";
8620
+ import { join as join17 } from "node:path";
8427
8621
  import {
8428
8622
  classifyVideoExtractionError,
8429
8623
  createFrameLookupTable,
@@ -8738,14 +8932,14 @@ async function runExtractVideosStage(input) {
8738
8932
  // because short boundary counts can differ by one frame.
8739
8933
  {
8740
8934
  fps: job.config.fps,
8741
- outputDir: join16(compiledDir, "__hyperframes_video_frames"),
8935
+ outputDir: join17(compiledDir, "__hyperframes_video_frames"),
8742
8936
  format: job.config.videoFrameFormat ?? "auto",
8743
8937
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
8744
8938
  timelineEnd: composition.duration,
8745
8939
  maxTransientRetries: extractionPolicy.maxTransientRetries,
8746
8940
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
8747
8941
  deferRangeExtraction,
8748
- contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)],
8942
+ contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
8749
8943
  urlByAbsentLocalPath
8750
8944
  },
8751
8945
  abortSignal,
@@ -8812,8 +9006,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8812
9006
  }
8813
9007
 
8814
9008
  // src/services/render/stages/audioStage.ts
8815
- import { join as join18 } from "node:path";
8816
- import { rmSync as rmSync8 } from "node:fs";
9009
+ import { join as join19 } from "node:path";
9010
+ import { rmSync as rmSync9 } from "node:fs";
8817
9011
  import {
8818
9012
  MIXED_AUDIO_FILENAME,
8819
9013
  audioFailureDiagnostics,
@@ -8822,8 +9016,8 @@ import {
8822
9016
 
8823
9017
  // src/services/render/audioPadTrim.ts
8824
9018
  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";
9019
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
9020
+ import { dirname as dirname7, join as join18 } from "node:path";
8827
9021
  import {
8828
9022
  DEFAULT_CONFIG,
8829
9023
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9026,7 +9220,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9026
9220
  error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
9027
9221
  };
9028
9222
  } finally {
9029
- for (const path of plan.cleanupPaths) rmSync7(path, { force: true });
9223
+ for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
9030
9224
  }
9031
9225
  let loweredDb = 0;
9032
9226
  if (probeTruePeak) {
@@ -9072,8 +9266,8 @@ async function constrainAacTruePeak(input) {
9072
9266
  async function enforceAacTruePeak(input) {
9073
9267
  let scratchDir;
9074
9268
  try {
9075
- scratchDir = mkdtempSync3(join17(dirname7(input.audioPath), ".true-peak-"));
9076
- const correctedPath = join17(scratchDir, "audio.m4a");
9269
+ scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9270
+ const correctedPath = join18(scratchDir, "audio.m4a");
9077
9271
  const measurements = [];
9078
9272
  let attenuationDb = 0;
9079
9273
  let measuredPath = input.audioPath;
@@ -9124,7 +9318,7 @@ async function enforceAacTruePeak(input) {
9124
9318
  )}`
9125
9319
  };
9126
9320
  } finally {
9127
- if (scratchDir) rmSync7(scratchDir, { recursive: true, force: true });
9321
+ if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9128
9322
  }
9129
9323
  }
9130
9324
  function failResult(outputPath, target, source, error) {
@@ -9271,7 +9465,7 @@ function assertAudioNotInterrupted(failures) {
9271
9465
  }
9272
9466
  async function runAudioStage(input) {
9273
9467
  const stageStart = Date.now();
9274
- const pcmSourcePath = join18(input.workDir, "audio-source.wav");
9468
+ const pcmSourcePath = join19(input.workDir, "audio-source.wav");
9275
9469
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9276
9470
  try {
9277
9471
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9291,7 +9485,7 @@ async function runAudioStage(input) {
9291
9485
  }
9292
9486
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9293
9487
  } finally {
9294
- rmSync8(pcmSourcePath, { force: true });
9488
+ rmSync9(pcmSourcePath, { force: true });
9295
9489
  }
9296
9490
  }
9297
9491
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9309,7 +9503,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9309
9503
  assertNotAborted,
9310
9504
  log
9311
9505
  } = input;
9312
- const audioOutputPath = join18(workDir, MIXED_AUDIO_FILENAME);
9506
+ const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
9313
9507
  let hasAudio = false;
9314
9508
  let audioError;
9315
9509
  let audioFailures;
@@ -9320,7 +9514,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9320
9514
  audioResult = await processCompositionAudio(
9321
9515
  audios,
9322
9516
  projectDir,
9323
- join18(workDir, "audio-work"),
9517
+ join19(workDir, "audio-work"),
9324
9518
  audioOutputPath,
9325
9519
  duration,
9326
9520
  abortSignal,
@@ -9389,7 +9583,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9389
9583
 
9390
9584
  // src/services/render/stages/captureStage.ts
9391
9585
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9392
- import { join as join19 } from "node:path";
9586
+ import { join as join20 } from "node:path";
9393
9587
  import {
9394
9588
  classifyCaptureFailure as classifyCaptureFailure2,
9395
9589
  frameFileExtension,
@@ -9462,13 +9656,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9462
9656
  if (!existsSync12(workDir)) return total;
9463
9657
  for (const attempt of readdirSync3(workDir)) {
9464
9658
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9465
- total += sampleDirectoryBytes(join19(workDir, attempt));
9659
+ total += sampleDirectoryBytes(join20(workDir, attempt));
9466
9660
  continue;
9467
9661
  }
9468
9662
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9469
- const attemptDir = join19(workDir, attempt);
9663
+ const attemptDir = join20(workDir, attempt);
9470
9664
  for (const name of readdirSync3(attemptDir)) {
9471
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join19(attemptDir, name));
9665
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
9472
9666
  }
9473
9667
  }
9474
9668
  return total;
@@ -9755,7 +9949,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
9755
9949
 
9756
9950
  // src/services/render/stages/captureHdrStage.ts
9757
9951
  import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
9758
- import { join as join23 } from "node:path";
9952
+ import { join as join24 } from "node:path";
9759
9953
  import {
9760
9954
  closeCaptureSession as closeCaptureSession7,
9761
9955
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -9827,7 +10021,7 @@ function createHdrImageTransferCache(options = {}) {
9827
10021
  }
9828
10022
 
9829
10023
  // src/services/render/stages/captureHdrSequentialLoop.ts
9830
- import { join as join20 } from "node:path";
10024
+ import { join as join21 } from "node:path";
9831
10025
  import {
9832
10026
  TRANSITIONS,
9833
10027
  crossfade
@@ -9952,7 +10146,7 @@ async function runSequentialLayeredFrameLoop(input) {
9952
10146
  );
9953
10147
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
9954
10148
  writeFileExclusiveSync(
9955
- join20(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10149
+ join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
9956
10150
  normalCanvas
9957
10151
  );
9958
10152
  }
@@ -9984,7 +10178,7 @@ async function runSequentialLayeredFrameLoop(input) {
9984
10178
  }
9985
10179
 
9986
10180
  // src/services/render/stages/captureHdrHybridLoop.ts
9987
- import { join as join22 } from "node:path";
10181
+ import { join as join23 } from "node:path";
9988
10182
  import {
9989
10183
  TRANSITIONS as TRANSITIONS2,
9990
10184
  closeCaptureSession as closeCaptureSession6,
@@ -9998,7 +10192,7 @@ import {
9998
10192
  // src/services/shaderTransitionWorkerPool.ts
9999
10193
  import { Worker } from "node:worker_threads";
10000
10194
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10001
- import { dirname as dirname8, join as join21 } from "node:path";
10195
+ import { dirname as dirname8, join as join22 } from "node:path";
10002
10196
  import { createRequire } from "node:module";
10003
10197
  import { existsSync as existsSync13 } from "node:fs";
10004
10198
  import { cpus as cpus2 } from "node:os";
@@ -10012,9 +10206,9 @@ function resolveWorkerEntry(explicit) {
10012
10206
  return { path: override, isTs };
10013
10207
  }
10014
10208
  const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10015
- const jsPath = join21(moduleDir, "shaderTransitionWorker.js");
10209
+ const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10016
10210
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10017
- const tsPath = join21(moduleDir, "shaderTransitionWorker.ts");
10211
+ const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10018
10212
  return { path: tsPath, isTs: true };
10019
10213
  }
10020
10214
  function buildExecArgv(entryIsTs) {
@@ -10382,7 +10576,7 @@ async function runHybridLayeredFrameLoop(input) {
10382
10576
  );
10383
10577
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10384
10578
  writeFileExclusiveSync(
10385
- join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10579
+ join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10386
10580
  canvas
10387
10581
  );
10388
10582
  }
@@ -10551,7 +10745,8 @@ async function runCaptureHdrStage(input) {
10551
10745
  width,
10552
10746
  height,
10553
10747
  abortSignal,
10554
- hdrDiagnostics
10748
+ hdrDiagnostics,
10749
+ maxResidentWindows: Math.max(1, workerCount) + 1
10555
10750
  });
10556
10751
  releaseHdrExtractionReservation = extracted.releaseReservation;
10557
10752
  for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
@@ -10569,7 +10764,7 @@ async function runCaptureHdrStage(input) {
10569
10764
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10570
10765
  }
10571
10766
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10572
- const debugDumpDir = debugDumpEnabled ? join23(framesDir, "debug-composite") : null;
10767
+ const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
10573
10768
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10574
10769
  mkdirSync7(debugDumpDir, { recursive: true });
10575
10770
  }
@@ -10729,12 +10924,12 @@ import {
10729
10924
  mkdirSync as mkdirSync8,
10730
10925
  readdirSync as readdirSync4,
10731
10926
  readFileSync as readFileSync7,
10732
- rmSync as rmSync9,
10733
- statSync as statSync5,
10927
+ rmSync as rmSync10,
10928
+ statSync as statSync6,
10734
10929
  writeFileSync as writeFileSync4
10735
10930
  } from "node:fs";
10736
10931
  import { readFile as readFile2 } from "node:fs/promises";
10737
- import { dirname as dirname9, join as join24 } from "node:path";
10932
+ import { dirname as dirname9, join as join25 } from "node:path";
10738
10933
  import {
10739
10934
  encodeFramesChunkedConcat,
10740
10935
  encodeFramesFromDir,
@@ -10859,7 +11054,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10859
11054
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
10860
11055
  if (failure) return failure;
10861
11056
  }
10862
- const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
11057
+ const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
10863
11058
  return {
10864
11059
  success: true,
10865
11060
  outputPath,
@@ -10868,7 +11063,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10868
11063
  fileSize
10869
11064
  };
10870
11065
  } finally {
10871
- rmSync9(input.palettePath, { force: true });
11066
+ rmSync10(input.palettePath, { force: true });
10872
11067
  }
10873
11068
  }
10874
11069
  function startEncodeProgress(job, stage, onProgress) {
@@ -10943,7 +11138,7 @@ function encodeWhileCapturing(input, totalFrames) {
10943
11138
  await new Promise((resolve10) => wake = resolve10);
10944
11139
  }
10945
11140
  if (signal.aborted) return;
10946
- const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
11141
+ const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
10947
11142
  const frame = await readLandedFrame(landed.get(next), mergedPath);
10948
11143
  landed.delete(next);
10949
11144
  if (!await stream.writeFrame(frame)) {
@@ -11026,11 +11221,11 @@ async function runEncodeStage(input) {
11026
11221
  );
11027
11222
  }
11028
11223
  captured.forEach((name, i) => {
11029
- const dst = join24(outputPath, formatExportFrameName(i, "png"));
11030
- copyFileSync3(join24(framesDir, name), dst);
11224
+ const dst = join25(outputPath, formatExportFrameName(i, "png"));
11225
+ copyFileSync3(join25(framesDir, name), dst);
11031
11226
  });
11032
11227
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11033
- copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
11228
+ copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11034
11229
  log.info(
11035
11230
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11036
11231
  );
@@ -11050,7 +11245,7 @@ async function runEncodeStage(input) {
11050
11245
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11051
11246
  fps: job.config.fps,
11052
11247
  loop,
11053
- palettePath: join24(dirname9(videoOnlyPath), "gif-palette.png"),
11248
+ palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11054
11249
  preserveAlpha: needsAlpha,
11055
11250
  signal: abortSignal,
11056
11251
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11263,9 +11458,9 @@ function sampleDirectoryBytes(dir) {
11263
11458
  continue;
11264
11459
  }
11265
11460
  for (const name of entries) {
11266
- const full = join25(current, name);
11461
+ const full = join26(current, name);
11267
11462
  try {
11268
- const st = statSync6(full);
11463
+ const st = statSync7(full);
11269
11464
  if (st.isDirectory()) {
11270
11465
  stack.push(full);
11271
11466
  } else if (st.isFile()) {
@@ -11412,8 +11607,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11412
11607
  const ranges = [];
11413
11608
  let rangeStart = null;
11414
11609
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11415
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11416
- const missing = !existsSync16(framePath) || statSync6(framePath).size <= 8;
11610
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11611
+ const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
11417
11612
  if (missing && rangeStart === null) {
11418
11613
  rangeStart = frameIndex;
11419
11614
  } else if (!missing && rangeStart !== null) {
@@ -11435,7 +11630,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
11435
11630
  workerId,
11436
11631
  startFrame: rangeStart + range.startFrame,
11437
11632
  endFrame: rangeStart + range.endFrame,
11438
- outputDir: join25(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11633
+ outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11439
11634
  outputFrameOffset: rangeStart
11440
11635
  }));
11441
11636
  batches.push(batch);
@@ -11463,8 +11658,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11463
11658
  return Math.max(1, Math.floor(currentWorkers / 2));
11464
11659
  }
11465
11660
  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}-`);
11661
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11662
+ return join26(dirname10(outputPath), `work-${jobId}-`);
11468
11663
  }
11469
11664
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11470
11665
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11496,7 +11691,7 @@ The composition is too large for the available memory. To reduce memory pressure
11496
11691
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11497
11692
  let captured = 0;
11498
11693
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11499
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11694
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11500
11695
  if (existsSync16(framePath)) captured++;
11501
11696
  }
11502
11697
  return captured;
@@ -11522,7 +11717,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11522
11717
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11523
11718
  });
11524
11719
  pendingTransientRetry = false;
11525
- const attemptWorkDir = join25(options.workDir, `capture-attempt-${attempt}`);
11720
+ const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
11526
11721
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11527
11722
  missingRanges,
11528
11723
  currentWorkers,
@@ -11979,7 +12174,7 @@ function resolveDeShortBand(args) {
11979
12174
  return args.bandOpen ? "applied" : "skipped_elements";
11980
12175
  }
11981
12176
  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;
12177
+ 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;
11983
12178
  }
11984
12179
  function isProbeDeGated(session) {
11985
12180
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
@@ -12115,17 +12310,17 @@ function deVerifyFallbackTelemetry(err) {
12115
12310
  }
12116
12311
  function resolveRenderDebugDir() {
12117
12312
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12118
- return join25(producerRoot, ".debug");
12313
+ return join26(producerRoot, ".debug");
12119
12314
  }
12120
12315
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12121
12316
  validateHlsRenderConfig(job.config);
12122
12317
  const debugDir = resolveRenderDebugDir();
12123
12318
  const outputDir = dirname10(outputPath);
12124
12319
  if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12125
- const workDir = job.config.debug ? join25(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12320
+ const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12126
12321
  const pipelineStart = Date.now();
12127
12322
  const baseLog = job.config.logger ?? defaultLogger;
12128
- const logPath = job.config.debug ? join25(workDir, "render.log") : null;
12323
+ const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12129
12324
  const execution = new RenderExecutionContext({
12130
12325
  request: { renderJobId: job.id, projectDir, outputPath },
12131
12326
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12139,7 +12334,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12139
12334
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12140
12335
  return;
12141
12336
  }
12142
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12337
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12143
12338
  });
12144
12339
  try {
12145
12340
  await executeRenderPipeline({
@@ -12170,7 +12365,9 @@ async function executeRenderPipeline(input) {
12170
12365
  const log = execution.logger;
12171
12366
  const eventPublisher = execution.events;
12172
12367
  const onProgress = execution.onProgress;
12173
- const executionSignal = execution.signal;
12368
+ const tooSlow = new AbortController();
12369
+ let tooSlowError;
12370
+ const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
12174
12371
  let fileServer = null;
12175
12372
  let probeSession = null;
12176
12373
  let lastBrowserConsole = [];
@@ -12183,7 +12380,7 @@ async function executeRenderPipeline(input) {
12183
12380
  imageDecodeFailures: 0
12184
12381
  };
12185
12382
  let hdrPerf;
12186
- const perfOutputPath = join25(workDir, "perf-summary.json");
12383
+ const perfOutputPath = join26(workDir, "perf-summary.json");
12187
12384
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12188
12385
  const observability = new RenderObservabilityRecorder({
12189
12386
  pipelineStartMs: pipelineStart,
@@ -12285,15 +12482,15 @@ async function executeRenderPipeline(input) {
12285
12482
  requestedWorkers: job.config.workers ?? "auto"
12286
12483
  });
12287
12484
  const entryFile = job.config.entryFile || "index.html";
12288
- let htmlPath = join25(projectDir, entryFile);
12485
+ let htmlPath = join26(projectDir, entryFile);
12289
12486
  if (!existsSync16(htmlPath)) {
12290
12487
  throw new Error(`Entry file not found: ${htmlPath}`);
12291
12488
  }
12292
12489
  assertNotAborted();
12293
12490
  const rawEntry = readFileSync8(htmlPath, "utf-8");
12294
12491
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12295
- const wrapperPath = join25(workDir, "standalone-entry.html");
12296
- const projectIndexPath = join25(projectDir, "index.html");
12492
+ const wrapperPath = join26(workDir, "standalone-entry.html");
12493
+ const projectIndexPath = join26(projectDir, "index.html");
12297
12494
  if (!existsSync16(projectIndexPath)) {
12298
12495
  throw new Error(
12299
12496
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -12456,7 +12653,7 @@ async function executeRenderPipeline(input) {
12456
12653
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12457
12654
  code: "extract_video"
12458
12655
  });
12459
- const compiledDir = join25(workDir, "compiled");
12656
+ const compiledDir = join26(workDir, "compiled");
12460
12657
  const extractResult = await observeRenderStage(
12461
12658
  observability,
12462
12659
  "video_extract",
@@ -12590,7 +12787,7 @@ async function executeRenderPipeline(input) {
12590
12787
  try {
12591
12788
  fileServer = await createFileServer({
12592
12789
  projectDir,
12593
- compiledDir: join25(workDir, "compiled"),
12790
+ compiledDir: join26(workDir, "compiled"),
12594
12791
  port: 0,
12595
12792
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12596
12793
  fps: job.config.fps
@@ -12608,7 +12805,7 @@ async function executeRenderPipeline(input) {
12608
12805
  if (!activeFileServer) {
12609
12806
  throw new Error("File server failed to initialize before frame capture");
12610
12807
  }
12611
- const framesDir = join25(workDir, "captured-frames");
12808
+ const framesDir = join26(workDir, "captured-frames");
12612
12809
  if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
12613
12810
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
12614
12811
  code: "start_browsers"
@@ -12712,7 +12909,6 @@ async function executeRenderPipeline(input) {
12712
12909
  const WOULD_RESOLVE_MULTI_WORKER = 2;
12713
12910
  const deInversionArgs = {
12714
12911
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12715
- requestedWorkers: job.config.workers,
12716
12912
  useDrawElement: cfg.useDrawElement,
12717
12913
  deCompileGate,
12718
12914
  forceScreenshot: captureForceScreenshot,
@@ -12918,7 +13114,7 @@ async function executeRenderPipeline(input) {
12918
13114
  } else if (deInversionEligible && workerCount > 1) {
12919
13115
  deWorkerInversion = "inverted";
12920
13116
  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.`
13117
+ `[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
13118
  );
12923
13119
  workerCount = 1;
12924
13120
  }
@@ -12962,6 +13158,27 @@ async function executeRenderPipeline(input) {
12962
13158
  deWorkerInversion: deWorkerInversion ?? "none",
12963
13159
  deParallelRouter: deParallelRouter ?? "none"
12964
13160
  });
13161
+ const calibratedTooSlow = checkRenderEta("capture_calibration", {
13162
+ maxRenderMs: job.config.maxRenderMs,
13163
+ elapsedMs: Date.now() - pipelineStart,
13164
+ framesCompleted: 0,
13165
+ totalFrames,
13166
+ msPerFrame: (captureCalibration?.estimate.p95Ms ?? 0) / workerCount
13167
+ });
13168
+ if (calibratedTooSlow) throw calibratedTooSlow;
13169
+ execution.defer(
13170
+ "stop render ETA guard",
13171
+ startRenderEtaGuard({
13172
+ maxRenderMs: job.config.maxRenderMs,
13173
+ job,
13174
+ totalFrames,
13175
+ pipelineStartMs: pipelineStart,
13176
+ onTooSlow: (error) => {
13177
+ tooSlowError = error;
13178
+ tooSlow.abort(error);
13179
+ }
13180
+ })
13181
+ );
12965
13182
  let useStreamingEncode = shouldUseStreamingEncode(
12966
13183
  cfg,
12967
13184
  outputFormat,
@@ -13055,7 +13272,7 @@ async function executeRenderPipeline(input) {
13055
13272
  hls: ".mp4"
13056
13273
  };
13057
13274
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13058
- const videoOnlyPath = join25(workDir, `video-only${videoExt}`);
13275
+ const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13059
13276
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13060
13277
  if (usePageSideCompositingForTransitions) {
13061
13278
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13301,7 +13518,7 @@ async function executeRenderPipeline(input) {
13301
13518
  pageMotionBlur: job.config.pageMotionBlur,
13302
13519
  captureQuality: captureOptions.quality
13303
13520
  });
13304
- const segmentDir = segmentDirFor(join25(projectDir, "renders"), segmentPlanHash);
13521
+ const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
13305
13522
  let completedSegments = /* @__PURE__ */ new Set();
13306
13523
  if (job.config.resumeSegments === true) {
13307
13524
  const existing = readSegmentManifest(segmentDir);
@@ -13316,10 +13533,10 @@ async function executeRenderPipeline(input) {
13316
13533
  });
13317
13534
  }
13318
13535
  if (completedSegments.size === 0) {
13319
- rmSync10(segmentDir, { recursive: true, force: true });
13536
+ rmSync11(segmentDir, { recursive: true, force: true });
13320
13537
  }
13321
13538
  } else {
13322
- rmSync10(segmentDir, { recursive: true, force: true });
13539
+ rmSync11(segmentDir, { recursive: true, force: true });
13323
13540
  }
13324
13541
  const segmentManifest = readSegmentManifest(segmentDir) ?? {
13325
13542
  version: 1,
@@ -13400,7 +13617,7 @@ async function executeRenderPipeline(input) {
13400
13617
  );
13401
13618
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13402
13619
  if (job.config.keepSegments !== true) {
13403
- rmSync10(segmentDir, { recursive: true, force: true });
13620
+ rmSync11(segmentDir, { recursive: true, force: true });
13404
13621
  }
13405
13622
  } else {
13406
13623
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -13669,7 +13886,7 @@ async function executeRenderPipeline(input) {
13669
13886
  "capture_disk",
13670
13887
  "drawElement capture failed; retrying with a fresh screenshot session"
13671
13888
  );
13672
- rmSync10(framesDir, { recursive: true, force: true });
13889
+ rmSync11(framesDir, { recursive: true, force: true });
13673
13890
  mkdirSync9(framesDir, { recursive: true });
13674
13891
  resetCaptureAttemptProgress(job);
13675
13892
  dedupPerfs.length = 0;
@@ -13856,7 +14073,7 @@ async function executeRenderPipeline(input) {
13856
14073
  }
13857
14074
  if (job.config.debug) {
13858
14075
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
13859
- const debugOutput = join25(workDir, `output${videoExt}`);
14076
+ const debugOutput = join26(workDir, `output${videoExt}`);
13860
14077
  copyFileSync4(stagedOutputPath, debugOutput);
13861
14078
  }
13862
14079
  }
@@ -13867,8 +14084,9 @@ async function executeRenderPipeline(input) {
13867
14084
  job.outputPath = outputPath;
13868
14085
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
13869
14086
  await eventPublisher.flush();
13870
- } catch (error) {
13871
- if (error instanceof RenderCancelledError || executionSignal?.aborted) {
14087
+ } catch (caught) {
14088
+ const error = tooSlowError ?? caught;
14089
+ if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
13872
14090
  job.error = error instanceof Error ? error.message : "render_cancelled";
13873
14091
  updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
13874
14092
  await eventPublisher.flush();
@@ -13962,13 +14180,13 @@ async function executeRenderPipeline(input) {
13962
14180
  import {
13963
14181
  existsSync as existsSync17,
13964
14182
  readFileSync as readFileSync9,
13965
- statSync as statSync7,
14183
+ statSync as statSync8,
13966
14184
  openSync as openSync4,
13967
- fstatSync as fstatSync4,
13968
- closeSync as closeSync5,
14185
+ fstatSync as fstatSync3,
14186
+ closeSync as closeSync4,
13969
14187
  constants as constants3
13970
14188
  } from "node:fs";
13971
- import { resolve as resolve8, join as join26, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
14189
+ import { resolve as resolve8, join as join27, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
13972
14190
  import { lintHyperframeHtml } from "@hyperframes/lint";
13973
14191
  function isStringRecord(value) {
13974
14192
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -14002,19 +14220,19 @@ function readEntryHtml(filePath) {
14002
14220
  } catch (error) {
14003
14221
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
14004
14222
  return null;
14005
- if (!statSync7(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14223
+ if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14006
14224
  throw error;
14007
14225
  }
14008
14226
  try {
14009
- if (!fstatSync4(fd).isFile()) return null;
14227
+ if (!fstatSync3(fd).isFile()) return null;
14010
14228
  return readFileSync9(fd, "utf-8");
14011
14229
  } finally {
14012
- closeSync5(fd);
14230
+ closeSync4(fd);
14013
14231
  }
14014
14232
  }
14015
14233
  function readProjectEntryFile(projectDir, preferredEntryFile) {
14016
14234
  const absProjectDir = resolve8(projectDir);
14017
- if (!existsSync17(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
14235
+ if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14018
14236
  return { error: `Project directory not found: ${absProjectDir}` };
14019
14237
  }
14020
14238
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -14036,7 +14254,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
14036
14254
  }
14037
14255
  }
14038
14256
  return {
14039
- error: `No HTML entry file found in project directory: ${join26(absProjectDir, preferredEntryFile || "index.html")}`
14257
+ error: `No HTML entry file found in project directory: ${join27(absProjectDir, preferredEntryFile || "index.html")}`
14040
14258
  };
14041
14259
  }
14042
14260
  function prepareHyperframeLintBody(body) {
@@ -14082,7 +14300,7 @@ async function runHyperframeLint(prepared) {
14082
14300
  // src/services/healthWorker.ts
14083
14301
  import { Worker as Worker2 } from "node:worker_threads";
14084
14302
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14085
- import { dirname as dirname11, join as join27 } from "node:path";
14303
+ import { dirname as dirname11, join as join28 } from "node:path";
14086
14304
  import { existsSync as existsSync18 } from "node:fs";
14087
14305
  var DEFAULT_HEALTH_PORT = 9848;
14088
14306
  async function startHealthWorker(options = {}) {
@@ -14158,7 +14376,7 @@ function defaultLogger2() {
14158
14376
  }
14159
14377
  function resolveWorkerEntry2() {
14160
14378
  const here = dirname11(fileURLToPath5(import.meta.url));
14161
- const candidates = [join27(here, "healthWorkerThread.js"), join27(here, "healthWorkerThread.ts")];
14379
+ const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
14162
14380
  for (const candidate of candidates) {
14163
14381
  if (existsSync18(candidate)) return candidate;
14164
14382
  }
@@ -14194,7 +14412,7 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
14194
14412
 
14195
14413
  // src/services/distributed/plan.ts
14196
14414
  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";
14415
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
14198
14416
  import {
14199
14417
  fpsToNumber as fpsToNumber10,
14200
14418
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -14298,9 +14516,9 @@ var PLAN_ROOT_CATEGORY = {
14298
14516
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
14299
14517
  var FREEZE_OWNED_PLAN_FILES = [
14300
14518
  "plan.json",
14301
- join28("meta", "composition.json"),
14302
- join28("meta", "encoder.json"),
14303
- join28("meta", "chunks.json")
14519
+ join29("meta", "composition.json"),
14520
+ join29("meta", "encoder.json"),
14521
+ join29("meta", "chunks.json")
14304
14522
  ];
14305
14523
 
14306
14524
  // src/services/distributed/renderConfigValidation.ts
@@ -14441,6 +14659,7 @@ function assertRequestOptionScalars(options) {
14441
14659
  assertOptionalInteger(options, "gifLoop");
14442
14660
  assertOptionalInteger(options, "hlsSegmentSeconds", 1);
14443
14661
  assertOptionalInteger(options, "workers", 1);
14662
+ assertOptionalInteger(options, "maxRenderMs");
14444
14663
  assertOptionalInteger(options, "crf");
14445
14664
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
14446
14665
  assertOptionalBoolean(options, field);
@@ -14533,7 +14752,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
14533
14752
  "INVALID_VIDEO_METADATA",
14534
14753
  "VIDEO_SOURCE_UNRENDERABLE",
14535
14754
  "VIDEO_EXTRACTION_FAILED",
14536
- "ENCODER_INTERRUPTED"
14755
+ "ENCODER_INTERRUPTED",
14756
+ "RENDER_TOO_SLOW"
14537
14757
  ]);
14538
14758
  function extractSafeRenderErrorCode(error) {
14539
14759
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -14592,6 +14812,7 @@ function parseRenderOptions(body) {
14592
14812
  const fps = parseServerFps(body.fps);
14593
14813
  const quality = parseServerQuality(body.quality);
14594
14814
  const workers = typeof body.workers === "number" ? body.workers : void 0;
14815
+ const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
14595
14816
  const useGpu = body.gpu === true;
14596
14817
  const debug = body.debug === true;
14597
14818
  const strictness = body.bestEffort === false ? "strict" : "best-effort";
@@ -14607,6 +14828,7 @@ function parseRenderOptions(body) {
14607
14828
  fps,
14608
14829
  quality,
14609
14830
  workers,
14831
+ maxRenderMs,
14610
14832
  crf,
14611
14833
  videoBitrate,
14612
14834
  useGpu,
@@ -14640,6 +14862,7 @@ function buildRenderJobConfig(input, outputPath, log) {
14640
14862
  quality: input.quality,
14641
14863
  format: input.format ?? "mp4",
14642
14864
  workers: input.workers,
14865
+ maxRenderMs: input.maxRenderMs,
14643
14866
  crf: input.crf,
14644
14867
  videoBitrate: input.videoBitrate,
14645
14868
  useGpu: input.useGpu,
@@ -14666,6 +14889,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
14666
14889
  function validateRenderOverrides(body) {
14667
14890
  const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
14668
14891
  if (!encoder.ok) return encoder.error;
14892
+ if (body.maxRenderMs !== void 0 && (typeof body.maxRenderMs !== "number" || !Number.isInteger(body.maxRenderMs) || body.maxRenderMs < 0)) {
14893
+ return "maxRenderMs must be a non-negative integer number of milliseconds";
14894
+ }
14669
14895
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
14670
14896
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
14671
14897
  }
@@ -14698,7 +14924,7 @@ function prepareProjectDirectory(projectDir, options) {
14698
14924
  const candidate = nonEmptyString(projectDir);
14699
14925
  if (!candidate) return null;
14700
14926
  const absProjectDir = resolve9(candidate);
14701
- if (!existsSync19(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14927
+ if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
14702
14928
  return { error: `Project directory not found: ${absProjectDir}` };
14703
14929
  }
14704
14930
  const entry = options.entryFile || "index.html";
@@ -14723,8 +14949,8 @@ async function resolveInlineRenderHtml(body) {
14723
14949
  }
14724
14950
  function materializeInlineProject(html, options) {
14725
14951
  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");
14952
+ const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
14953
+ writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
14728
14954
  return {
14729
14955
  prepared: {
14730
14956
  input: { projectDir: tempProjectDir, ...options },
@@ -14785,7 +15011,7 @@ function createArtifactStore(ttlMs) {
14785
15011
  function cleanupTempDir(dir, log) {
14786
15012
  if (!dir) return;
14787
15013
  try {
14788
- rmSync11(dir, { recursive: true, force: true });
15014
+ rmSync12(dir, { recursive: true, force: true });
14789
15015
  } catch (error) {
14790
15016
  log.warn("Failed to cleanup temp project dir", {
14791
15017
  cleanupProjectDir: dir,
@@ -14794,7 +15020,7 @@ function cleanupTempDir(dir, log) {
14794
15020
  }
14795
15021
  }
14796
15022
  function outputFileSize(path) {
14797
- return existsSync19(path) ? statSync8(path).size : 0;
15023
+ return existsSync19(path) ? statSync9(path).size : 0;
14798
15024
  }
14799
15025
  function createBlockingProgressReporter(log, requestId) {
14800
15026
  let lastLoggedPct = -10;
@@ -15094,7 +15320,7 @@ function createRenderHandlers(options = {}) {
15094
15320
  store.delete(token);
15095
15321
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
15096
15322
  }
15097
- const stats = statSync8(artifact.path);
15323
+ const stats = statSync9(artifact.path);
15098
15324
  return new Response(createReadStream3(artifact.path), {
15099
15325
  headers: {
15100
15326
  "content-type": "video/mp4",