@hyperframes/producer 0.8.197 → 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,19 +4,20 @@
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";
17
17
  import { Hono as Hono2 } from "hono";
18
18
  import { streamSSE } from "hono/streaming";
19
19
  import { serve as serve2 } from "@hono/node-server";
20
+ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
20
21
 
21
22
  // src/services/renderOrchestrator.ts
22
23
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
@@ -25,8 +26,8 @@ import {
25
26
  mkdirSync as mkdirSync9,
26
27
  readFileSync as readFileSync8,
27
28
  readdirSync as readdirSync5,
28
- rmSync as rmSync10,
29
- statSync as statSync6,
29
+ rmSync as rmSync11,
30
+ statSync as statSync7,
30
31
  writeFileSync as writeFileSync5,
31
32
  copyFileSync as copyFileSync4,
32
33
  appendFileSync
@@ -75,7 +76,7 @@ import {
75
76
  augmentProtocolTimeoutError,
76
77
  augmentPageNavigationTimeoutError
77
78
  } from "@hyperframes/engine";
78
- 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";
79
80
  import { totalmem as totalmem2 } from "node:os";
80
81
  import { randomUUID } from "crypto";
81
82
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -1909,8 +1910,8 @@ function replanAfterFailure(plan, failure) {
1909
1910
  }
1910
1911
 
1911
1912
  // src/services/render/stages/captureSegmentedStage.ts
1912
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
1913
- 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";
1914
1915
  import {
1915
1916
  captureFrameToBuffer as captureFrameToBuffer2,
1916
1917
  closeCaptureSession as closeCaptureSession2,
@@ -2294,7 +2295,7 @@ import {
2294
2295
  } from "@hyperframes/engine";
2295
2296
 
2296
2297
  // src/services/hdrCompositor.ts
2297
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2298
+ import { readSync as readSync2 } from "fs";
2298
2299
  import { join as join6 } from "path";
2299
2300
  import {
2300
2301
  captureAlphaPng,
@@ -2347,10 +2348,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
2347
2348
  }
2348
2349
  function closeHdrVideoFrameSource(source, log) {
2349
2350
  try {
2350
- closeSync3(source.fd);
2351
+ source.windows.close();
2351
2352
  } catch (err) {
2352
- log?.warn("Failed to close HDR raw frame file", {
2353
- rawPath: source.rawPath,
2353
+ log?.warn("Failed to close HDR raw frame windows", {
2354
+ dir: source.dir,
2354
2355
  error: err instanceof Error ? err.message : String(err)
2355
2356
  });
2356
2357
  }
@@ -2360,7 +2361,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
2360
2361
  if (frameIndex < 0 || frameCount < 1) return null;
2361
2362
  return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
2362
2363
  }
2363
- 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) {
2364
2365
  const frameSource = hdrVideoFrameSources.get(el.id);
2365
2366
  const startTime = hdrStartTimes.get(el.id);
2366
2367
  if (!frameSource || startTime === void 0 || el.opacity <= 0) {
@@ -2376,13 +2377,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
2376
2377
  if (effectiveIndex === null) return;
2377
2378
  const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
2378
2379
  if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
2379
- 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) {
2380
2401
  try {
2381
- if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2382
2402
  const bytesRead = timeHdrPhase(
2383
2403
  hdrPerf,
2384
2404
  "hdrVideoReadDecodeMs",
2385
- () => readSync2(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset)
2405
+ () => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
2386
2406
  );
2387
2407
  if (bytesRead !== frameSource.frameSize) return;
2388
2408
  const hdrRgb = frameSource.scratch;
@@ -2611,7 +2631,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2611
2631
  hdrPerf
2612
2632
  );
2613
2633
  } else {
2614
- blitHdrVideoLayer(
2634
+ await blitHdrVideoLayer(
2615
2635
  canvas,
2616
2636
  layer.element,
2617
2637
  time,
@@ -2655,7 +2675,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2655
2675
  startTime,
2656
2676
  localTime: localTime.toFixed(3),
2657
2677
  hdrFrameNum: frameNum,
2658
- rawPath: frameSource?.rawPath ?? null,
2678
+ dir: frameSource?.dir ?? null,
2659
2679
  frameCount: frameSource?.frameCount ?? null
2660
2680
  });
2661
2681
  }
@@ -2751,18 +2771,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2751
2771
  }
2752
2772
 
2753
2773
  // src/services/render/stages/captureHdrResources.ts
2754
- import {
2755
- closeSync as closeSync4,
2756
- constants as constants2,
2757
- fstatSync as fstatSync3,
2758
- mkdirSync as mkdirSync2,
2759
- mkdtempSync as mkdtempSync2,
2760
- openSync as openSync3,
2761
- readFileSync as readFileSync3,
2762
- rmSync as rmSync4,
2763
- statfsSync
2764
- } from "node:fs";
2765
- 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";
2766
2776
  import {
2767
2777
  decodePngToRgb48le,
2768
2778
  extractMediaMetadata as extractMediaMetadata2,
@@ -2776,7 +2786,90 @@ import {
2776
2786
  runFfmpeg
2777
2787
  } from "@hyperframes/engine";
2778
2788
  import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2779
- 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
2780
2873
  function tempDirSafePrefix(id) {
2781
2874
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
2782
2875
  return safe || "video";
@@ -2821,10 +2914,16 @@ async function probeHdrExtractionDims(args) {
2821
2914
  const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
2822
2915
  for (const el of stacking) {
2823
2916
  if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
2824
- 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
+ });
2825
2921
  }
2826
2922
  if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
2827
- 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
+ });
2828
2927
  }
2829
2928
  }
2830
2929
  }
@@ -2843,23 +2942,21 @@ async function probeHdrExtractionDims(args) {
2843
2942
  const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
2844
2943
  for (const el of retryStacking) {
2845
2944
  if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
2846
- 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
+ });
2847
2949
  if (!prep.hdrImageFitInfo.has(el.id)) {
2848
- 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
+ });
2849
2954
  }
2850
2955
  break;
2851
2956
  }
2852
2957
  }
2853
2958
  }
2854
2959
  }
2855
- function estimateHdrExtractionBytes(videos, fps) {
2856
- let total = 0;
2857
- for (const v of videos) {
2858
- const frames = Math.ceil(Math.max(0, v.durationSeconds) * fps);
2859
- total += frames * v.width * v.height * 6;
2860
- }
2861
- return total;
2862
- }
2863
2960
  var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
2864
2961
  var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
2865
2962
  var HDR_EXTRACTION_WARN_BYTES = 1e10;
@@ -2923,24 +3020,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
2923
3020
  }
2924
3021
  return window2;
2925
3022
  }
2926
- function cleanupHdrFrameDirectory(frameDir, rawPath, log) {
3023
+ function cleanupHdrFrameDirectory(frameDir, log) {
2927
3024
  if (process.env.KEEP_TEMP === "1") return;
2928
3025
  try {
2929
- rmSync4(frameDir, { recursive: true, force: true });
3026
+ rmSync5(frameDir, { recursive: true, force: true });
2930
3027
  } catch (err) {
2931
3028
  log?.warn("Failed to clean up HDR raw frame directory", {
2932
3029
  frameDir,
2933
- rawPath,
2934
3030
  error: err instanceof Error ? err.message : String(err)
2935
3031
  });
2936
3032
  }
2937
3033
  }
2938
3034
  function cleanupHdrVideoFrameSource(source, log) {
2939
3035
  closeHdrVideoFrameSource(source, log);
2940
- cleanupHdrFrameDirectory(source.dir, source.rawPath, log);
3036
+ cleanupHdrFrameDirectory(source.dir, log);
2941
3037
  }
2942
- function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2943
- const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
3038
+ function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
2944
3039
  const configuredBudget = resolveHdrExtractionBudgetBytes(
2945
3040
  process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
2946
3041
  );
@@ -2974,14 +3069,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2974
3069
  budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
2975
3070
  };
2976
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
+ }
2977
3084
  async function extractHdrVideoFrames(args) {
2978
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;
2979
3088
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
2980
3089
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
2981
3090
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3091
+ const fps = fpsToNumber4(job.config.fps);
2982
3092
  const out = /* @__PURE__ */ new Map();
2983
3093
  mkdirSync2(framesDir, { recursive: true });
2984
- const plannedVideos = [];
3094
+ let scratchBytes = 0;
2985
3095
  const extractionWindows = /* @__PURE__ */ new Map();
2986
3096
  for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
2987
3097
  const video = composition.videos.find((v) => v.id === videoId);
@@ -3002,16 +3112,13 @@ async function extractHdrVideoFrames(args) {
3002
3112
  videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
3003
3113
  });
3004
3114
  prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
3005
- plannedVideos.push({
3006
- durationSeconds: window2.durationSeconds,
3007
- width: dims.width,
3008
- height: dims.height
3009
- });
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;
3010
3118
  }
3011
3119
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
3012
3120
  framesDir,
3013
- plannedVideos,
3014
- fpsToNumber4(job.config.fps),
3121
+ scratchBytes,
3015
3122
  log
3016
3123
  );
3017
3124
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3023,79 +3130,79 @@ async function extractHdrVideoFrames(args) {
3023
3130
  if (!video || !planned) continue;
3024
3131
  const { window: window2 } = planned;
3025
3132
  mkdirSync2(framesDir, { recursive: true });
3026
- const frameDir = mkdtempSync2(join7(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3133
+ const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3027
3134
  createdFrameDirs.add(frameDir);
3028
3135
  const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
3029
- const rawPath = join7(frameDir, "frames.rgb48le");
3030
- const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3031
- const extractionStart = String(extractionMediaStart);
3032
- const ffmpegArgs = [];
3033
- if (window2.finalFrameOnly) {
3034
- ffmpegArgs.push("-i", srcPath, "-ss", extractionStart, "-frames:v", "1");
3035
- } else {
3036
- ffmpegArgs.push(
3037
- "-ss",
3038
- extractionStart,
3039
- "-i",
3040
- srcPath,
3041
- "-t",
3042
- String(window2.durationSeconds)
3043
- );
3044
- }
3045
- if (!window2.finalFrameOnly) {
3046
- ffmpegArgs.push("-r", fpsToFfmpegArg(job.config.fps));
3047
- }
3048
- ffmpegArgs.push(
3049
- "-vf",
3050
- `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
3051
- "-pix_fmt",
3052
- "rgb48le",
3053
- "-f",
3054
- "rawvideo",
3055
- "-y",
3056
- rawPath
3057
- );
3058
- const result = await runFfmpegImpl(ffmpegArgs, { signal: abortSignal });
3059
- if (!result.success) {
3060
- hdrDiagnostics.videoExtractionFailures += 1;
3061
- log.error("HDR frame pre-extraction failed; aborting render", {
3062
- videoId,
3063
- srcPath,
3064
- stderr: result.stderr.slice(-400)
3065
- });
3066
- throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3067
- error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3068
- failureReason: result.failureReason
3069
- });
3070
- }
3071
3136
  const frameSize = dims.width * dims.height * 6;
3072
- const fd = openSync3(rawPath, constants2.O_RDONLY | NO_FOLLOW_FLAG);
3073
- let handedOff = false;
3074
- try {
3075
- const frameCount = Math.floor(fstatSync3(fd).size / frameSize);
3076
- if (frameCount < 1) {
3077
- hdrDiagnostics.videoExtractionFailures += 1;
3078
- throw new Error(
3079
- `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 }
3080
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);
3081
3181
  }
3082
- out.set(videoId, {
3083
- dir: frameDir,
3084
- rawPath,
3085
- fd,
3086
- width: dims.width,
3087
- height: dims.height,
3088
- frameSize,
3089
- frameCount,
3090
- scratch: Buffer.allocUnsafe(frameSize),
3091
- loop: video.loop,
3092
- mediaStart: extractionMediaStart,
3093
- videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3094
- });
3095
- handedOff = true;
3096
- } finally {
3097
- 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
+ );
3098
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
+ });
3099
3206
  }
3100
3207
  return { sources: out, estimatedBytes, releaseReservation };
3101
3208
  } catch (error) {
@@ -3104,12 +3211,23 @@ async function extractHdrVideoFrames(args) {
3104
3211
  createdFrameDirs.delete(source.dir);
3105
3212
  }
3106
3213
  for (const frameDir of createdFrameDirs) {
3107
- cleanupHdrFrameDirectory(frameDir, void 0, log);
3214
+ cleanupHdrFrameDirectory(frameDir, log);
3108
3215
  }
3109
3216
  releaseReservation();
3110
3217
  throw error;
3111
3218
  }
3112
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
+ }
3113
3231
  function decodeHdrImageBuffers(args) {
3114
3232
  const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
3115
3233
  const out = /* @__PURE__ */ new Map();
@@ -3129,7 +3247,11 @@ function decodeHdrImageBuffers(args) {
3129
3247
  fit,
3130
3248
  fitInfo?.position
3131
3249
  );
3132
- 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
+ });
3133
3255
  } else {
3134
3256
  out.set(imageId, {
3135
3257
  data: Buffer.from(decoded.data),
@@ -3241,7 +3363,7 @@ async function captureSceneIntoBuffer(a) {
3241
3363
  hdrPerf
3242
3364
  );
3243
3365
  } else {
3244
- blitHdrVideoLayer(
3366
+ await blitHdrVideoLayer(
3245
3367
  sceneBuf,
3246
3368
  el,
3247
3369
  time,
@@ -3443,7 +3565,7 @@ function isTargetLossError(error) {
3443
3565
  // src/services/render/stages/captureStreamingStage.ts
3444
3566
  import { mkdtemp, writeFile } from "node:fs/promises";
3445
3567
  import { tmpdir } from "node:os";
3446
- import { join as join8 } from "node:path";
3568
+ import { join as join9 } from "node:path";
3447
3569
  import {
3448
3570
  DrawElementVerificationError,
3449
3571
  captureFrameToBuffer,
@@ -3605,11 +3727,11 @@ function createDrainFrameGuard(args) {
3605
3727
  return buf;
3606
3728
  }
3607
3729
  if (db < verifyMinDb) {
3608
- 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);
3609
3731
  if (dumpDir) {
3610
3732
  await Promise.all([
3611
- writeFile(join8(dumpDir, `frame-${idx}-de.jpg`), buf),
3612
- 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)
3613
3735
  ]).catch(() => {
3614
3736
  });
3615
3737
  }
@@ -4058,7 +4180,7 @@ async function runCaptureStreamingStage(input) {
4058
4180
 
4059
4181
  // src/services/render/stages/captureSegmentedStage.ts
4060
4182
  function segmentOutputPath(segmentDir, index) {
4061
- return join9(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4183
+ return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4062
4184
  }
4063
4185
  function segmentEncoderOptions(base, segment) {
4064
4186
  return {
@@ -4134,7 +4256,7 @@ async function captureSegment(ctx, segment) {
4134
4256
  path: segmentPath,
4135
4257
  // Recorded so resume can prove the file on disk is the one that
4136
4258
  // finished; absent only in unit tests, whose fake encoder writes none.
4137
- bytes: existsSync6(segmentPath) ? statSync2(segmentPath).size : 0
4259
+ bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4138
4260
  });
4139
4261
  return encodeResult.durationMs;
4140
4262
  } finally {
@@ -4152,7 +4274,7 @@ var DEFAULT_DEPS = {
4152
4274
  // is invisible to a test that swaps the engine module after this file has
4153
4275
  // been evaluated (bun updates live bindings, not copies).
4154
4276
  closeSession: (session) => closeCaptureSession2(session),
4155
- removeFile: (path) => rmSync5(path, { force: true })
4277
+ removeFile: (path) => rmSync6(path, { force: true })
4156
4278
  };
4157
4279
  function defaultSessionFactory(input) {
4158
4280
  let probeSession = input.probeSession;
@@ -4319,7 +4441,7 @@ async function runCaptureSegmentedStage(input) {
4319
4441
  const { log, dedupPerfs, assertNotAborted } = input;
4320
4442
  const deps = { ...DEFAULT_DEPS, ...input.deps };
4321
4443
  const segments = planSegments(input.totalFrames, input.segmentFrames);
4322
- const segmentDir = input.segmentDir ?? join9(input.workDir, "segments");
4444
+ const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
4323
4445
  mkdirSync3(segmentDir, { recursive: true });
4324
4446
  const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
4325
4447
  const pending = segments.filter((s) => !skip.has(s.index));
@@ -4390,10 +4512,83 @@ async function runCaptureSegmentedStage(input) {
4390
4512
  };
4391
4513
  }
4392
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
+
4393
4588
  // src/services/render/segmentManifest.ts
4394
4589
  import { createHash as createHash2 } from "node:crypto";
4395
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4396
- 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";
4397
4592
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4398
4593
  var MANIFEST_FILENAME = "segments.json";
4399
4594
  function computeSegmentPlanHash(input) {
@@ -4419,7 +4614,7 @@ function computeSegmentPlanHash(input) {
4419
4614
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4420
4615
  }
4421
4616
  function segmentDirFor(projectRendersDir, planHash) {
4422
- return join10(projectRendersDir, ".hf-segments", planHash);
4617
+ return join11(projectRendersDir, ".hf-segments", planHash);
4423
4618
  }
4424
4619
  function hasNumber(value, key) {
4425
4620
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -4437,7 +4632,7 @@ function isManifest(value) {
4437
4632
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
4438
4633
  }
4439
4634
  function readSegmentManifest(segmentDir) {
4440
- const path = join10(segmentDir, MANIFEST_FILENAME);
4635
+ const path = join11(segmentDir, MANIFEST_FILENAME);
4441
4636
  if (!existsSync7(path)) return null;
4442
4637
  try {
4443
4638
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -4448,9 +4643,9 @@ function readSegmentManifest(segmentDir) {
4448
4643
  }
4449
4644
  function writeSegmentManifest(segmentDir, manifest) {
4450
4645
  mkdirSync4(segmentDir, { recursive: true });
4451
- const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4646
+ const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4452
4647
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4453
- renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4648
+ renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
4454
4649
  }
4455
4650
  async function probeSegmentFrameCount(path, probe) {
4456
4651
  try {
@@ -4468,7 +4663,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
4468
4663
  if (manifest.planHash !== expectedHash) return ok;
4469
4664
  for (const entry of manifest.completed) {
4470
4665
  if (!existsSync7(entry.path)) continue;
4471
- const size = statSync3(entry.path).size;
4666
+ const size = statSync4(entry.path).size;
4472
4667
  if (size <= 0 || size !== entry.bytes) continue;
4473
4668
  const frames = await probeFrames(entry.path);
4474
4669
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -4562,7 +4757,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4562
4757
  }
4563
4758
 
4564
4759
  // src/services/render/captureCost.ts
4565
- import { join as join11 } from "node:path";
4760
+ import { join as join12 } from "node:path";
4566
4761
  import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4567
4762
  import {
4568
4763
  calculateOptimalWorkers,
@@ -4807,7 +5002,7 @@ async function runCaptureCalibration(input) {
4807
5002
  });
4808
5003
  let calibration;
4809
5004
  try {
4810
- calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
5005
+ calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
4811
5006
  } catch (error) {
4812
5007
  const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
4813
5008
  if (!shouldFallback) {
@@ -4840,7 +5035,7 @@ async function runCaptureCalibration(input) {
4840
5035
  const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
4841
5036
  try {
4842
5037
  calibration = await runOneCalibration(
4843
- join11(workDir, "capture-calibration-screenshot"),
5038
+ join12(workDir, "capture-calibration-screenshot"),
4844
5039
  screenshotCfg
4845
5040
  );
4846
5041
  } catch (fallbackError) {
@@ -5272,14 +5467,14 @@ function countAuthoredTimedClips(html) {
5272
5467
  }
5273
5468
 
5274
5469
  // src/services/render/stages/compileStage.ts
5275
- import { join as join14 } from "node:path";
5470
+ import { join as join15 } from "node:path";
5276
5471
  import { suggestMatchingPreset } from "@hyperframes/core";
5277
5472
 
5278
5473
  // src/services/htmlCompiler.ts
5279
5474
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5280
5475
  import { createHash as createHash5 } from "node:crypto";
5281
5476
  import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
5282
- 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";
5283
5478
  import { parseHTML as parseHTML4 } from "linkedom";
5284
5479
  import {
5285
5480
  COMPOSITION_SOURCE_URL,
@@ -5506,8 +5701,8 @@ import {
5506
5701
 
5507
5702
  // src/services/animatedGifPrep.ts
5508
5703
  import { createHash as createHash4 } from "node:crypto";
5509
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
5510
- 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";
5511
5706
  import { parseHTML as parseHTML3 } from "linkedom";
5512
5707
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5513
5708
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -5556,7 +5751,7 @@ function resolveGifSourcePath(src, options) {
5556
5751
  }
5557
5752
  function isUsableFile(path) {
5558
5753
  try {
5559
- const stat = statSync4(path);
5754
+ const stat = statSync5(path);
5560
5755
  return stat.isFile() && stat.size > 0;
5561
5756
  } catch {
5562
5757
  return false;
@@ -5691,10 +5886,10 @@ async function ensurePreparedWebm(input) {
5691
5886
  if (!isUsableFile(input.cachePath)) {
5692
5887
  renameWithRetry3(tmpPath, input.cachePath);
5693
5888
  } else {
5694
- rmSync6(tmpPath, { force: true });
5889
+ rmSync7(tmpPath, { force: true });
5695
5890
  }
5696
5891
  } catch (error) {
5697
- rmSync6(tmpPath, { force: true });
5892
+ rmSync7(tmpPath, { force: true });
5698
5893
  throw error;
5699
5894
  }
5700
5895
  }
@@ -5750,7 +5945,7 @@ function replaceImageWithVideo(input) {
5750
5945
  return video;
5751
5946
  }
5752
5947
  async function prepareAnimatedGifInputs(html, options) {
5753
- const outputDir = options.outputDir ?? join12(options.downloadDir, PREPARED_GIF_SUBDIR);
5948
+ const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
5754
5949
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
5755
5950
  const cacheDir = options.cacheDir ?? outputDir;
5756
5951
  const { document: document2 } = parseHTML3(html);
@@ -5772,8 +5967,8 @@ async function prepareAnimatedGifInputs(html, options) {
5772
5967
  const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
5773
5968
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
5774
5969
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
5775
- const cachePath = join12(cacheDir, filename);
5776
- const outputPath = join12(outputDir, filename);
5970
+ const cachePath = join13(cacheDir, filename);
5971
+ const outputPath = join13(outputDir, filename);
5777
5972
  const outputSrc = `${outputSrcPrefix}/${filename}`;
5778
5973
  await ensurePreparedWebm({
5779
5974
  sourcePath,
@@ -6187,7 +6382,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6187
6382
  }
6188
6383
  } else {
6189
6384
  filePath = decodedUrlPath(src);
6190
- if (!filePath.startsWith("/")) filePath = join13(baseDir, filePath);
6385
+ if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6191
6386
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6192
6387
  }
6193
6388
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6807,7 +7002,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
6807
7002
  return downloadAndRewriteUrls(
6808
7003
  urlSet,
6809
7004
  html,
6810
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7005
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6811
7006
  "Remote media download failed",
6812
7007
  "Localized remote media source(s)"
6813
7008
  );
@@ -6822,7 +7017,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
6822
7017
  return downloadAndRewriteUrls(
6823
7018
  urlSet,
6824
7019
  html,
6825
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7020
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6826
7021
  "Remote image download failed",
6827
7022
  "Localized remote image source(s)"
6828
7023
  );
@@ -6838,7 +7033,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
6838
7033
  return downloadAndRewriteUrls(
6839
7034
  urlSet,
6840
7035
  html,
6841
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7036
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6842
7037
  "Remote background-image download failed",
6843
7038
  "Localized remote background-image(s)",
6844
7039
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -6973,7 +7168,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
6973
7168
  return downloadAndRewriteUrls(
6974
7169
  urlSet,
6975
7170
  processed,
6976
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7171
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6977
7172
  "Remote font download failed",
6978
7173
  "Localized remote font face(s)",
6979
7174
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7640,7 +7835,7 @@ async function runCompileStage(input) {
7640
7835
  abortSignal
7641
7836
  } = input;
7642
7837
  const compileStart = Date.now();
7643
- const compiled = await compileForRender(projectDir, htmlPath, join14(workDir, "downloads"), {
7838
+ const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
7644
7839
  log,
7645
7840
  failClosedFontFetch: failClosedFontFetch === true,
7646
7841
  allowSystemFontCapture,
@@ -7648,7 +7843,7 @@ async function runCompileStage(input) {
7648
7843
  variables: input.variables,
7649
7844
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7650
7845
  pageMotionBlur: input.pageMotionBlur,
7651
- animatedGifCacheDir: cfg.extractCacheDir ? join14(cfg.extractCacheDir, "animated-gif") : void 0,
7846
+ animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
7652
7847
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7653
7848
  });
7654
7849
  assertNotAborted();
@@ -7711,7 +7906,7 @@ async function runCompileStage(input) {
7711
7906
  };
7712
7907
  await preflightCompositionAssetMediaTypes({
7713
7908
  projectDir,
7714
- compiledDir: join14(workDir, "compiled"),
7909
+ compiledDir: join15(workDir, "compiled"),
7715
7910
  composition,
7716
7911
  signal: abortSignal
7717
7912
  });
@@ -7756,7 +7951,7 @@ async function runCompileStage(input) {
7756
7951
  }
7757
7952
 
7758
7953
  // src/services/render/stages/probeStage.ts
7759
- import { join as join15 } from "node:path";
7954
+ import { join as join16 } from "node:path";
7760
7955
  import { parseHTML as parseHTML5 } from "linkedom";
7761
7956
  import {
7762
7957
  closeCaptureSession as closeCaptureSession4,
@@ -7948,7 +8143,7 @@ async function runProbeStage(input) {
7948
8143
  });
7949
8144
  fileServer = await createFileServer({
7950
8145
  projectDir,
7951
- compiledDir: join15(workDir, "compiled"),
8146
+ compiledDir: join16(workDir, "compiled"),
7952
8147
  port: 0,
7953
8148
  preHeadScripts: [VIRTUAL_TIME_SHIM],
7954
8149
  fps: job.config.fps
@@ -7976,7 +8171,7 @@ async function runProbeStage(input) {
7976
8171
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
7977
8172
  probeSession = await createCaptureSession4(
7978
8173
  fileServer.url,
7979
- join15(workDir, "probe"),
8174
+ join16(workDir, "probe"),
7980
8175
  captureOpts,
7981
8176
  null,
7982
8177
  probeCfg
@@ -8061,7 +8256,7 @@ async function runProbeStage(input) {
8061
8256
  });
8062
8257
  probeSession = await createCaptureSession4(
8063
8258
  fileServer.url,
8064
- join15(workDir, "probe-screenshot"),
8259
+ join16(workDir, "probe-screenshot"),
8065
8260
  captureOpts,
8066
8261
  null,
8067
8262
  { ...probeCfg, forceScreenshot: true }
@@ -8097,7 +8292,7 @@ async function runProbeStage(input) {
8097
8292
  compiled,
8098
8293
  resolutions,
8099
8294
  projectDir,
8100
- join15(workDir, "downloads")
8295
+ join16(workDir, "downloads")
8101
8296
  );
8102
8297
  assertNotAborted();
8103
8298
  composition.videos = compiled.videos;
@@ -8288,7 +8483,7 @@ async function runProbeStage(input) {
8288
8483
  }
8289
8484
  await preflightCompositionAssetMediaTypes({
8290
8485
  projectDir,
8291
- compiledDir: join15(workDir, "compiled"),
8486
+ compiledDir: join16(workDir, "compiled"),
8292
8487
  composition,
8293
8488
  signal: abortSignal
8294
8489
  });
@@ -8422,7 +8617,7 @@ function validateRenderDuration(input) {
8422
8617
 
8423
8618
  // src/services/render/stages/extractVideosStage.ts
8424
8619
  import { existsSync as existsSync11 } from "node:fs";
8425
- import { join as join16 } from "node:path";
8620
+ import { join as join17 } from "node:path";
8426
8621
  import {
8427
8622
  classifyVideoExtractionError,
8428
8623
  createFrameLookupTable,
@@ -8737,14 +8932,14 @@ async function runExtractVideosStage(input) {
8737
8932
  // because short boundary counts can differ by one frame.
8738
8933
  {
8739
8934
  fps: job.config.fps,
8740
- outputDir: join16(compiledDir, "__hyperframes_video_frames"),
8935
+ outputDir: join17(compiledDir, "__hyperframes_video_frames"),
8741
8936
  format: job.config.videoFrameFormat ?? "auto",
8742
8937
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
8743
8938
  timelineEnd: composition.duration,
8744
8939
  maxTransientRetries: extractionPolicy.maxTransientRetries,
8745
8940
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
8746
8941
  deferRangeExtraction,
8747
- contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)],
8942
+ contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
8748
8943
  urlByAbsentLocalPath
8749
8944
  },
8750
8945
  abortSignal,
@@ -8811,8 +9006,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8811
9006
  }
8812
9007
 
8813
9008
  // src/services/render/stages/audioStage.ts
8814
- import { join as join18 } from "node:path";
8815
- import { rmSync as rmSync8 } from "node:fs";
9009
+ import { join as join19 } from "node:path";
9010
+ import { rmSync as rmSync9 } from "node:fs";
8816
9011
  import {
8817
9012
  MIXED_AUDIO_FILENAME,
8818
9013
  audioFailureDiagnostics,
@@ -8821,8 +9016,8 @@ import {
8821
9016
 
8822
9017
  // src/services/render/audioPadTrim.ts
8823
9018
  import { spawn } from "node:child_process";
8824
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync7 } from "node:fs";
8825
- 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";
8826
9021
  import {
8827
9022
  DEFAULT_CONFIG,
8828
9023
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9025,7 +9220,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9025
9220
  error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
9026
9221
  };
9027
9222
  } finally {
9028
- for (const path of plan.cleanupPaths) rmSync7(path, { force: true });
9223
+ for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
9029
9224
  }
9030
9225
  let loweredDb = 0;
9031
9226
  if (probeTruePeak) {
@@ -9071,8 +9266,8 @@ async function constrainAacTruePeak(input) {
9071
9266
  async function enforceAacTruePeak(input) {
9072
9267
  let scratchDir;
9073
9268
  try {
9074
- scratchDir = mkdtempSync3(join17(dirname7(input.audioPath), ".true-peak-"));
9075
- const correctedPath = join17(scratchDir, "audio.m4a");
9269
+ scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9270
+ const correctedPath = join18(scratchDir, "audio.m4a");
9076
9271
  const measurements = [];
9077
9272
  let attenuationDb = 0;
9078
9273
  let measuredPath = input.audioPath;
@@ -9123,7 +9318,7 @@ async function enforceAacTruePeak(input) {
9123
9318
  )}`
9124
9319
  };
9125
9320
  } finally {
9126
- if (scratchDir) rmSync7(scratchDir, { recursive: true, force: true });
9321
+ if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9127
9322
  }
9128
9323
  }
9129
9324
  function failResult(outputPath, target, source, error) {
@@ -9270,7 +9465,7 @@ function assertAudioNotInterrupted(failures) {
9270
9465
  }
9271
9466
  async function runAudioStage(input) {
9272
9467
  const stageStart = Date.now();
9273
- const pcmSourcePath = join18(input.workDir, "audio-source.wav");
9468
+ const pcmSourcePath = join19(input.workDir, "audio-source.wav");
9274
9469
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9275
9470
  try {
9276
9471
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9290,7 +9485,7 @@ async function runAudioStage(input) {
9290
9485
  }
9291
9486
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9292
9487
  } finally {
9293
- rmSync8(pcmSourcePath, { force: true });
9488
+ rmSync9(pcmSourcePath, { force: true });
9294
9489
  }
9295
9490
  }
9296
9491
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9308,7 +9503,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9308
9503
  assertNotAborted,
9309
9504
  log
9310
9505
  } = input;
9311
- const audioOutputPath = join18(workDir, MIXED_AUDIO_FILENAME);
9506
+ const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
9312
9507
  let hasAudio = false;
9313
9508
  let audioError;
9314
9509
  let audioFailures;
@@ -9319,7 +9514,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9319
9514
  audioResult = await processCompositionAudio(
9320
9515
  audios,
9321
9516
  projectDir,
9322
- join18(workDir, "audio-work"),
9517
+ join19(workDir, "audio-work"),
9323
9518
  audioOutputPath,
9324
9519
  duration,
9325
9520
  abortSignal,
@@ -9388,7 +9583,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9388
9583
 
9389
9584
  // src/services/render/stages/captureStage.ts
9390
9585
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9391
- import { join as join19 } from "node:path";
9586
+ import { join as join20 } from "node:path";
9392
9587
  import {
9393
9588
  classifyCaptureFailure as classifyCaptureFailure2,
9394
9589
  frameFileExtension,
@@ -9461,13 +9656,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9461
9656
  if (!existsSync12(workDir)) return total;
9462
9657
  for (const attempt of readdirSync3(workDir)) {
9463
9658
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9464
- total += sampleDirectoryBytes(join19(workDir, attempt));
9659
+ total += sampleDirectoryBytes(join20(workDir, attempt));
9465
9660
  continue;
9466
9661
  }
9467
9662
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9468
- const attemptDir = join19(workDir, attempt);
9663
+ const attemptDir = join20(workDir, attempt);
9469
9664
  for (const name of readdirSync3(attemptDir)) {
9470
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join19(attemptDir, name));
9665
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
9471
9666
  }
9472
9667
  }
9473
9668
  return total;
@@ -9754,7 +9949,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
9754
9949
 
9755
9950
  // src/services/render/stages/captureHdrStage.ts
9756
9951
  import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
9757
- import { join as join23 } from "node:path";
9952
+ import { join as join24 } from "node:path";
9758
9953
  import {
9759
9954
  closeCaptureSession as closeCaptureSession7,
9760
9955
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -9826,7 +10021,7 @@ function createHdrImageTransferCache(options = {}) {
9826
10021
  }
9827
10022
 
9828
10023
  // src/services/render/stages/captureHdrSequentialLoop.ts
9829
- import { join as join20 } from "node:path";
10024
+ import { join as join21 } from "node:path";
9830
10025
  import {
9831
10026
  TRANSITIONS,
9832
10027
  crossfade
@@ -9951,7 +10146,7 @@ async function runSequentialLayeredFrameLoop(input) {
9951
10146
  );
9952
10147
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
9953
10148
  writeFileExclusiveSync(
9954
- join20(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10149
+ join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
9955
10150
  normalCanvas
9956
10151
  );
9957
10152
  }
@@ -9983,7 +10178,7 @@ async function runSequentialLayeredFrameLoop(input) {
9983
10178
  }
9984
10179
 
9985
10180
  // src/services/render/stages/captureHdrHybridLoop.ts
9986
- import { join as join22 } from "node:path";
10181
+ import { join as join23 } from "node:path";
9987
10182
  import {
9988
10183
  TRANSITIONS as TRANSITIONS2,
9989
10184
  closeCaptureSession as closeCaptureSession6,
@@ -9997,7 +10192,7 @@ import {
9997
10192
  // src/services/shaderTransitionWorkerPool.ts
9998
10193
  import { Worker } from "node:worker_threads";
9999
10194
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10000
- import { dirname as dirname8, join as join21 } from "node:path";
10195
+ import { dirname as dirname8, join as join22 } from "node:path";
10001
10196
  import { createRequire } from "node:module";
10002
10197
  import { existsSync as existsSync13 } from "node:fs";
10003
10198
  import { cpus as cpus2 } from "node:os";
@@ -10011,9 +10206,9 @@ function resolveWorkerEntry(explicit) {
10011
10206
  return { path: override, isTs };
10012
10207
  }
10013
10208
  const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10014
- const jsPath = join21(moduleDir, "shaderTransitionWorker.js");
10209
+ const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10015
10210
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10016
- const tsPath = join21(moduleDir, "shaderTransitionWorker.ts");
10211
+ const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10017
10212
  return { path: tsPath, isTs: true };
10018
10213
  }
10019
10214
  function buildExecArgv(entryIsTs) {
@@ -10381,7 +10576,7 @@ async function runHybridLayeredFrameLoop(input) {
10381
10576
  );
10382
10577
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10383
10578
  writeFileExclusiveSync(
10384
- join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10579
+ join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10385
10580
  canvas
10386
10581
  );
10387
10582
  }
@@ -10550,7 +10745,8 @@ async function runCaptureHdrStage(input) {
10550
10745
  width,
10551
10746
  height,
10552
10747
  abortSignal,
10553
- hdrDiagnostics
10748
+ hdrDiagnostics,
10749
+ maxResidentWindows: Math.max(1, workerCount) + 1
10554
10750
  });
10555
10751
  releaseHdrExtractionReservation = extracted.releaseReservation;
10556
10752
  for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
@@ -10568,7 +10764,7 @@ async function runCaptureHdrStage(input) {
10568
10764
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10569
10765
  }
10570
10766
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10571
- const debugDumpDir = debugDumpEnabled ? join23(framesDir, "debug-composite") : null;
10767
+ const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
10572
10768
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10573
10769
  mkdirSync7(debugDumpDir, { recursive: true });
10574
10770
  }
@@ -10728,12 +10924,12 @@ import {
10728
10924
  mkdirSync as mkdirSync8,
10729
10925
  readdirSync as readdirSync4,
10730
10926
  readFileSync as readFileSync7,
10731
- rmSync as rmSync9,
10732
- statSync as statSync5,
10927
+ rmSync as rmSync10,
10928
+ statSync as statSync6,
10733
10929
  writeFileSync as writeFileSync4
10734
10930
  } from "node:fs";
10735
10931
  import { readFile as readFile2 } from "node:fs/promises";
10736
- import { dirname as dirname9, join as join24 } from "node:path";
10932
+ import { dirname as dirname9, join as join25 } from "node:path";
10737
10933
  import {
10738
10934
  encodeFramesChunkedConcat,
10739
10935
  encodeFramesFromDir,
@@ -10858,7 +11054,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10858
11054
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
10859
11055
  if (failure) return failure;
10860
11056
  }
10861
- const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
11057
+ const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
10862
11058
  return {
10863
11059
  success: true,
10864
11060
  outputPath,
@@ -10867,7 +11063,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10867
11063
  fileSize
10868
11064
  };
10869
11065
  } finally {
10870
- rmSync9(input.palettePath, { force: true });
11066
+ rmSync10(input.palettePath, { force: true });
10871
11067
  }
10872
11068
  }
10873
11069
  function startEncodeProgress(job, stage, onProgress) {
@@ -10942,7 +11138,7 @@ function encodeWhileCapturing(input, totalFrames) {
10942
11138
  await new Promise((resolve10) => wake = resolve10);
10943
11139
  }
10944
11140
  if (signal.aborted) return;
10945
- const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
11141
+ const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
10946
11142
  const frame = await readLandedFrame(landed.get(next), mergedPath);
10947
11143
  landed.delete(next);
10948
11144
  if (!await stream.writeFrame(frame)) {
@@ -11025,11 +11221,11 @@ async function runEncodeStage(input) {
11025
11221
  );
11026
11222
  }
11027
11223
  captured.forEach((name, i) => {
11028
- const dst = join24(outputPath, formatExportFrameName(i, "png"));
11029
- copyFileSync3(join24(framesDir, name), dst);
11224
+ const dst = join25(outputPath, formatExportFrameName(i, "png"));
11225
+ copyFileSync3(join25(framesDir, name), dst);
11030
11226
  });
11031
11227
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11032
- copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
11228
+ copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11033
11229
  log.info(
11034
11230
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11035
11231
  );
@@ -11049,7 +11245,7 @@ async function runEncodeStage(input) {
11049
11245
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11050
11246
  fps: job.config.fps,
11051
11247
  loop,
11052
- palettePath: join24(dirname9(videoOnlyPath), "gif-palette.png"),
11248
+ palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11053
11249
  preserveAlpha: needsAlpha,
11054
11250
  signal: abortSignal,
11055
11251
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11262,9 +11458,9 @@ function sampleDirectoryBytes(dir) {
11262
11458
  continue;
11263
11459
  }
11264
11460
  for (const name of entries) {
11265
- const full = join25(current, name);
11461
+ const full = join26(current, name);
11266
11462
  try {
11267
- const st = statSync6(full);
11463
+ const st = statSync7(full);
11268
11464
  if (st.isDirectory()) {
11269
11465
  stack.push(full);
11270
11466
  } else if (st.isFile()) {
@@ -11411,8 +11607,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11411
11607
  const ranges = [];
11412
11608
  let rangeStart = null;
11413
11609
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11414
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11415
- 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;
11416
11612
  if (missing && rangeStart === null) {
11417
11613
  rangeStart = frameIndex;
11418
11614
  } else if (!missing && rangeStart !== null) {
@@ -11434,7 +11630,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
11434
11630
  workerId,
11435
11631
  startFrame: rangeStart + range.startFrame,
11436
11632
  endFrame: rangeStart + range.endFrame,
11437
- outputDir: join25(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11633
+ outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11438
11634
  outputFrameOffset: rangeStart
11439
11635
  }));
11440
11636
  batches.push(batch);
@@ -11462,8 +11658,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11462
11658
  return Math.max(1, Math.floor(currentWorkers / 2));
11463
11659
  }
11464
11660
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11465
- if (platform2 === "win32" || platform2 === "darwin") return join25(systemTempDir, "hf-render-");
11466
- return join25(dirname10(outputPath), `work-${jobId}-`);
11661
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11662
+ return join26(dirname10(outputPath), `work-${jobId}-`);
11467
11663
  }
11468
11664
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11469
11665
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11495,7 +11691,7 @@ The composition is too large for the available memory. To reduce memory pressure
11495
11691
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11496
11692
  let captured = 0;
11497
11693
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11498
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11694
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11499
11695
  if (existsSync16(framePath)) captured++;
11500
11696
  }
11501
11697
  return captured;
@@ -11521,7 +11717,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11521
11717
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11522
11718
  });
11523
11719
  pendingTransientRetry = false;
11524
- const attemptWorkDir = join25(options.workDir, `capture-attempt-${attempt}`);
11720
+ const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
11525
11721
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11526
11722
  missingRanges,
11527
11723
  currentWorkers,
@@ -11978,7 +12174,7 @@ function resolveDeShortBand(args) {
11978
12174
  return args.bandOpen ? "applied" : "skipped_elements";
11979
12175
  }
11980
12176
  function shouldPreferSingleWorkerDrawElement(args) {
11981
- 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;
11982
12178
  }
11983
12179
  function isProbeDeGated(session) {
11984
12180
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
@@ -12114,17 +12310,17 @@ function deVerifyFallbackTelemetry(err) {
12114
12310
  }
12115
12311
  function resolveRenderDebugDir() {
12116
12312
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12117
- return join25(producerRoot, ".debug");
12313
+ return join26(producerRoot, ".debug");
12118
12314
  }
12119
12315
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12120
12316
  validateHlsRenderConfig(job.config);
12121
12317
  const debugDir = resolveRenderDebugDir();
12122
12318
  const outputDir = dirname10(outputPath);
12123
12319
  if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12124
- 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);
12125
12321
  const pipelineStart = Date.now();
12126
12322
  const baseLog = job.config.logger ?? defaultLogger;
12127
- const logPath = job.config.debug ? join25(workDir, "render.log") : null;
12323
+ const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12128
12324
  const execution = new RenderExecutionContext({
12129
12325
  request: { renderJobId: job.id, projectDir, outputPath },
12130
12326
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12138,7 +12334,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12138
12334
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12139
12335
  return;
12140
12336
  }
12141
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12337
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12142
12338
  });
12143
12339
  try {
12144
12340
  await executeRenderPipeline({
@@ -12169,7 +12365,9 @@ async function executeRenderPipeline(input) {
12169
12365
  const log = execution.logger;
12170
12366
  const eventPublisher = execution.events;
12171
12367
  const onProgress = execution.onProgress;
12172
- 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;
12173
12371
  let fileServer = null;
12174
12372
  let probeSession = null;
12175
12373
  let lastBrowserConsole = [];
@@ -12182,7 +12380,7 @@ async function executeRenderPipeline(input) {
12182
12380
  imageDecodeFailures: 0
12183
12381
  };
12184
12382
  let hdrPerf;
12185
- const perfOutputPath = join25(workDir, "perf-summary.json");
12383
+ const perfOutputPath = join26(workDir, "perf-summary.json");
12186
12384
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12187
12385
  const observability = new RenderObservabilityRecorder({
12188
12386
  pipelineStartMs: pipelineStart,
@@ -12284,15 +12482,15 @@ async function executeRenderPipeline(input) {
12284
12482
  requestedWorkers: job.config.workers ?? "auto"
12285
12483
  });
12286
12484
  const entryFile = job.config.entryFile || "index.html";
12287
- let htmlPath = join25(projectDir, entryFile);
12485
+ let htmlPath = join26(projectDir, entryFile);
12288
12486
  if (!existsSync16(htmlPath)) {
12289
12487
  throw new Error(`Entry file not found: ${htmlPath}`);
12290
12488
  }
12291
12489
  assertNotAborted();
12292
12490
  const rawEntry = readFileSync8(htmlPath, "utf-8");
12293
12491
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12294
- const wrapperPath = join25(workDir, "standalone-entry.html");
12295
- const projectIndexPath = join25(projectDir, "index.html");
12492
+ const wrapperPath = join26(workDir, "standalone-entry.html");
12493
+ const projectIndexPath = join26(projectDir, "index.html");
12296
12494
  if (!existsSync16(projectIndexPath)) {
12297
12495
  throw new Error(
12298
12496
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -12455,7 +12653,7 @@ async function executeRenderPipeline(input) {
12455
12653
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12456
12654
  code: "extract_video"
12457
12655
  });
12458
- const compiledDir = join25(workDir, "compiled");
12656
+ const compiledDir = join26(workDir, "compiled");
12459
12657
  const extractResult = await observeRenderStage(
12460
12658
  observability,
12461
12659
  "video_extract",
@@ -12589,7 +12787,7 @@ async function executeRenderPipeline(input) {
12589
12787
  try {
12590
12788
  fileServer = await createFileServer({
12591
12789
  projectDir,
12592
- compiledDir: join25(workDir, "compiled"),
12790
+ compiledDir: join26(workDir, "compiled"),
12593
12791
  port: 0,
12594
12792
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12595
12793
  fps: job.config.fps
@@ -12607,7 +12805,7 @@ async function executeRenderPipeline(input) {
12607
12805
  if (!activeFileServer) {
12608
12806
  throw new Error("File server failed to initialize before frame capture");
12609
12807
  }
12610
- const framesDir = join25(workDir, "captured-frames");
12808
+ const framesDir = join26(workDir, "captured-frames");
12611
12809
  if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
12612
12810
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
12613
12811
  code: "start_browsers"
@@ -12711,7 +12909,6 @@ async function executeRenderPipeline(input) {
12711
12909
  const WOULD_RESOLVE_MULTI_WORKER = 2;
12712
12910
  const deInversionArgs = {
12713
12911
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12714
- requestedWorkers: job.config.workers,
12715
12912
  useDrawElement: cfg.useDrawElement,
12716
12913
  deCompileGate,
12717
12914
  forceScreenshot: captureForceScreenshot,
@@ -12917,7 +13114,7 @@ async function executeRenderPipeline(input) {
12917
13114
  } else if (deInversionEligible && workerCount > 1) {
12918
13115
  deWorkerInversion = "inverted";
12919
13116
  log.info(
12920
- `[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.`
12921
13118
  );
12922
13119
  workerCount = 1;
12923
13120
  }
@@ -12961,6 +13158,27 @@ async function executeRenderPipeline(input) {
12961
13158
  deWorkerInversion: deWorkerInversion ?? "none",
12962
13159
  deParallelRouter: deParallelRouter ?? "none"
12963
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
+ );
12964
13182
  let useStreamingEncode = shouldUseStreamingEncode(
12965
13183
  cfg,
12966
13184
  outputFormat,
@@ -13054,7 +13272,7 @@ async function executeRenderPipeline(input) {
13054
13272
  hls: ".mp4"
13055
13273
  };
13056
13274
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13057
- const videoOnlyPath = join25(workDir, `video-only${videoExt}`);
13275
+ const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13058
13276
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13059
13277
  if (usePageSideCompositingForTransitions) {
13060
13278
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13300,7 +13518,7 @@ async function executeRenderPipeline(input) {
13300
13518
  pageMotionBlur: job.config.pageMotionBlur,
13301
13519
  captureQuality: captureOptions.quality
13302
13520
  });
13303
- const segmentDir = segmentDirFor(join25(projectDir, "renders"), segmentPlanHash);
13521
+ const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
13304
13522
  let completedSegments = /* @__PURE__ */ new Set();
13305
13523
  if (job.config.resumeSegments === true) {
13306
13524
  const existing = readSegmentManifest(segmentDir);
@@ -13315,10 +13533,10 @@ async function executeRenderPipeline(input) {
13315
13533
  });
13316
13534
  }
13317
13535
  if (completedSegments.size === 0) {
13318
- rmSync10(segmentDir, { recursive: true, force: true });
13536
+ rmSync11(segmentDir, { recursive: true, force: true });
13319
13537
  }
13320
13538
  } else {
13321
- rmSync10(segmentDir, { recursive: true, force: true });
13539
+ rmSync11(segmentDir, { recursive: true, force: true });
13322
13540
  }
13323
13541
  const segmentManifest = readSegmentManifest(segmentDir) ?? {
13324
13542
  version: 1,
@@ -13399,7 +13617,7 @@ async function executeRenderPipeline(input) {
13399
13617
  );
13400
13618
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13401
13619
  if (job.config.keepSegments !== true) {
13402
- rmSync10(segmentDir, { recursive: true, force: true });
13620
+ rmSync11(segmentDir, { recursive: true, force: true });
13403
13621
  }
13404
13622
  } else {
13405
13623
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -13668,7 +13886,7 @@ async function executeRenderPipeline(input) {
13668
13886
  "capture_disk",
13669
13887
  "drawElement capture failed; retrying with a fresh screenshot session"
13670
13888
  );
13671
- rmSync10(framesDir, { recursive: true, force: true });
13889
+ rmSync11(framesDir, { recursive: true, force: true });
13672
13890
  mkdirSync9(framesDir, { recursive: true });
13673
13891
  resetCaptureAttemptProgress(job);
13674
13892
  dedupPerfs.length = 0;
@@ -13855,7 +14073,7 @@ async function executeRenderPipeline(input) {
13855
14073
  }
13856
14074
  if (job.config.debug) {
13857
14075
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
13858
- const debugOutput = join25(workDir, `output${videoExt}`);
14076
+ const debugOutput = join26(workDir, `output${videoExt}`);
13859
14077
  copyFileSync4(stagedOutputPath, debugOutput);
13860
14078
  }
13861
14079
  }
@@ -13866,8 +14084,9 @@ async function executeRenderPipeline(input) {
13866
14084
  job.outputPath = outputPath;
13867
14085
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
13868
14086
  await eventPublisher.flush();
13869
- } catch (error) {
13870
- if (error instanceof RenderCancelledError || executionSignal?.aborted) {
14087
+ } catch (caught) {
14088
+ const error = tooSlowError ?? caught;
14089
+ if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
13871
14090
  job.error = error instanceof Error ? error.message : "render_cancelled";
13872
14091
  updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
13873
14092
  await eventPublisher.flush();
@@ -13961,13 +14180,13 @@ async function executeRenderPipeline(input) {
13961
14180
  import {
13962
14181
  existsSync as existsSync17,
13963
14182
  readFileSync as readFileSync9,
13964
- statSync as statSync7,
14183
+ statSync as statSync8,
13965
14184
  openSync as openSync4,
13966
- fstatSync as fstatSync4,
13967
- closeSync as closeSync5,
14185
+ fstatSync as fstatSync3,
14186
+ closeSync as closeSync4,
13968
14187
  constants as constants3
13969
14188
  } from "node:fs";
13970
- 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";
13971
14190
  import { lintHyperframeHtml } from "@hyperframes/lint";
13972
14191
  function isStringRecord(value) {
13973
14192
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -14001,19 +14220,19 @@ function readEntryHtml(filePath) {
14001
14220
  } catch (error) {
14002
14221
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
14003
14222
  return null;
14004
- if (!statSync7(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14223
+ if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
14005
14224
  throw error;
14006
14225
  }
14007
14226
  try {
14008
- if (!fstatSync4(fd).isFile()) return null;
14227
+ if (!fstatSync3(fd).isFile()) return null;
14009
14228
  return readFileSync9(fd, "utf-8");
14010
14229
  } finally {
14011
- closeSync5(fd);
14230
+ closeSync4(fd);
14012
14231
  }
14013
14232
  }
14014
14233
  function readProjectEntryFile(projectDir, preferredEntryFile) {
14015
14234
  const absProjectDir = resolve8(projectDir);
14016
- if (!existsSync17(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
14235
+ if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14017
14236
  return { error: `Project directory not found: ${absProjectDir}` };
14018
14237
  }
14019
14238
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -14035,7 +14254,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
14035
14254
  }
14036
14255
  }
14037
14256
  return {
14038
- 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")}`
14039
14258
  };
14040
14259
  }
14041
14260
  function prepareHyperframeLintBody(body) {
@@ -14081,7 +14300,7 @@ async function runHyperframeLint(prepared) {
14081
14300
  // src/services/healthWorker.ts
14082
14301
  import { Worker as Worker2 } from "node:worker_threads";
14083
14302
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14084
- import { dirname as dirname11, join as join27 } from "node:path";
14303
+ import { dirname as dirname11, join as join28 } from "node:path";
14085
14304
  import { existsSync as existsSync18 } from "node:fs";
14086
14305
  var DEFAULT_HEALTH_PORT = 9848;
14087
14306
  async function startHealthWorker(options = {}) {
@@ -14157,7 +14376,7 @@ function defaultLogger2() {
14157
14376
  }
14158
14377
  function resolveWorkerEntry2() {
14159
14378
  const here = dirname11(fileURLToPath5(import.meta.url));
14160
- const candidates = [join27(here, "healthWorkerThread.js"), join27(here, "healthWorkerThread.ts")];
14379
+ const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
14161
14380
  for (const candidate of candidates) {
14162
14381
  if (existsSync18(candidate)) return candidate;
14163
14382
  }
@@ -14193,7 +14412,7 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
14193
14412
 
14194
14413
  // src/services/distributed/plan.ts
14195
14414
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
14196
- 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";
14197
14416
  import {
14198
14417
  fpsToNumber as fpsToNumber10,
14199
14418
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -14233,6 +14452,9 @@ import {
14233
14452
  var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
14234
14453
  var execFile = promisify2(execFileCallback);
14235
14454
 
14455
+ // src/services/distributed/chunkWeights.ts
14456
+ import { resolveFrameFormat } from "@hyperframes/engine";
14457
+
14236
14458
  // src/services/distributed/planProtocol.ts
14237
14459
  var PLAN_SCHEMA_VERSION = 1;
14238
14460
  var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
@@ -14294,9 +14516,9 @@ var PLAN_ROOT_CATEGORY = {
14294
14516
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
14295
14517
  var FREEZE_OWNED_PLAN_FILES = [
14296
14518
  "plan.json",
14297
- join28("meta", "composition.json"),
14298
- join28("meta", "encoder.json"),
14299
- join28("meta", "chunks.json")
14519
+ join29("meta", "composition.json"),
14520
+ join29("meta", "encoder.json"),
14521
+ join29("meta", "chunks.json")
14300
14522
  ];
14301
14523
 
14302
14524
  // src/services/distributed/renderConfigValidation.ts
@@ -14437,6 +14659,7 @@ function assertRequestOptionScalars(options) {
14437
14659
  assertOptionalInteger(options, "gifLoop");
14438
14660
  assertOptionalInteger(options, "hlsSegmentSeconds", 1);
14439
14661
  assertOptionalInteger(options, "workers", 1);
14662
+ assertOptionalInteger(options, "maxRenderMs");
14440
14663
  assertOptionalInteger(options, "crf");
14441
14664
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
14442
14665
  assertOptionalBoolean(options, field);
@@ -14529,7 +14752,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
14529
14752
  "INVALID_VIDEO_METADATA",
14530
14753
  "VIDEO_SOURCE_UNRENDERABLE",
14531
14754
  "VIDEO_EXTRACTION_FAILED",
14532
- "ENCODER_INTERRUPTED"
14755
+ "ENCODER_INTERRUPTED",
14756
+ "RENDER_TOO_SLOW"
14533
14757
  ]);
14534
14758
  function extractSafeRenderErrorCode(error) {
14535
14759
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -14588,6 +14812,7 @@ function parseRenderOptions(body) {
14588
14812
  const fps = parseServerFps(body.fps);
14589
14813
  const quality = parseServerQuality(body.quality);
14590
14814
  const workers = typeof body.workers === "number" ? body.workers : void 0;
14815
+ const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
14591
14816
  const useGpu = body.gpu === true;
14592
14817
  const debug = body.debug === true;
14593
14818
  const strictness = body.bestEffort === false ? "strict" : "best-effort";
@@ -14603,6 +14828,7 @@ function parseRenderOptions(body) {
14603
14828
  fps,
14604
14829
  quality,
14605
14830
  workers,
14831
+ maxRenderMs,
14606
14832
  crf,
14607
14833
  videoBitrate,
14608
14834
  useGpu,
@@ -14636,6 +14862,7 @@ function buildRenderJobConfig(input, outputPath, log) {
14636
14862
  quality: input.quality,
14637
14863
  format: input.format ?? "mp4",
14638
14864
  workers: input.workers,
14865
+ maxRenderMs: input.maxRenderMs,
14639
14866
  crf: input.crf,
14640
14867
  videoBitrate: input.videoBitrate,
14641
14868
  useGpu: input.useGpu,
@@ -14662,6 +14889,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
14662
14889
  function validateRenderOverrides(body) {
14663
14890
  const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
14664
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
+ }
14665
14895
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
14666
14896
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
14667
14897
  }
@@ -14689,11 +14919,12 @@ function validateOutputResolutionOverride(body) {
14689
14919
  }
14690
14920
  return void 0;
14691
14921
  }
14922
+ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
14692
14923
  function prepareProjectDirectory(projectDir, options) {
14693
14924
  const candidate = nonEmptyString(projectDir);
14694
14925
  if (!candidate) return null;
14695
14926
  const absProjectDir = resolve9(candidate);
14696
- if (!existsSync19(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
14927
+ if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
14697
14928
  return { error: `Project directory not found: ${absProjectDir}` };
14698
14929
  }
14699
14930
  const entry = options.entryFile || "index.html";
@@ -14709,11 +14940,7 @@ async function resolveInlineRenderHtml(body) {
14709
14940
  if (!previewUrl)
14710
14941
  return { error: "Missing render source: provide projectDir, previewUrl, or html" };
14711
14942
  try {
14712
- const response = await fetch(previewUrl, { method: "GET" });
14713
- if (!response.ok) {
14714
- return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
14715
- }
14716
- return { html: await response.text() };
14943
+ return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
14717
14944
  } catch (error) {
14718
14945
  return {
14719
14946
  error: `Failed to fetch previewUrl: ${error instanceof Error ? error.message : String(error)}`
@@ -14722,8 +14949,8 @@ async function resolveInlineRenderHtml(body) {
14722
14949
  }
14723
14950
  function materializeInlineProject(html, options) {
14724
14951
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
14725
- const tempProjectDir = mkdtempSync4(join29(tempRoot, "producer-project-"));
14726
- 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");
14727
14954
  return {
14728
14955
  prepared: {
14729
14956
  input: { projectDir: tempProjectDir, ...options },
@@ -14784,7 +15011,7 @@ function createArtifactStore(ttlMs) {
14784
15011
  function cleanupTempDir(dir, log) {
14785
15012
  if (!dir) return;
14786
15013
  try {
14787
- rmSync11(dir, { recursive: true, force: true });
15014
+ rmSync12(dir, { recursive: true, force: true });
14788
15015
  } catch (error) {
14789
15016
  log.warn("Failed to cleanup temp project dir", {
14790
15017
  cleanupProjectDir: dir,
@@ -14793,7 +15020,7 @@ function cleanupTempDir(dir, log) {
14793
15020
  }
14794
15021
  }
14795
15022
  function outputFileSize(path) {
14796
- return existsSync19(path) ? statSync8(path).size : 0;
15023
+ return existsSync19(path) ? statSync9(path).size : 0;
14797
15024
  }
14798
15025
  function createBlockingProgressReporter(log, requestId) {
14799
15026
  let lastLoggedPct = -10;
@@ -15093,7 +15320,7 @@ function createRenderHandlers(options = {}) {
15093
15320
  store.delete(token);
15094
15321
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
15095
15322
  }
15096
- const stats = statSync8(artifact.path);
15323
+ const stats = statSync9(artifact.path);
15097
15324
  return new Response(createReadStream3(artifact.path), {
15098
15325
  headers: {
15099
15326
  "content-type": "video/mp4",