@hyperframes/producer 0.8.206 → 0.8.208

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  mkdirSync as mkdirSync10,
6
6
  readFileSync as readFileSync9,
7
7
  readdirSync as readdirSync5,
8
- rmSync as rmSync12,
8
+ rmSync as rmSync10,
9
9
  statSync as statSync8,
10
10
  writeFileSync as writeFileSync6,
11
11
  copyFileSync as copyFileSync5,
@@ -14,7 +14,7 @@ import {
14
14
  import { tmpdir as tmpdir2 } from "node:os";
15
15
  import {
16
16
  chainCapture,
17
- fpsToNumber as fpsToNumber9,
17
+ fpsToNumber as fpsToNumber8,
18
18
  HF_COLOR_GRADING_ATTR,
19
19
  HF_VFX_ATTR,
20
20
  parseVfxChain,
@@ -810,7 +810,7 @@ function outputUsesH264Pipeline(format) {
810
810
  }
811
811
 
812
812
  // src/services/render/hlsConfig.ts
813
- import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
813
+ import { keyframeIntervalToGopSize } from "@hyperframes/engine";
814
814
  var DEFAULT_HLS_SEGMENT_SECONDS = 4;
815
815
  function validateHlsRenderConfig(config) {
816
816
  if (config.format !== "hls") return;
@@ -835,7 +835,7 @@ function resolveHlsSegmentSeconds(config) {
835
835
  return config.hlsSegmentSeconds ?? DEFAULT_HLS_SEGMENT_SECONDS;
836
836
  }
837
837
  function resolveHlsGopSize(fps, segmentSeconds) {
838
- return Math.max(1, Math.round(fpsToNumber2(fps) * segmentSeconds));
838
+ return keyframeIntervalToGopSize(fps, segmentSeconds);
839
839
  }
840
840
  function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
841
841
  if (outputFormat !== "hls") return { lockGopForChunkConcat: false, gopSize: void 0 };
@@ -845,6 +845,41 @@ function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
845
845
  };
846
846
  }
847
847
 
848
+ // src/services/render/deliveryEncoder.ts
849
+ import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
850
+ function validateDeliveryEncoderConfig(config) {
851
+ const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
852
+ if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
853
+ return;
854
+ if (keyframeIntervalSeconds !== void 0) {
855
+ if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
856
+ throw new Error(
857
+ `keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
858
+ );
859
+ }
860
+ if (format === "hls") {
861
+ throw new Error(
862
+ "keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
863
+ );
864
+ }
865
+ }
866
+ if (format !== void 0 && format !== "mp4") {
867
+ throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
868
+ }
869
+ appendRateCapArgs([], { maxBitrate, bufferSize });
870
+ }
871
+ function chunkGopFrames(framesInChunk, intervalFrames) {
872
+ return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
873
+ }
874
+ function resolveDeliveryEncoderOptions(config) {
875
+ const { maxBitrate, bufferSize, keyframeIntervalSeconds } = config;
876
+ const gop = keyframeIntervalSeconds === void 0 ? resolveHlsEncoderGopLock(config.format ?? "mp4", config.fps, config.hlsSegmentSeconds) : {
877
+ lockGopForChunkConcat: true,
878
+ gopSize: keyframeIntervalToGopSize2(config.fps, keyframeIntervalSeconds)
879
+ };
880
+ return { maxBitrate, bufferSize, ...gop };
881
+ }
882
+
848
883
  // src/services/render/shared.ts
849
884
  import {
850
885
  copyFileSync,
@@ -1864,7 +1899,7 @@ function replanSegmentedAfterFailure(plan2, failure) {
1864
1899
  const keepSegmenting = failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture";
1865
1900
  return createCapturePlan({
1866
1901
  ...plan2,
1867
- forceScreenshot: keepSegmenting || failure.kind === "capture_failure",
1902
+ forceScreenshot: plan2.forceScreenshot || keepSegmenting || failure.kind === "capture_failure",
1868
1903
  useStreamingEncode: true,
1869
1904
  useLayeredComposite: false,
1870
1905
  useSegmentedCapture: keepSegmenting,
@@ -1934,7 +1969,7 @@ import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1934
1969
 
1935
1970
  // src/services/render/perfSummary.ts
1936
1971
  import { arch, cpus, platform, totalmem } from "node:os";
1937
- import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1972
+ import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
1938
1973
 
1939
1974
  // src/services/render/hdrPerf.ts
1940
1975
  function createHdrPerfCollector() {
@@ -2205,7 +2240,7 @@ function buildRenderPerfSummary(input) {
2205
2240
  // payloads (CLI telemetry, regression-harness reports) where a single
2206
2241
  // number is friendlier than `{num,den}`. Callers needing the rational
2207
2242
  // back can read `job.config.fps`.
2208
- fps: fpsToNumber3(input.job.config.fps),
2243
+ fps: fpsToNumber2(input.job.config.fps),
2209
2244
  quality: input.job.config.quality,
2210
2245
  workers: input.workerCount,
2211
2246
  workerSizing: input.workerSizing,
@@ -2790,7 +2825,7 @@ import {
2790
2825
  resolveVideoExtractionWindow,
2791
2826
  runFfmpeg
2792
2827
  } from "@hyperframes/engine";
2793
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2828
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
2794
2829
 
2795
2830
  // src/services/hdrFrameWindows.ts
2796
2831
  import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
@@ -3093,7 +3128,7 @@ async function extractHdrVideoFrames(args) {
3093
3128
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
3094
3129
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
3095
3130
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3096
- const fps = fpsToNumber4(job.config.fps);
3131
+ const fps = fpsToNumber3(job.config.fps);
3097
3132
  const out = /* @__PURE__ */ new Map();
3098
3133
  mkdirSync2(framesDir, { recursive: true });
3099
3134
  let scratchBytes = 0;
@@ -3931,7 +3966,6 @@ async function runCaptureStreamingStage(input) {
3931
3966
  const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
3932
3967
  if (workerCount > 1) {
3933
3968
  const deParallelStream = forceParallelStream === true || process.env.HF_DE_PARALLEL_STREAM === "true";
3934
- const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames, workerCount, workDir) : distributeFrames(totalFrames, workerCount, workDir);
3935
3969
  const parallelStats = {
3936
3970
  verifyChecked: 0,
3937
3971
  blankSuspects: 0,
@@ -3944,143 +3978,158 @@ async function runCaptureStreamingStage(input) {
3944
3978
  frameTime: (i) => i * job.config.fps.den / job.config.fps.num
3945
3979
  });
3946
3980
  let parallelGuardRan = false;
3947
- let parallelDrainError = null;
3948
- const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
3949
- try {
3950
- if (deParallelStream && workerSession.captureMode === "drawelement") {
3951
- parallelGuardRan = true;
3952
- buffer = await parallelGuard(workerSession, frameIndex, buffer);
3953
- }
3954
- await reorderBuffer.waitForFrame(frameIndex);
3955
- ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
3956
- reorderBuffer.advanceTo(frameIndex + 1);
3957
- } catch (err) {
3958
- const e = err instanceof Error ? err : new Error(String(err));
3959
- if (!parallelDrainError) {
3960
- parallelDrainError = e;
3961
- reorderBuffer.abort(e);
3981
+ let writtenFrames = 0;
3982
+ let resumeFrom = 0;
3983
+ let resumedAfterStall = false;
3984
+ let workerResults;
3985
+ for (; ; ) {
3986
+ const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
3987
+ const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames - resumeFrom, workerCount, workDir, resumeFrom) : distributeFrames(totalFrames - resumeFrom, workerCount, workDir, resumeFrom);
3988
+ let parallelDrainError = null;
3989
+ const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
3990
+ try {
3991
+ if (deParallelStream && workerSession.captureMode === "drawelement") {
3992
+ parallelGuardRan = true;
3993
+ buffer = await parallelGuard(workerSession, frameIndex, buffer);
3994
+ }
3995
+ await parallelReorder.waitForFrame(frameIndex);
3996
+ ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
3997
+ parallelReorder.advanceTo(frameIndex + 1);
3998
+ writtenFrames = frameIndex + 1;
3999
+ } catch (err) {
4000
+ const e = err instanceof Error ? err : new Error(String(err));
4001
+ if (!parallelDrainError) {
4002
+ parallelDrainError = e;
4003
+ parallelReorder.abort(e);
4004
+ }
4005
+ throw e;
3962
4006
  }
3963
- throw e;
4007
+ };
4008
+ const captureLabel = input.parallelCaptureLabel ?? "worker";
4009
+ let watchdogArmed = false;
4010
+ const stallController = new AbortController();
4011
+ const forwardParentAbort = () => stallController.abort();
4012
+ if (abortSignal) {
4013
+ if (abortSignal.aborted) stallController.abort();
4014
+ else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
3964
4015
  }
3965
- };
3966
- const captureLabel = input.parallelCaptureLabel ?? "worker";
3967
- let watchdogArmed = false;
3968
- const stallController = new AbortController();
3969
- const forwardParentAbort = () => stallController.abort();
3970
- if (abortSignal) {
3971
- if (abortSignal.aborted) stallController.abort();
3972
- else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
3973
- }
3974
- const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3975
- let lastCapturedFrames = 0;
3976
- let lastProgressAt = Date.now();
3977
- const workerPhases = /* @__PURE__ */ new Map();
3978
- const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
3979
- let stalled = false;
3980
- const stallTimer = setInterval(
3981
- () => {
3982
- if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
3983
- stalled = true;
3984
- const stallErr = new ParallelCaptureStallError({
3985
- variant: "drain",
3986
- captureLabel,
3987
- stallTimeoutMs,
3988
- lastCapturedFrames,
3989
- totalFrames,
3990
- phaseSummary: phaseSummary()
3991
- });
3992
- reorderBuffer.abort(stallErr);
3993
- stallController.abort();
3994
- },
3995
- Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
3996
- );
3997
- let workerResults;
3998
- try {
3999
- workerResults = await executeParallelCapture(
4000
- fileServer.url,
4001
- workDir,
4002
- tasks,
4003
- buildCaptureOptions(),
4004
- createRenderVideoFrameInjector,
4005
- stallController.signal,
4006
- (progress) => {
4007
- if (progress.latestWorkerPhase) {
4008
- const phase = progress.latestWorkerPhase;
4009
- const detail = `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
4010
- workerPhases.set(phase.workerId, detail);
4011
- if (phase.phase === "frame_capture" && !watchdogArmed) {
4012
- watchdogArmed = true;
4016
+ const stallTimeoutMs = resolveCaptureStallTimeoutMs();
4017
+ let lastCapturedFrames = 0;
4018
+ let lastProgressAt = Date.now();
4019
+ const workerPhases = /* @__PURE__ */ new Map();
4020
+ const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
4021
+ const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
4022
+ let stalled = false;
4023
+ const stallTimer = setInterval(
4024
+ () => {
4025
+ if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
4026
+ stalled = true;
4027
+ const stallErr = new ParallelCaptureStallError({
4028
+ variant: "drain",
4029
+ captureLabel,
4030
+ stallTimeoutMs,
4031
+ lastCapturedFrames: resumeFrom + lastCapturedFrames,
4032
+ totalFrames,
4033
+ phaseSummary: phaseSummary()
4034
+ });
4035
+ parallelReorder.abort(stallErr);
4036
+ stallController.abort();
4037
+ },
4038
+ Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
4039
+ );
4040
+ try {
4041
+ workerResults = await executeParallelCapture(
4042
+ fileServer.url,
4043
+ workDir,
4044
+ tasks,
4045
+ buildCaptureOptions(),
4046
+ createRenderVideoFrameInjector,
4047
+ stallController.signal,
4048
+ (progress) => {
4049
+ if (progress.latestWorkerPhase) {
4050
+ const phase = progress.latestWorkerPhase;
4051
+ workerPhases.set(phase.workerId, phaseDetail(phase));
4052
+ if (phase.phase === "frame_capture" && !watchdogArmed) {
4053
+ watchdogArmed = true;
4054
+ lastProgressAt = Date.now();
4055
+ }
4056
+ log.info("[Render] Parallel capture worker phase", {
4057
+ workerId: phase.workerId,
4058
+ phase: phase.phase,
4059
+ frameIndex: phase.frameIndex,
4060
+ browserExecutable: phase.browserExecutable,
4061
+ browserVersion: phase.browserVersion,
4062
+ canvasDrawElement: phase.canvasDrawElement,
4063
+ gpuBackend: phase.gpuBackend
4064
+ });
4065
+ if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
4066
+ return;
4067
+ }
4068
+ if (progress.capturedFrames > lastCapturedFrames) {
4069
+ lastCapturedFrames = progress.capturedFrames;
4013
4070
  lastProgressAt = Date.now();
4071
+ watchdogArmed = true;
4072
+ }
4073
+ const doneFrames = resumeFrom + progress.capturedFrames;
4074
+ job.framesRendered = doneFrames;
4075
+ const frameProgress = doneFrames / totalFrames;
4076
+ reportFrameProgress(
4077
+ job,
4078
+ `Streaming frame ${doneFrames}/${totalFrames} (${workerCount} workers)`,
4079
+ Math.round(25 + frameProgress * 55),
4080
+ onProgress,
4081
+ doneFrames,
4082
+ totalFrames
4083
+ );
4084
+ },
4085
+ onFrameBuffer,
4086
+ // Interleaved DE workers each get their own browser process: co-tenant pages starve
4087
+ // each other's paint events.
4088
+ deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
4089
+ {
4090
+ // A worker died: release parked peers with its original failure, not the stall the
4091
+ // watchdog raises later.
4092
+ onWorkerOperation: (phase) => workerPhases.set(phase.workerId, phaseDetail(phase)),
4093
+ onWorkerFailure: (failure) => {
4094
+ if (parallelDrainError) return;
4095
+ parallelDrainError = failure;
4096
+ parallelReorder.abort(failure);
4014
4097
  }
4015
- log.info("[Render] Parallel capture worker phase", {
4016
- workerId: phase.workerId,
4017
- phase: phase.phase,
4018
- frameIndex: phase.frameIndex,
4019
- browserExecutable: phase.browserExecutable,
4020
- browserVersion: phase.browserVersion,
4021
- canvasDrawElement: phase.canvasDrawElement,
4022
- gpuBackend: phase.gpuBackend
4023
- });
4024
- if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
4025
- return;
4026
- }
4027
- if (progress.capturedFrames > lastCapturedFrames) {
4028
- lastCapturedFrames = progress.capturedFrames;
4029
- lastProgressAt = Date.now();
4030
- watchdogArmed = true;
4031
4098
  }
4032
- job.framesRendered = progress.capturedFrames;
4033
- const frameProgress = progress.capturedFrames / progress.totalFrames;
4034
- reportFrameProgress(
4035
- job,
4036
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
4037
- Math.round(25 + frameProgress * 55),
4038
- onProgress,
4039
- progress.capturedFrames,
4040
- progress.totalFrames
4099
+ );
4100
+ } catch (err) {
4101
+ if (stalled && abortSignal?.aborted !== true && captureLabel === "BeginFrame" && !resumedAfterStall && writtenFrames > resumeFrom) {
4102
+ resumedAfterStall = true;
4103
+ log.warn(
4104
+ "[Render] Parallel BeginFrame capture stalled; resuming from the first unwritten frame",
4105
+ {
4106
+ resumeFrom: writtenFrames,
4107
+ totalFrames,
4108
+ workerPhases: phaseSummary()
4109
+ }
4041
4110
  );
4042
- },
4043
- onFrameBuffer,
4044
- // Interleaved DE workers each need their own browser PROCESS:
4045
- // pages co-tenant in one browser share one compositor, whose
4046
- // internal frame scheduling deprioritizes non-active pages — their
4047
- // canvas `paint` events starve and the drawElement paint-wait slows
4048
- // ~3x (measured: 86s vs 30s on a 3,245-frame rAF comp). This is
4049
- // Chromium's internal frame-production scheduling on ALL platforms,
4050
- // NOT the Linux-only HeadlessExperimental.beginFrame capture mode.
4051
- deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
4052
- {
4053
- // A worker died (Target closed, Page crashed, launch failure).
4054
- // Its frames are gone and peers are parked in waitForFrame on the
4055
- // first of them; release them with the ORIGINAL failure so the
4056
- // orchestrator sees a transient browser error it knows how to
4057
- // retry, not the stall the watchdog would have raised a minute
4058
- // later. First failure wins, same as the drain guard above.
4059
- onWorkerFailure: (failure) => {
4060
- if (parallelDrainError) return;
4061
- parallelDrainError = failure;
4062
- reorderBuffer.abort(failure);
4063
- }
4111
+ resumeFrom = writtenFrames;
4112
+ continue;
4064
4113
  }
4065
- );
4066
- } catch (err) {
4067
- if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4068
- if (stalled && abortSignal?.aborted !== true) {
4069
- throw new ParallelCaptureStallError({
4070
- variant: "pool",
4071
- captureLabel,
4072
- stallTimeoutMs,
4073
- lastCapturedFrames,
4074
- totalFrames,
4075
- phaseSummary: phaseSummary()
4076
- });
4114
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4115
+ if (stalled && abortSignal?.aborted !== true) {
4116
+ throw new ParallelCaptureStallError({
4117
+ variant: "pool",
4118
+ captureLabel,
4119
+ stallTimeoutMs,
4120
+ lastCapturedFrames: resumeFrom + lastCapturedFrames,
4121
+ totalFrames,
4122
+ phaseSummary: phaseSummary()
4123
+ });
4124
+ }
4125
+ throw poolFailure(err, probeSession);
4126
+ } finally {
4127
+ clearInterval(stallTimer);
4128
+ abortSignal?.removeEventListener("abort", forwardParentAbort);
4077
4129
  }
4078
- throw poolFailure(err, probeSession);
4079
- } finally {
4080
- clearInterval(stallTimer);
4081
- abortSignal?.removeEventListener("abort", forwardParentAbort);
4130
+ if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4131
+ break;
4082
4132
  }
4083
- if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
4084
4133
  pushWorkerDedupPerfs(workerResults, dedupPerfs);
4085
4134
  const workerPageErrors = [];
4086
4135
  pushWorkerPageErrors(workerResults, workerPageErrors);
@@ -4241,7 +4290,7 @@ function segmentEncoderOptions(base, segment) {
4241
4290
  return {
4242
4291
  ...base,
4243
4292
  lockGopForChunkConcat: true,
4244
- gopSize: segment.endFrame - segment.startFrame
4293
+ gopSize: Math.min(base.gopSize ?? Infinity, segment.endFrame - segment.startFrame)
4245
4294
  };
4246
4295
  }
4247
4296
  var SegmentEncoderSpawnError = class extends Error {
@@ -4335,6 +4384,12 @@ function defaultSessionFactory(input) {
4335
4384
  let probeSession = input.probeSession;
4336
4385
  return {
4337
4386
  create: async () => {
4387
+ const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
4388
+ if (probeSession && probeCannotCaptureEveryFrame) {
4389
+ await closeCaptureSession2(probeSession).catch(() => {
4390
+ });
4391
+ probeSession = null;
4392
+ }
4338
4393
  const session = probeSession ?? await openSegmentedSession(input);
4339
4394
  if (probeSession) {
4340
4395
  openSegmentedSessionReuse(input, probeSession);
@@ -4665,8 +4720,10 @@ function computeSegmentPlanHash(input) {
4665
4720
  input.motionBlur,
4666
4721
  input.pageMotionBlur ?? "",
4667
4722
  input.captureQuality === void 0 ? "" : String(input.captureQuality)
4668
- ].join(" ");
4669
- return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4723
+ ];
4724
+ const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
4725
+ if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
4726
+ return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
4670
4727
  }
4671
4728
  function segmentDirFor(projectRendersDir, planHash) {
4672
4729
  return join11(projectRendersDir, ".hf-segments", planHash);
@@ -4813,7 +4870,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4813
4870
 
4814
4871
  // src/services/render/captureCost.ts
4815
4872
  import { join as join12 } from "node:path";
4816
- import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4873
+ import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
4817
4874
  import {
4818
4875
  calculateOptimalWorkers,
4819
4876
  captureFrameToBuffer as captureFrameToBuffer3,
@@ -5017,7 +5074,7 @@ async function runCaptureCalibration(input) {
5017
5074
  let probeSession = input.probeSession;
5018
5075
  let forceScreenshot = input.forceScreenshot;
5019
5076
  let lastBrowserConsole = [];
5020
- const fps = fpsToNumber5(job.config.fps);
5077
+ const fps = fpsToNumber4(job.config.fps);
5021
5078
  const sessionRef = { current: null };
5022
5079
  const runOneCalibration = async (sessionDir, sessionCfg) => {
5023
5080
  log.info("Launching browser for capture calibration...");
@@ -5477,7 +5534,7 @@ function emitFallbackCaptureProfile(recorder, perfSummaries, env = process.env)
5477
5534
  // src/services/render/videoFrameCoverage.ts
5478
5535
  import { parseHTML } from "linkedom";
5479
5536
  import {
5480
- fpsToNumber as fpsToNumber6,
5537
+ fpsToNumber as fpsToNumber5,
5481
5538
  normalizeRateSpec,
5482
5539
  sourceTimeAt,
5483
5540
  toFps
@@ -5512,7 +5569,7 @@ function resolveVideoCoverageThreshold(envValue = process.env.HF_VIDEO_COVERAGE_
5512
5569
  return parsed;
5513
5570
  }
5514
5571
  function expectedFramesForClip(start, end, fps, rounding = "ceil") {
5515
- const fpsValue = fpsToNumber6(toFps(fps));
5572
+ const fpsValue = fpsToNumber5(toFps(fps));
5516
5573
  if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(fpsValue)) return 0;
5517
5574
  if (fpsValue <= 0) return 0;
5518
5575
  const duration = Math.max(0, end - start);
@@ -6774,7 +6831,7 @@ function detectShaderTransitionUsage(html) {
6774
6831
  }
6775
6832
  return false;
6776
6833
  }
6777
- async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
6834
+ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
6778
6835
  let filePath = src;
6779
6836
  if (isHttpUrl(src)) {
6780
6837
  if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
@@ -6801,45 +6858,52 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6801
6858
  if (error instanceof Error && error.stack) wrapped.stack = error.stack;
6802
6859
  return wrapped;
6803
6860
  };
6804
- return withMediaProbeSlot(async () => {
6805
- let profile;
6861
+ const probe = () => withMediaProbeSlot(async () => {
6806
6862
  try {
6807
6863
  await assertMediaPayload(filePath, elementIdentity);
6808
- profile = await probeMediaProfile2(filePath);
6809
- } catch (error) {
6810
- if (tagName !== "video") {
6811
- if (error instanceof NotMediaPayloadError2) {
6812
- log?.warn(
6813
- `[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
6814
- );
6815
- }
6816
- return { duration: null, resolvedPath: filePath };
6864
+ const profile = await probeMediaProfile2(filePath);
6865
+ if (tagName === "video") {
6866
+ return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
6817
6867
  }
6818
- throw withSrcContext(error);
6819
- }
6820
- assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
6821
- let metadata;
6822
- if (tagName === "video") {
6823
- try {
6824
- metadata = await extractMediaMetadata(filePath);
6825
- } catch (error) {
6826
- throw withSrcContext(error);
6827
- }
6828
- } else {
6829
6868
  try {
6830
- metadata = await extractAudioMetadata(filePath);
6869
+ return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
6831
6870
  } catch {
6832
- return { duration: null, resolvedPath: filePath };
6871
+ return { profile, fileDuration: null };
6833
6872
  }
6873
+ } catch (error) {
6874
+ return { error };
6834
6875
  }
6835
- const fileDuration = metadata.durationSeconds;
6836
- const duration = resolveNaturalMediaTimelineDurationFromValues(
6837
- fileDuration,
6838
- mediaStart,
6839
- playbackRate
6840
- );
6841
- return { duration, resolvedPath: filePath };
6842
6876
  });
6877
+ const key = `${tagName}\0${filePath}`;
6878
+ let probed = probes?.get(key);
6879
+ if (!probed) {
6880
+ probed = probe();
6881
+ probes?.set(key, probed);
6882
+ }
6883
+ const outcome = await probed;
6884
+ if ("error" in outcome) {
6885
+ if (tagName !== "video") {
6886
+ if (outcome.error instanceof NotMediaPayloadError2) {
6887
+ log?.warn(
6888
+ `[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
6889
+ );
6890
+ }
6891
+ return { duration: null, resolvedPath: filePath };
6892
+ }
6893
+ throw withSrcContext(outcome.error);
6894
+ }
6895
+ assertAssetMediaTypeProfile(
6896
+ tagName === "video" ? "video" : "audio",
6897
+ outcome.profile,
6898
+ elementIdentity
6899
+ );
6900
+ if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
6901
+ const duration = resolveNaturalMediaTimelineDurationFromValues(
6902
+ outcome.fileDuration,
6903
+ mediaStart,
6904
+ playbackRate
6905
+ );
6906
+ return { duration, resolvedPath: filePath };
6843
6907
  }
6844
6908
  function markInferredVariableMediaDurations(html, unresolvedMedia) {
6845
6909
  const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
@@ -6853,7 +6917,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
6853
6917
  }
6854
6918
  return changed ? document2.toString() : html;
6855
6919
  }
6856
- async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
6920
+ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
6857
6921
  const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
6858
6922
  const mediaUnresolved = unresolved.filter(
6859
6923
  (el) => (el.tagName === "video" || el.tagName === "audio") && el.src
@@ -6870,7 +6934,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6870
6934
  el.tagName,
6871
6935
  el.id,
6872
6936
  log,
6873
- urlByAbsentLocalPath
6937
+ urlByAbsentLocalPath,
6938
+ probes
6874
6939
  ).then(({ duration }) => ({ id: el.id, duration }))
6875
6940
  )
6876
6941
  );
@@ -6891,7 +6956,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6891
6956
  downloadDir,
6892
6957
  el.tagName,
6893
6958
  el.id,
6894
- log
6959
+ log,
6960
+ void 0,
6961
+ probes
6895
6962
  );
6896
6963
  return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
6897
6964
  })
@@ -6913,7 +6980,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
6913
6980
  compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
6914
6981
  return { html: compiledHtml, unresolvedCompositions };
6915
6982
  }
6916
- async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
6983
+ async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
6917
6984
  const subCompositions = /* @__PURE__ */ new Map();
6918
6985
  const { document: document2 } = parseHTML5(html);
6919
6986
  const compEls = document2.querySelectorAll("[data-composition-src]");
@@ -6941,14 +7008,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
6941
7008
  dirname7(item.filePath),
6942
7009
  downloadDir,
6943
7010
  void 0,
6944
- urlByAbsentLocalPath
7011
+ urlByAbsentLocalPath,
7012
+ probes
6945
7013
  );
6946
7014
  const nested = await parseSubCompositions(
6947
7015
  compiledSub,
6948
7016
  projectDir,
6949
7017
  downloadDir,
6950
7018
  item.nestedVisited,
6951
- urlByAbsentLocalPath
7019
+ urlByAbsentLocalPath,
7020
+ probes
6952
7021
  );
6953
7022
  return {
6954
7023
  srcPath: item.srcPath,
@@ -7718,19 +7787,22 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7718
7787
  );
7719
7788
  const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
7720
7789
  assertSubCompositionsUsable(rawHtml, projectDir);
7790
+ const probes = /* @__PURE__ */ new Map();
7721
7791
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
7722
7792
  rawHtml,
7723
7793
  projectDir,
7724
7794
  downloadDir,
7725
7795
  options.log,
7726
- options.urlByAbsentLocalPath
7796
+ options.urlByAbsentLocalPath,
7797
+ probes
7727
7798
  );
7728
7799
  const { subCompositions } = await parseSubCompositions(
7729
7800
  compiledHtml,
7730
7801
  projectDir,
7731
7802
  downloadDir,
7732
7803
  void 0,
7733
- options.urlByAbsentLocalPath
7804
+ options.urlByAbsentLocalPath,
7805
+ probes
7734
7806
  );
7735
7807
  const fullHtml = ensureFullDocument(compiledHtml);
7736
7808
  const inlinedHtml = inlineSubCompositions(
@@ -7833,9 +7905,12 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7833
7905
  }
7834
7906
  const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
7835
7907
  const masterFxChain = parseMasterFxChain(html);
7908
+ const advised = /* @__PURE__ */ new Set();
7836
7909
  for (const video of videos) {
7837
7910
  if (isHttpUrl(video.src)) continue;
7838
7911
  const videoPath = resolve7(projectDir, video.src);
7912
+ if (advised.has(videoPath)) continue;
7913
+ advised.add(videoPath);
7839
7914
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
7840
7915
  (async () => {
7841
7916
  const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
@@ -8386,13 +8461,14 @@ import {
8386
8461
  } from "@hyperframes/engine";
8387
8462
  import {
8388
8463
  MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
8389
- fpsToNumber as fpsToNumber7,
8464
+ fpsToNumber as fpsToNumber6,
8390
8465
  normalizeRateSpec as normalizeRateSpec3,
8391
8466
  readMediaStart as readMediaStart2,
8392
8467
  shiftRateLane as shiftRateLane2,
8393
8468
  sourceTimeAt as sourceTimeAt3,
8394
8469
  timeAtSourceTime
8395
8470
  } from "@hyperframes/core";
8471
+ import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
8396
8472
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
8397
8473
 
8398
8474
  // src/services/render/stages/probeFailures.ts
@@ -8430,8 +8506,15 @@ function hasAutoStartVideos(html) {
8430
8506
  const { document: document2 } = parseHTML6(html);
8431
8507
  return document2.querySelector("video[data-hf-auto-start]") !== null;
8432
8508
  }
8509
+ function variableHasValue(element, variableId, variables) {
8510
+ if (variables && Object.hasOwn(variables, variableId)) return true;
8511
+ for (let scope = element.parentElement; scope; scope = scope.parentElement) {
8512
+ if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
8513
+ if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
8514
+ }
8515
+ return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
8516
+ }
8433
8517
  function hasVariableBoundMedia(html, variables) {
8434
- if (!variables || Object.keys(variables).length === 0) return false;
8435
8518
  const { document: document2 } = parseHTML6(html);
8436
8519
  return Array.from(
8437
8520
  document2.querySelectorAll(
@@ -8439,7 +8522,7 @@ function hasVariableBoundMedia(html, variables) {
8439
8522
  )
8440
8523
  ).some((element) => {
8441
8524
  const variableId = element.getAttribute("data-var-src")?.trim();
8442
- return Boolean(variableId && Object.hasOwn(variables, variableId));
8525
+ return Boolean(variableId && variableHasValue(element, variableId, variables));
8443
8526
  });
8444
8527
  }
8445
8528
  function authoredMediaStarts(html) {
@@ -8865,7 +8948,7 @@ async function runProbeStage(input) {
8865
8948
  session.page,
8866
8949
  composition.audios.map((audio) => audio.id),
8867
8950
  composition.duration,
8868
- fpsToNumber7(job.config.fps)
8951
+ fpsToNumber6(job.config.fps)
8869
8952
  );
8870
8953
  assertNotAborted();
8871
8954
  if (automation.length > 0) {
@@ -8928,7 +9011,7 @@ async function runProbeStage(input) {
8928
9011
  }
8929
9012
  const browserProbeMs = Date.now() - probeStart;
8930
9013
  const duration = composition.duration;
8931
- const totalFrames = durationToFrameCount(duration, fpsToNumber7(job.config.fps));
9014
+ const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
8932
9015
  if (duration <= 0) {
8933
9016
  const diagnostics = [];
8934
9017
  try {
@@ -9173,10 +9256,11 @@ var VideoExtractionStageError = class extends Error {
9173
9256
  kind: failure.kind,
9174
9257
  affectedElementCount: failure.count
9175
9258
  }))
9176
- )) {
9259
+ ), ffmpegReason) {
9177
9260
  const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
9178
9261
  const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
9179
- super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
9262
+ const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
9263
+ super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
9180
9264
  this.code = code;
9181
9265
  this.retryable = retryable;
9182
9266
  this.failures = failures;
@@ -9223,7 +9307,8 @@ function buildVideoExtractionStageError(result) {
9223
9307
  retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
9224
9308
  retryable,
9225
9309
  failures,
9226
- extractionFailureMetadataFromResult(result)
9310
+ extractionFailureMetadataFromResult(result),
9311
+ result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
9227
9312
  );
9228
9313
  }
9229
9314
  function buildHdrProbeStageError(failures) {
@@ -9464,16 +9549,17 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
9464
9549
 
9465
9550
  // src/services/render/stages/audioStage.ts
9466
9551
  import { join as join20 } from "node:path";
9467
- import { rmSync as rmSync10 } from "node:fs";
9468
9552
  import {
9469
9553
  MIXED_AUDIO_FILENAME,
9470
9554
  audioFailureDiagnostics,
9471
- processCompositionAudio
9555
+ processCompositionAudio,
9556
+ removeScratch as removeScratch2,
9557
+ collectScratchLeftovers
9472
9558
  } from "@hyperframes/engine";
9473
9559
 
9474
9560
  // src/services/render/audioPadTrim.ts
9475
9561
  import { spawn } from "node:child_process";
9476
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync9 } from "node:fs";
9562
+ import { mkdtempSync as mkdtempSync3 } from "node:fs";
9477
9563
  import { dirname as dirname8, join as join19 } from "node:path";
9478
9564
  import {
9479
9565
  DEFAULT_CONFIG,
@@ -9482,6 +9568,7 @@ import {
9482
9568
  extractAudioMetadata as extractAudioMetadata3,
9483
9569
  getFfprobeBinary,
9484
9570
  ManagedChildProcess,
9571
+ removeScratch,
9485
9572
  renameWithRetry as renameWithRetry5,
9486
9573
  runFfmpeg as runFfmpeg5,
9487
9574
  trackChildProcess
@@ -9677,7 +9764,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9677
9764
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
9678
9765
  };
9679
9766
  } finally {
9680
- for (const path of plan2.cleanupPaths) rmSync9(path, { force: true });
9767
+ for (const path of plan2.cleanupPaths) removeScratch(path);
9681
9768
  }
9682
9769
  let loweredDb = 0;
9683
9770
  if (probeTruePeak) {
@@ -9740,7 +9827,7 @@ async function enforceAacTruePeak(input) {
9740
9827
  }
9741
9828
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
9742
9829
  if (measuredPath !== correctedPath) return { success: true };
9743
- renameWithRetry5(correctedPath, input.audioPath);
9830
+ renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
9744
9831
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
9745
9832
  }
9746
9833
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -9781,7 +9868,7 @@ async function enforceAacTruePeak(input) {
9781
9868
  )}`
9782
9869
  };
9783
9870
  } finally {
9784
- if (scratchDir) rmSync9(scratchDir, { recursive: true, force: true });
9871
+ if (scratchDir) removeScratch(scratchDir);
9785
9872
  }
9786
9873
  }
9787
9874
  function failResult(outputPath, target, source, error) {
@@ -9926,7 +10013,15 @@ function assertAudioNotInterrupted(failures) {
9926
10013
  ])
9927
10014
  });
9928
10015
  }
9929
- async function runAudioStage(input) {
10016
+ function runAudioStage(input) {
10017
+ return collectScratchLeftovers(
10018
+ () => runAudioStageScoped(input),
10019
+ (left) => {
10020
+ if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
10021
+ }
10022
+ );
10023
+ }
10024
+ async function runAudioStageScoped(input) {
9930
10025
  const stageStart = Date.now();
9931
10026
  const pcmSourcePath = join20(input.workDir, "audio-source.wav");
9932
10027
  const duration = input.totalFrames * input.fps.den / input.fps.num;
@@ -9948,7 +10043,7 @@ async function runAudioStage(input) {
9948
10043
  }
9949
10044
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9950
10045
  } finally {
9951
- rmSync10(pcmSourcePath, { force: true });
10046
+ removeScratch2(pcmSourcePath);
9952
10047
  }
9953
10048
  }
9954
10049
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -10210,6 +10305,7 @@ async function runCaptureStage(input) {
10210
10305
  dedupPerfs,
10211
10306
  pageErrorLines: workerPageErrors,
10212
10307
  openAttempt: input.openAttempt,
10308
+ verifyFirstSession: input.verifyFirstSession,
10213
10309
  onProgress: (progress) => {
10214
10310
  if (progress.latestWorkerPhase) {
10215
10311
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -10428,7 +10524,7 @@ import {
10428
10524
  initializeSession as initializeSession7,
10429
10525
  spawnStreamingEncoder as spawnStreamingEncoder3
10430
10526
  } from "@hyperframes/engine";
10431
- import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
10527
+ import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
10432
10528
 
10433
10529
  // src/services/hdrImageTransferCache.ts
10434
10530
  import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
@@ -11102,6 +11198,7 @@ async function runCaptureHdrStage(input) {
11102
11198
  preset,
11103
11199
  effectiveQuality,
11104
11200
  effectiveBitrate,
11201
+ deliveryEncoder,
11105
11202
  fileServer,
11106
11203
  buildCaptureOptions,
11107
11204
  createRenderVideoFrameInjector,
@@ -11163,7 +11260,7 @@ async function runCaptureHdrStage(input) {
11163
11260
  }
11164
11261
  return map;
11165
11262
  });
11166
- const fpsDecimal = fpsToNumber8(job.config.fps);
11263
+ const fpsDecimal = fpsToNumber7(job.config.fps);
11167
11264
  const transitionRanges = transitionMeta.map((t) => ({
11168
11265
  ...t,
11169
11266
  startFrame: Math.floor(t.time * fpsDecimal),
@@ -11191,6 +11288,7 @@ async function runCaptureHdrStage(input) {
11191
11288
  preset: preset.preset,
11192
11289
  quality: effectiveQuality,
11193
11290
  bitrate: effectiveBitrate,
11291
+ ...deliveryEncoder,
11194
11292
  pixelFormat: preset.pixelFormat,
11195
11293
  hdr: preset.hdr,
11196
11294
  rawInputFormat: "rgb48le"
@@ -11250,7 +11348,7 @@ async function runCaptureHdrStage(input) {
11250
11348
  beforeCaptureHook: domSession.onBeforeCapture,
11251
11349
  width,
11252
11350
  height,
11253
- fps: fpsToNumber8(job.config.fps),
11351
+ fps: fpsToNumber7(job.config.fps),
11254
11352
  compositeTransfer,
11255
11353
  nativeHdrImageIds,
11256
11354
  hdrImageBuffers,
@@ -11394,7 +11492,7 @@ import {
11394
11492
  mkdirSync as mkdirSync9,
11395
11493
  readdirSync as readdirSync4,
11396
11494
  readFileSync as readFileSync8,
11397
- rmSync as rmSync11,
11495
+ rmSync as rmSync9,
11398
11496
  statSync as statSync7,
11399
11497
  writeFileSync as writeFileSync5
11400
11498
  } from "node:fs";
@@ -11533,7 +11631,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11533
11631
  fileSize
11534
11632
  };
11535
11633
  } finally {
11536
- rmSync11(input.palettePath, { force: true });
11634
+ rmSync9(input.palettePath, { force: true });
11537
11635
  }
11538
11636
  }
11539
11637
  function startEncodeProgress(job, stage, onProgress) {
@@ -11557,6 +11655,8 @@ function videoEncoderOptions(input, engineCfg) {
11557
11655
  preset: preset.preset,
11558
11656
  quality: input.effectiveQuality,
11559
11657
  bitrate: input.effectiveBitrate,
11658
+ maxBitrate: input.maxBitrate,
11659
+ bufferSize: input.bufferSize,
11560
11660
  pixelFormat: preset.pixelFormat,
11561
11661
  vp9CpuUsed: engineCfg.vp9CpuUsed,
11562
11662
  useGpu: job.config.useGpu,
@@ -12265,7 +12365,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12265
12365
  });
12266
12366
  } : void 0,
12267
12367
  void 0,
12268
- options.cfg
12368
+ options.cfg,
12369
+ { verifyFirstSession: options.verifyFirstSession }
12269
12370
  );
12270
12371
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
12271
12372
  if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
@@ -12831,6 +12932,7 @@ function resolveRenderDebugDir() {
12831
12932
  }
12832
12933
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12833
12934
  validateHlsRenderConfig(job.config);
12935
+ validateDeliveryEncoderConfig(job.config);
12834
12936
  const debugDir = resolveRenderDebugDir();
12835
12937
  const outputDir = dirname11(outputPath);
12836
12938
  if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
@@ -12851,7 +12953,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12851
12953
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12852
12954
  return;
12853
12955
  }
12854
- rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12956
+ rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12855
12957
  });
12856
12958
  try {
12857
12959
  await executeRenderPipeline({
@@ -12909,11 +13011,14 @@ async function executeRenderPipeline(input) {
12909
13011
  const isGif = outputFormat === "gif";
12910
13012
  const isHls = outputFormat === "hls";
12911
13013
  const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
12912
- const hlsEncoderGopLock = resolveHlsEncoderGopLock(
12913
- outputFormat,
12914
- job.config.fps,
12915
- job.config.hlsSegmentSeconds
12916
- );
13014
+ const deliveryEncoder = resolveDeliveryEncoderOptions({
13015
+ format: outputFormat,
13016
+ fps: job.config.fps,
13017
+ hlsSegmentSeconds: job.config.hlsSegmentSeconds,
13018
+ keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
13019
+ maxBitrate: job.config.maxBitrate,
13020
+ bufferSize: job.config.bufferSize
13021
+ });
12917
13022
  const artifactTransaction = new ArtifactTransaction(
12918
13023
  outputPath,
12919
13024
  isPngSequence || isHls ? "directory" : "file",
@@ -13153,7 +13258,7 @@ async function executeRenderPipeline(input) {
13153
13258
  validateRenderDuration({
13154
13259
  duration: probeResult.duration,
13155
13260
  totalFrames,
13156
- fps: fpsToNumber9(job.config.fps)
13261
+ fps: fpsToNumber8(job.config.fps)
13157
13262
  });
13158
13263
  perfStages.browserProbeMs = probeResult.browserProbeMs;
13159
13264
  perfStages.compileMs = Date.now() - stage1Start;
@@ -13996,6 +14101,7 @@ async function executeRenderPipeline(input) {
13996
14101
  preset,
13997
14102
  effectiveQuality,
13998
14103
  effectiveBitrate,
14104
+ deliveryEncoder,
13999
14105
  fileServer: activeFileServer,
14000
14106
  buildCaptureOptions,
14001
14107
  createRenderVideoFrameInjector,
@@ -14020,9 +14126,7 @@ async function executeRenderPipeline(input) {
14020
14126
  } else {
14021
14127
  let streamingHandled = false;
14022
14128
  if (capturePlan.kind === "sdr_segmented") {
14023
- const segmentedPlan = capturePlan;
14024
14129
  const captureFrameStart = Date.now();
14025
- resetCaptureAttemptProgress(job);
14026
14130
  const segmentFrames = resolveSegmentFrames(process.env);
14027
14131
  const segmentImageFormat = captureOptions.format || "jpeg";
14028
14132
  const segmentPlanHash = computeSegmentPlanHash({
@@ -14037,6 +14141,9 @@ async function executeRenderPipeline(input) {
14037
14141
  preset: preset.preset,
14038
14142
  quality: effectiveQuality,
14039
14143
  bitrate: effectiveBitrate,
14144
+ maxBitrate: deliveryEncoder.maxBitrate,
14145
+ bufferSize: deliveryEncoder.bufferSize,
14146
+ gopSize: deliveryEncoder.gopSize,
14040
14147
  pixelFormat: preset.pixelFormat,
14041
14148
  imageFormat: segmentImageFormat,
14042
14149
  useGpu: job.config.useGpu === true,
@@ -14062,71 +14169,129 @@ async function executeRenderPipeline(input) {
14062
14169
  });
14063
14170
  }
14064
14171
  if (completedSegments.size === 0) {
14065
- rmSync12(segmentDir, { recursive: true, force: true });
14172
+ rmSync10(segmentDir, { recursive: true, force: true });
14066
14173
  }
14067
14174
  } else {
14068
- rmSync12(segmentDir, { recursive: true, force: true });
14175
+ rmSync10(segmentDir, { recursive: true, force: true });
14069
14176
  }
14070
- const segmentManifest = readSegmentManifest(segmentDir) ?? {
14177
+ const emptySegmentManifest = () => ({
14071
14178
  version: 1,
14072
14179
  planHash: segmentPlanHash,
14073
14180
  totalFrames,
14074
14181
  segmentFrames,
14075
14182
  completed: []
14183
+ });
14184
+ let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
14185
+ const invokeSegmented = () => {
14186
+ if (capturePlan.kind !== "sdr_segmented") {
14187
+ throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
14188
+ }
14189
+ const segmentedPlan = capturePlan;
14190
+ resetCaptureAttemptProgress(job);
14191
+ return observeRenderStage(
14192
+ observability,
14193
+ "capture_segmented",
14194
+ captureStageObservationData(),
14195
+ () => runCaptureSegmentedStage({
14196
+ fileServer: activeFileServer,
14197
+ workDir,
14198
+ framesDir,
14199
+ videoOnlyPath,
14200
+ job,
14201
+ totalFrames,
14202
+ cfg,
14203
+ plan: segmentedPlan,
14204
+ log,
14205
+ probeSession,
14206
+ outputFormat,
14207
+ streamingEncoderOptions: {
14208
+ fps: job.config.fps,
14209
+ width: captureCompositionWidth ?? width,
14210
+ height: captureCompositionHeight ?? height,
14211
+ codec: preset.codec,
14212
+ preset: preset.preset,
14213
+ quality: effectiveQuality,
14214
+ bitrate: effectiveBitrate,
14215
+ pixelFormat: preset.pixelFormat,
14216
+ vp9CpuUsed: cfg.vp9CpuUsed,
14217
+ useGpu: job.config.useGpu,
14218
+ imageFormat: segmentImageFormat,
14219
+ hdr: preset.hdr,
14220
+ // Each segment sets its own GOP (at most the interval); hls never reaches this route.
14221
+ ...deliveryEncoder
14222
+ },
14223
+ buildCaptureOptions,
14224
+ createRenderVideoFrameInjector,
14225
+ abortSignal: executionSignal,
14226
+ assertNotAborted,
14227
+ onProgress,
14228
+ dedupPerfs,
14229
+ segmentFrames,
14230
+ segmentDir,
14231
+ workerCount: segmentedPlan.workerCount,
14232
+ browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
14233
+ completedSegments,
14234
+ onSegmentComplete: (entry) => {
14235
+ segmentManifest.completed = [
14236
+ ...segmentManifest.completed.filter((e) => e.index !== entry.index),
14237
+ { ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
14238
+ ];
14239
+ writeSegmentManifest(segmentDir, segmentManifest);
14240
+ },
14241
+ updateCaptureObservability
14242
+ })
14243
+ );
14076
14244
  };
14077
- const segmentedRes = await observeRenderStage(
14078
- observability,
14079
- "capture_segmented",
14080
- captureStageObservationData(),
14081
- () => runCaptureSegmentedStage({
14082
- fileServer: activeFileServer,
14083
- workDir,
14084
- framesDir,
14085
- videoOnlyPath,
14086
- job,
14087
- totalFrames,
14088
- cfg,
14089
- plan: segmentedPlan,
14090
- log,
14091
- probeSession,
14092
- outputFormat,
14093
- streamingEncoderOptions: {
14094
- fps: job.config.fps,
14095
- width: captureCompositionWidth ?? width,
14096
- height: captureCompositionHeight ?? height,
14097
- codec: preset.codec,
14098
- preset: preset.preset,
14099
- quality: effectiveQuality,
14100
- bitrate: effectiveBitrate,
14101
- pixelFormat: preset.pixelFormat,
14102
- vp9CpuUsed: cfg.vp9CpuUsed,
14103
- useGpu: job.config.useGpu,
14104
- imageFormat: segmentImageFormat,
14105
- hdr: preset.hdr
14106
- // No hlsEncoderGopLock here: each segment sets its own GOP to
14107
- // its own length, and hls never reaches this route.
14108
- },
14109
- buildCaptureOptions,
14110
- createRenderVideoFrameInjector,
14111
- abortSignal: executionSignal,
14112
- assertNotAborted,
14113
- onProgress,
14114
- dedupPerfs,
14115
- segmentFrames,
14116
- segmentDir,
14117
- workerCount: segmentedPlan.workerCount,
14118
- browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
14119
- completedSegments,
14120
- onSegmentComplete: (entry) => {
14121
- segmentManifest.completed = [
14122
- ...segmentManifest.completed.filter((e) => e.index !== entry.index),
14123
- { ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
14124
- ];
14125
- writeSegmentManifest(segmentDir, segmentManifest);
14126
- },
14127
- updateCaptureObservability
14128
- })
14129
- );
14245
+ let segmentedRes;
14246
+ try {
14247
+ segmentedRes = await invokeSegmented();
14248
+ } catch (err) {
14249
+ const isVerifyError = isDrawElementVerificationError(err);
14250
+ const isDeStall = isDeRendererStallError(err);
14251
+ if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
14252
+ throw err;
14253
+ }
14254
+ deSelfVerifyFallback = isVerifyError;
14255
+ const t = deVerifyFallbackTelemetry(err);
14256
+ deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
14257
+ deFallbackFailedDb = t.failedDb;
14258
+ deFallbackFrameIndex = t.frameIndex;
14259
+ deFallbackThresholdDb = t.thresholdDb;
14260
+ log.warn(
14261
+ "[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
14262
+ {
14263
+ error: err instanceof Error ? err.message : String(err)
14264
+ }
14265
+ );
14266
+ observability.checkpoint(
14267
+ "capture_segmented",
14268
+ "drawElement capture failed; retrying with a fresh screenshot session"
14269
+ );
14270
+ if (probeSession) {
14271
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
14272
+ const orphaned = probeSession;
14273
+ probeSession = null;
14274
+ await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
14275
+ }
14276
+ rmSync10(segmentDir, { recursive: true, force: true });
14277
+ completedSegments = /* @__PURE__ */ new Set();
14278
+ segmentManifest = emptySegmentManifest();
14279
+ dedupPerfs.length = 0;
14280
+ capturePlan = replanAfterFailure(capturePlan, {
14281
+ kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
14282
+ });
14283
+ syncCapturePlan();
14284
+ updateCaptureObservability({
14285
+ forceScreenshot: capturePlan.forceScreenshot,
14286
+ deSelfVerifyFallback,
14287
+ deFallbackReason,
14288
+ deFallbackFailedDb,
14289
+ deFallbackFrameIndex,
14290
+ deFallbackThresholdDb
14291
+ });
14292
+ segmentedRes = await invokeSegmented();
14293
+ observability.clearFailure("capture_segmented");
14294
+ }
14130
14295
  if (segmentedRes.success) {
14131
14296
  streamingHandled = true;
14132
14297
  workerCount = segmentedRes.workerCount;
@@ -14146,7 +14311,7 @@ async function executeRenderPipeline(input) {
14146
14311
  );
14147
14312
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
14148
14313
  if (job.config.keepSegments !== true) {
14149
- rmSync12(segmentDir, { recursive: true, force: true });
14314
+ rmSync10(segmentDir, { recursive: true, force: true });
14150
14315
  }
14151
14316
  } else {
14152
14317
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -14199,7 +14364,7 @@ async function executeRenderPipeline(input) {
14199
14364
  // assemble stage's `-c copy` segmentation cuts exactly on
14200
14365
  // the segment boundary. Every other format leaves the
14201
14366
  // encoder's open-GOP output untouched.
14202
- ...hlsEncoderGopLock
14367
+ ...deliveryEncoder
14203
14368
  },
14204
14369
  buildCaptureOptions,
14205
14370
  createRenderVideoFrameInjector,
@@ -14362,7 +14527,7 @@ async function executeRenderPipeline(input) {
14362
14527
  engineConfig: cfg,
14363
14528
  // Same value the streaming encoder above spreads — the disk and
14364
14529
  // chunked-concat encoders need an identical GOP lock for HLS.
14365
- ...hlsEncoderGopLock,
14530
+ ...deliveryEncoder,
14366
14531
  abortSignal: executionSignal,
14367
14532
  assertNotAborted,
14368
14533
  onProgress
@@ -14418,7 +14583,7 @@ async function executeRenderPipeline(input) {
14418
14583
  "capture_disk",
14419
14584
  "drawElement capture failed; retrying with a fresh screenshot session"
14420
14585
  );
14421
- rmSync12(framesDir, { recursive: true, force: true });
14586
+ rmSync10(framesDir, { recursive: true, force: true });
14422
14587
  mkdirSync10(framesDir, { recursive: true });
14423
14588
  resetCaptureAttemptProgress(job);
14424
14589
  dedupPerfs.length = 0;
@@ -14516,7 +14681,7 @@ async function executeRenderPipeline(input) {
14516
14681
  buildArtifactExpectation({
14517
14682
  outputFormat,
14518
14683
  durationSeconds: job.duration,
14519
- fps: fpsToNumber9(job.config.fps),
14684
+ fps: fpsToNumber8(job.config.fps),
14520
14685
  expectedFrames: captureTotalFrames,
14521
14686
  width: outputWidth,
14522
14687
  height: outputHeight
@@ -14735,16 +14900,17 @@ import {
14735
14900
  existsSync as existsSync21,
14736
14901
  linkSync,
14737
14902
  mkdirSync as mkdirSync14,
14738
- rmSync as rmSync14,
14903
+ rmSync as rmSync12,
14739
14904
  writeFileSync as writeFileSync9
14740
14905
  } from "node:fs";
14741
14906
  import { extname as extname6, isAbsolute as isAbsolute3, join as join35, relative as relative5, sep as sep2 } from "node:path";
14742
14907
  import {
14743
- fpsToNumber as fpsToNumber10,
14908
+ fpsToNumber as fpsToNumber9,
14744
14909
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
14745
14910
  } from "@hyperframes/core";
14746
14911
  import {
14747
14912
  getEncoderPreset as getEncoderPreset4,
14913
+ keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
14748
14914
  normalizeVp9CpuUsed,
14749
14915
  resolveConfig as resolveConfig3,
14750
14916
  declareContentAddressedUrl,
@@ -15770,7 +15936,7 @@ import { join as join33 } from "node:path";
15770
15936
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
15771
15937
 
15772
15938
  // src/services/distributed/mp4Stitch.ts
15773
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as rmSync13 } from "node:fs";
15939
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
15774
15940
  import { open } from "node:fs/promises";
15775
15941
  import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
15776
15942
  import { dirname as dirname13, join as join32 } from "node:path";
@@ -16148,7 +16314,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
16148
16314
  renameWithRetry6(join32(workDir, "out.mp4"), outputPath);
16149
16315
  } finally {
16150
16316
  out.destroy();
16151
- rmSync13(workDir, { recursive: true, force: true });
16317
+ rmSync11(workDir, { recursive: true, force: true });
16152
16318
  }
16153
16319
  }
16154
16320
 
@@ -16656,7 +16822,7 @@ var FREEZE_OWNED_PLAN_FILES = [
16656
16822
  ];
16657
16823
  function removeFreezeOwnedPlanFiles(planDir) {
16658
16824
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
16659
- rmSync14(join35(planDir, relativePath), { force: true });
16825
+ rmSync12(join35(planDir, relativePath), { force: true });
16660
16826
  }
16661
16827
  }
16662
16828
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -16716,6 +16882,9 @@ function buildLockedRenderConfig(input) {
16716
16882
  preset,
16717
16883
  crf: config.crf,
16718
16884
  bitrate: config.bitrate,
16885
+ maxBitrate: config.maxBitrate,
16886
+ bufferSize: config.bufferSize,
16887
+ keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
16719
16888
  ...locksVp9CpuUsed,
16720
16889
  // GOP === chunkSize so every chunk's first frame is an IDR keyframe and
16721
16890
  // ffmpeg concat-copy round-trips losslessly.
@@ -16867,7 +17036,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16867
17036
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
16868
17037
  }
16869
17038
  const workDir = join35(planDir, ".plan-work");
16870
- rmSync14(workDir, { recursive: true, force: true });
17039
+ rmSync12(workDir, { recursive: true, force: true });
16871
17040
  mkdirSync14(workDir, { recursive: true });
16872
17041
  const compiledDir = join35(workDir, "compiled");
16873
17042
  mkdirSync14(compiledDir, { recursive: true });
@@ -16960,7 +17129,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16960
17129
  validateDistributedDuration({
16961
17130
  duration: probeResult.duration,
16962
17131
  totalFrames,
16963
- fps: fpsToNumber10(job.config.fps)
17132
+ fps: fpsToNumber9(job.config.fps)
16964
17133
  });
16965
17134
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
16966
17135
  if (probeResult.probeSession) {
@@ -17074,15 +17243,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
17074
17243
  logPlanStage(log, "extract", stageStarted);
17075
17244
  const stagedVideoFrames = join35(compiledDir, "__hyperframes_video_frames");
17076
17245
  const videoFramesDst = join35(planDir, "video-frames");
17077
- if (existsSync21(videoFramesDst)) rmSync14(videoFramesDst, { recursive: true, force: true });
17246
+ if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
17078
17247
  const videoSourcesDir = join35(planDir, "video-sources");
17079
- rmSync14(videoSourcesDir, { recursive: true, force: true });
17248
+ rmSync12(videoSourcesDir, { recursive: true, force: true });
17080
17249
  if (existsSync21(stagedVideoFrames)) {
17081
17250
  renameWithRetry7(stagedVideoFrames, videoFramesDst);
17082
17251
  } else {
17083
17252
  mkdirSync14(videoFramesDst, { recursive: true });
17084
17253
  }
17085
- if (existsSync21(finalCompiledDir)) rmSync14(finalCompiledDir, { recursive: true, force: true });
17254
+ if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
17086
17255
  renameWithRetry7(compiledDir, finalCompiledDir);
17087
17256
  const shipVideoSource = createVideoSourceShipper({
17088
17257
  planDir,
@@ -17250,7 +17419,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
17250
17419
  format: config.format
17251
17420
  };
17252
17421
  try {
17253
- rmSync14(workDir, { recursive: true, force: true });
17422
+ rmSync12(workDir, { recursive: true, force: true });
17254
17423
  } catch (err) {
17255
17424
  log.warn("[plan] failed to remove temp work dir", {
17256
17425
  workDir,
@@ -17401,6 +17570,20 @@ function validateDistributedRenderConfig(config) {
17401
17570
  `must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
17402
17571
  );
17403
17572
  }
17573
+ try {
17574
+ validateDeliveryEncoderConfig({
17575
+ format: config.format,
17576
+ fps: { num: config.fps, den: 1 },
17577
+ keyframeIntervalSeconds: config.keyframeIntervalSeconds,
17578
+ maxBitrate: config.maxBitrate,
17579
+ bufferSize: config.bufferSize
17580
+ });
17581
+ } catch (error) {
17582
+ throw new InvalidConfigError(
17583
+ "config.maxBitrate",
17584
+ error instanceof Error ? error.message : String(error)
17585
+ );
17586
+ }
17404
17587
  if (config.chunkSize !== void 0) {
17405
17588
  if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
17406
17589
  throw new InvalidConfigError(
@@ -17580,6 +17763,12 @@ function assertOptionalInteger(options, field, min = 0) {
17580
17763
  throw new Error(`Render request ${field} must be an integer >= ${min}`);
17581
17764
  }
17582
17765
  }
17766
+ function assertOptionalPositiveNumber(options, field) {
17767
+ const value = options[field];
17768
+ if (value !== void 0 && !(typeof value === "number" && value > 0)) {
17769
+ throw new Error(`Render request ${field} must be a positive number`);
17770
+ }
17771
+ }
17583
17772
  function assertOptionalString(options, field) {
17584
17773
  if (options[field] !== void 0 && typeof options[field] !== "string") {
17585
17774
  throw new Error(`Render request ${field} must be a string`);
@@ -17627,10 +17816,11 @@ function assertRequestOptionScalars(options) {
17627
17816
  assertOptionalInteger(options, "workers", 1);
17628
17817
  assertOptionalInteger(options, "maxRenderMs");
17629
17818
  assertOptionalInteger(options, "crf");
17819
+ assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
17630
17820
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
17631
17821
  assertOptionalBoolean(options, field);
17632
17822
  }
17633
- for (const field of ["entryFile", "videoBitrate"]) {
17823
+ for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
17634
17824
  assertOptionalString(options, field);
17635
17825
  }
17636
17826
  assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
@@ -17733,6 +17923,9 @@ function distributedConfigFromRequest(request, runtime = {}) {
17733
17923
  crf: options.crf,
17734
17924
  pageMotionBlur: options.pageMotionBlur,
17735
17925
  bitrate: options.videoBitrate,
17926
+ keyframeIntervalSeconds: options.keyframeIntervalSeconds,
17927
+ maxBitrate: options.maxBitrate,
17928
+ bufferSize: options.bufferSize,
17736
17929
  videoFrameFormat: options.videoFrameFormat,
17737
17930
  outputResolution: options.outputResolution,
17738
17931
  outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
@@ -17780,6 +17973,9 @@ function renderRequestFromDistributedConfig(input) {
17780
17973
  ...optionalProperty("crf", config.crf),
17781
17974
  ...optionalProperty("pageMotionBlur", config.pageMotionBlur),
17782
17975
  ...optionalProperty("videoBitrate", config.bitrate),
17976
+ ...optionalProperty("keyframeIntervalSeconds", config.keyframeIntervalSeconds),
17977
+ ...optionalProperty("maxBitrate", config.maxBitrate),
17978
+ ...optionalProperty("bufferSize", config.bufferSize),
17783
17979
  ...optionalProperty("videoFrameFormat", config.videoFrameFormat),
17784
17980
  ...optionalProperty("outputResolution", config.outputResolution),
17785
17981
  ...optionalProperty("outputResolutionAspectAgnostic", config.outputResolutionAspectAgnostic),
@@ -17825,7 +18021,7 @@ import {
17825
18021
  statSync as statSync10,
17826
18022
  mkdtempSync as mkdtempSync5,
17827
18023
  writeFileSync as writeFileSync10,
17828
- rmSync as rmSync15,
18024
+ rmSync as rmSync13,
17829
18025
  createReadStream as createReadStream5
17830
18026
  } from "node:fs";
17831
18027
  import { resolve as resolve11, dirname as dirname15, join as join38 } from "node:path";
@@ -18131,7 +18327,7 @@ function parseRenderOptions(body) {
18131
18327
  const format = parseServerFormat(body.format);
18132
18328
  const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
18133
18329
  const encoder = parseEncoderOptions(body, format ?? "mp4");
18134
- const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
18330
+ const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
18135
18331
  const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
18136
18332
  return {
18137
18333
  outputPath,
@@ -18141,6 +18337,9 @@ function parseRenderOptions(body) {
18141
18337
  maxRenderMs,
18142
18338
  crf,
18143
18339
  videoBitrate,
18340
+ keyframeIntervalSeconds,
18341
+ maxBitrate,
18342
+ bufferSize,
18144
18343
  useGpu,
18145
18344
  debug,
18146
18345
  strictness,
@@ -18175,6 +18374,9 @@ function buildRenderJobConfig(input, outputPath, log) {
18175
18374
  maxRenderMs: input.maxRenderMs,
18176
18375
  crf: input.crf,
18177
18376
  videoBitrate: input.videoBitrate,
18377
+ keyframeIntervalSeconds: input.keyframeIntervalSeconds,
18378
+ maxBitrate: input.maxBitrate,
18379
+ bufferSize: input.bufferSize,
18178
18380
  useGpu: input.useGpu,
18179
18381
  debug: input.debug,
18180
18382
  strictness: input.strictness,
@@ -18321,7 +18523,7 @@ function createArtifactStore(ttlMs) {
18321
18523
  function cleanupTempDir(dir, log) {
18322
18524
  if (!dir) return;
18323
18525
  try {
18324
- rmSync15(dir, { recursive: true, force: true });
18526
+ rmSync13(dir, { recursive: true, force: true });
18325
18527
  } catch (error) {
18326
18528
  log.warn("Failed to cleanup temp project dir", {
18327
18529
  cleanupProjectDir: dir,
@@ -18735,7 +18937,7 @@ import {
18735
18937
  mkdtempSync as mkdtempSync7,
18736
18938
  readFileSync as readFileSync14,
18737
18939
  readdirSync as readdirSync9,
18738
- rmSync as rmSync17,
18940
+ rmSync as rmSync15,
18739
18941
  statSync as statSync12,
18740
18942
  writeFileSync as writeFileSync12
18741
18943
  } from "node:fs";
@@ -18751,7 +18953,7 @@ import {
18751
18953
  linkSync as linkSync2,
18752
18954
  mkdirSync as mkdirSync16,
18753
18955
  mkdtempSync as mkdtempSync6,
18754
- rmSync as rmSync16,
18956
+ rmSync as rmSync14,
18755
18957
  statSync as statSync11,
18756
18958
  writeFileSync as writeFileSync11
18757
18959
  } from "node:fs";
@@ -18846,7 +19048,7 @@ var LocalPlanV2ArtifactPublisher = class {
18846
19048
  }
18847
19049
  renameWithRetry8(temporaryPath, destinationPath);
18848
19050
  } finally {
18849
- rmSync16(stagingDir, { recursive: true, force: true });
19051
+ rmSync14(stagingDir, { recursive: true, force: true });
18850
19052
  }
18851
19053
  }
18852
19054
  async commitManifest(manifestBytes) {
@@ -18863,7 +19065,7 @@ var LocalPlanV2ArtifactPublisher = class {
18863
19065
  }
18864
19066
  async abort() {
18865
19067
  if (!this.#committed) {
18866
- rmSync16(this.temporaryDir, { recursive: true, force: true });
19068
+ rmSync14(this.temporaryDir, { recursive: true, force: true });
18867
19069
  }
18868
19070
  }
18869
19071
  };
@@ -19132,7 +19334,7 @@ function writeBlob(sourcePath, destinationPath) {
19132
19334
  copyFileSync8(sourcePath, temporaryPath);
19133
19335
  renameWithRetry9(temporaryPath, destinationPath);
19134
19336
  } finally {
19135
- rmSync17(temporaryDir, { recursive: true, force: true });
19337
+ rmSync15(temporaryDir, { recursive: true, force: true });
19136
19338
  }
19137
19339
  }
19138
19340
  function publishBlobFor(absolutePath, digests) {
@@ -19265,7 +19467,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
19265
19467
  renameWithRetry9(tempDir, planV2Dir);
19266
19468
  return resultFromManifest(planV2Dir, publication.manifest);
19267
19469
  } catch (error) {
19268
- rmSync17(tempDir, { recursive: true, force: true });
19470
+ rmSync15(tempDir, { recursive: true, force: true });
19269
19471
  throw error;
19270
19472
  }
19271
19473
  }
@@ -19332,7 +19534,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
19332
19534
  }
19333
19535
  throw error;
19334
19536
  } finally {
19335
- rmSync17(stagingRoot, { recursive: true, force: true });
19537
+ rmSync15(stagingRoot, { recursive: true, force: true });
19336
19538
  }
19337
19539
  }
19338
19540
  async function planV2(projectDir, config, planV2Dir) {
@@ -19549,7 +19751,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
19549
19751
  );
19550
19752
  renameWithRetry9(tempDir, destinationDir);
19551
19753
  } catch (error) {
19552
- rmSync17(tempDir, { recursive: true, force: true });
19754
+ rmSync15(tempDir, { recursive: true, force: true });
19553
19755
  throw error;
19554
19756
  }
19555
19757
  return {
@@ -19613,7 +19815,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
19613
19815
  }
19614
19816
 
19615
19817
  // src/services/distributed/planV2Execution.ts
19616
- import { mkdtempSync as mkdtempSync9, rmSync as rmSync20 } from "node:fs";
19818
+ import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
19617
19819
  import { tmpdir as tmpdir5 } from "node:os";
19618
19820
  import { join as join44 } from "node:path";
19619
19821
 
@@ -19625,12 +19827,13 @@ import {
19625
19827
  mkdtempSync as mkdtempSync8,
19626
19828
  readFileSync as readFileSync15,
19627
19829
  readdirSync as readdirSync10,
19628
- rmSync as rmSync18,
19830
+ rmSync as rmSync16,
19629
19831
  statSync as statSync13,
19630
19832
  writeFileSync as writeFileSync13
19631
19833
  } from "node:fs";
19632
19834
  import { dirname as dirname18, join as join42 } from "node:path";
19633
19835
  import {
19836
+ appendRateCapArgs as appendRateCapArgs2,
19634
19837
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
19635
19838
  describeFfmpegFailure as describeFfmpegFailure4,
19636
19839
  getEncoderPreset as getEncoderPreset5,
@@ -19704,7 +19907,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
19704
19907
  });
19705
19908
  } finally {
19706
19909
  try {
19707
- rmSync18(workDir, { recursive: true, force: true });
19910
+ rmSync16(workDir, { recursive: true, force: true });
19708
19911
  } catch (err) {
19709
19912
  log.warn("[assemble] failed to remove work dir", {
19710
19913
  workDir,
@@ -19766,6 +19969,9 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
19766
19969
  "-r",
19767
19970
  fpsArg
19768
19971
  ];
19972
+ if (encoder.keyframeIntervalFrames !== void 0)
19973
+ args.push("-g", String(encoder.keyframeIntervalFrames));
19974
+ appendRateCapArgs2(args, encoder);
19769
19975
  appendSoftwareEncoderThreadsArg(args, "libx264");
19770
19976
  return args;
19771
19977
  }
@@ -19902,7 +20108,7 @@ function readCfrEncoder(planDir, format) {
19902
20108
  return encoderJson;
19903
20109
  }
19904
20110
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
19905
- if (existsSync27(outputPath)) rmSync18(outputPath, { recursive: true, force: true });
20111
+ if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
19906
20112
  mkdirSync18(outputPath, { recursive: true });
19907
20113
  let globalIdx = 0;
19908
20114
  for (const chunkDir of chunkPaths) {
@@ -19947,7 +20153,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19947
20153
 
19948
20154
  // src/services/distributed/renderChunk.ts
19949
20155
  import { randomBytes } from "node:crypto";
19950
- import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync16, readdirSync as readdirSync11, rmSync as rmSync19, writeFileSync as writeFileSync14 } from "node:fs";
20156
+ import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync16, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync14 } from "node:fs";
19951
20157
  import { cpus as cpus3 } from "node:os";
19952
20158
  import { join as join43 } from "node:path";
19953
20159
  import {
@@ -20043,16 +20249,18 @@ function observedChunkCaptureMode(perfs) {
20043
20249
  }
20044
20250
  return mode;
20045
20251
  }
20252
+ var PROBE_SELECTED_SCREENSHOT = "[renderChunk] probe selected screenshot capture";
20046
20253
  function shouldRetryChunkCaptureWithScreenshot(error) {
20047
20254
  const failure = classifyCaptureFailure4(error);
20048
20255
  if (failure.kind === "cancelled" || failure.kind === "memory_exhaustion") return false;
20049
- return /HeadlessExperimental\.beginFrame/i.test(failure.message) || /beginFrame probe timeout/i.test(failure.message) || /Another frame is pending|Frame still pending/i.test(failure.message);
20256
+ return /HeadlessExperimental\.beginFrame/i.test(failure.message) || /beginFrame probe timeout/i.test(failure.message) || failure.message.includes(PROBE_SELECTED_SCREENSHOT) || /Another frame is pending|Frame still pending/i.test(failure.message);
20050
20257
  }
20051
20258
  async function runCaptureWithScreenshotFallback(input) {
20052
20259
  try {
20053
20260
  return await input.run(input.forceScreenshot);
20054
20261
  } catch (error) {
20055
- if (input.forceScreenshot || !shouldRetryChunkCaptureWithScreenshot(error)) throw error;
20262
+ if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
20263
+ if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
20056
20264
  input.onFallback?.(error);
20057
20265
  await input.resetForScreenshotRetry();
20058
20266
  return await input.run(true);
@@ -20261,7 +20469,24 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20261
20469
  let extraction;
20262
20470
  const overlap = { current: null };
20263
20471
  try {
20264
- if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
20472
+ const verifyInWorker = chunkWorkerCount > 1 && !forceScreenshotForChunk;
20473
+ let boxesMeasured;
20474
+ let boxesFailed;
20475
+ const boxesReady = new Promise((resolve13, reject) => {
20476
+ boxesMeasured = resolve13;
20477
+ boxesFailed = reject;
20478
+ });
20479
+ boxesReady.catch(() => {
20480
+ });
20481
+ const measureBoxes = async (page) => {
20482
+ shownBoxes = await measureVideoShownBoxes(
20483
+ page,
20484
+ [...shownFrames.keys()],
20485
+ chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
20486
+ );
20487
+ boxesMeasured();
20488
+ };
20489
+ if (chunkWorkerCount === 1) {
20265
20490
  const bootStarted = Date.now();
20266
20491
  session = await createVerifiedDistributedCaptureSession(
20267
20492
  fileServer.url,
@@ -20270,11 +20495,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20270
20495
  cfg
20271
20496
  );
20272
20497
  sessionBootMs = Date.now() - bootStarted;
20273
- shownBoxes = await measureVideoShownBoxes(
20274
- session.page,
20275
- [...shownFrames.keys()],
20276
- chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
20277
- );
20498
+ await measureBoxes(session.page);
20278
20499
  const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
20279
20500
  const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
20280
20501
  if (browserSelectedScreenshot) {
@@ -20283,10 +20504,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20283
20504
  "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk",
20284
20505
  { chunkIndex }
20285
20506
  );
20286
- if (chunkWorkerCount > 1) {
20287
- await closeCaptureSession11(session);
20288
- session = null;
20289
- }
20290
20507
  } else if (beginFrameStalled) {
20291
20508
  forceScreenshotForChunk = true;
20292
20509
  log.warn(
@@ -20296,41 +20513,66 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20296
20513
  await closeCaptureSession11(session).catch(() => {
20297
20514
  });
20298
20515
  session = null;
20299
- if (chunkWorkerCount === 1) {
20300
- const screenshotBootStarted = Date.now();
20301
- const screenshotCfg = {
20302
- ...cfg,
20303
- forceScreenshot: true,
20304
- forceScreenshotExplicitlyOptedOut: false
20305
- };
20306
- session = await createVerifiedDistributedCaptureSession(
20307
- fileServer.url,
20308
- framesDir,
20309
- captureOptions,
20310
- screenshotCfg
20516
+ const screenshotBootStarted = Date.now();
20517
+ const screenshotCfg = {
20518
+ ...cfg,
20519
+ forceScreenshot: true,
20520
+ forceScreenshotExplicitlyOptedOut: false
20521
+ };
20522
+ session = await createVerifiedDistributedCaptureSession(
20523
+ fileServer.url,
20524
+ framesDir,
20525
+ captureOptions,
20526
+ screenshotCfg
20527
+ );
20528
+ sessionBootMs += Date.now() - screenshotBootStarted;
20529
+ }
20530
+ } else if (!verifyInWorker) {
20531
+ boxesMeasured();
20532
+ }
20533
+ let verification;
20534
+ const verifyFirstWorker = (first) => verification ??= (async () => {
20535
+ sessionBootMs = Date.now() - verifyStarted;
20536
+ try {
20537
+ await measureBoxes(first.page);
20538
+ const selectedScreenshot = first.launchCaptureMode === "screenshot";
20539
+ if (selectedScreenshot || await beginFrameSessionNeedsScreenshotFallback(first)) {
20540
+ forceScreenshotForChunk = true;
20541
+ log.warn(
20542
+ selectedScreenshot ? "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk" : "[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
20543
+ { chunkIndex }
20311
20544
  );
20312
- sessionBootMs += Date.now() - screenshotBootStarted;
20545
+ throw new Error(PROBE_SELECTED_SCREENSHOT);
20313
20546
  }
20314
- } else if (chunkWorkerCount > 1) {
20315
- await closeCaptureSession11(session);
20316
- session = null;
20547
+ } catch (error) {
20548
+ if (!forceScreenshotForChunk) {
20549
+ verifyFailure = error;
20550
+ boxesFailed(error);
20551
+ }
20552
+ throw error;
20317
20553
  }
20318
- }
20319
- const extractStarted = Date.now();
20320
- const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
20321
- planDir,
20322
- planVideos,
20323
- frames: chunkFrameNumbers,
20324
- fps: chunkFps,
20325
- outputRoot: videoFramesDir,
20326
- cfg,
20327
- remoteSources,
20328
- shownBoxes
20329
- }).then((frames) => {
20330
- videoExtractMs = Date.now() - extractStarted;
20331
- return frames;
20332
- }) : Promise.resolve([]);
20554
+ })();
20555
+ let verifyStarted = 0;
20556
+ let verifyFailure;
20557
+ const deferredVideoFrames = planVideos ? boxesReady.then(() => {
20558
+ const extractStarted = Date.now();
20559
+ return extractDeferredVideoFramesForChunk({
20560
+ planDir,
20561
+ planVideos,
20562
+ frames: chunkFrameNumbers,
20563
+ fps: chunkFps,
20564
+ outputRoot: videoFramesDir,
20565
+ cfg,
20566
+ remoteSources,
20567
+ shownBoxes
20568
+ }).then((frames) => {
20569
+ videoExtractMs = Date.now() - extractStarted;
20570
+ return frames;
20571
+ });
20572
+ }) : boxesReady.then(() => []);
20333
20573
  extraction = deferredVideoFrames;
20574
+ deferredVideoFrames.catch(() => {
20575
+ });
20334
20576
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
20335
20577
  (deferred) => createFrameLookupTable5(
20336
20578
  planVideos.videos,
@@ -20402,11 +20644,14 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20402
20644
  // first frame is an IDR keyframe and concat-copy at assemble time
20403
20645
  // round-trips losslessly.
20404
20646
  lockGopForChunkConcat: !isPngSequence,
20405
- gopSize: framesInChunk
20647
+ gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
20648
+ maxBitrate: encoder.maxBitrate,
20649
+ bufferSize: encoder.bufferSize
20406
20650
  };
20407
20651
  const captureStarted = Date.now();
20408
20652
  captureMode = await runCaptureWithScreenshotFallback({
20409
20653
  forceScreenshot: forceScreenshotForChunk,
20654
+ canRetry: () => verifyFailure === void 0,
20410
20655
  // fallow-ignore-next-line complexity
20411
20656
  run: async (forceScreenshot) => {
20412
20657
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : {
@@ -20436,6 +20681,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20436
20681
  }
20437
20682
  const captureSession = session;
20438
20683
  session = null;
20684
+ verifyStarted = Date.now();
20439
20685
  overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus3().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
20440
20686
  await observeRenderStage(
20441
20687
  new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
@@ -20463,7 +20709,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20463
20709
  frameRange: range,
20464
20710
  onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20465
20711
  interleaveFrames: interleavesCapture(overlap.current !== null, options),
20466
- openAttempt
20712
+ openAttempt,
20713
+ verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
20467
20714
  }),
20468
20715
  { stall: frameStallRule(job) }
20469
20716
  ).catch(async (captureError) => {
@@ -20474,7 +20721,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20474
20721
  return observedChunkCaptureMode(capturePerfs);
20475
20722
  },
20476
20723
  resetForScreenshotRetry: () => {
20477
- rmSync19(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20724
+ rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20725
+ boxesMeasured();
20478
20726
  mkdirSync19(framesDir, { recursive: true });
20479
20727
  capturePerfs.length = 0;
20480
20728
  captureAttempts.length = 0;
@@ -20490,6 +20738,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20490
20738
  );
20491
20739
  }
20492
20740
  }).catch(async (captureError) => {
20741
+ if (verifyFailure !== void 0) throw verifyFailure;
20742
+ boxesFailed(captureError);
20493
20743
  await deferredVideoFrames;
20494
20744
  throw captureError;
20495
20745
  });
@@ -20517,8 +20767,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20517
20767
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
20518
20768
  timeoutMs: cfg.ffmpegProcessTimeout
20519
20769
  });
20520
- rmSync19(videoOnlyPath, { force: true });
20521
- rmSync19(audioScratchPath, { force: true });
20770
+ rmSync17(videoOnlyPath, { force: true });
20771
+ rmSync17(audioScratchPath, { force: true });
20522
20772
  }
20523
20773
  encodeStageMs = Date.now() - encodeStarted;
20524
20774
  } finally {
@@ -20568,8 +20818,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20568
20818
  writeFileSync14(perfPath, `${JSON.stringify(perfPayload, null, 2)}
20569
20819
  `, "utf-8");
20570
20820
  try {
20571
- rmSync19(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20572
- rmSync19(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20821
+ rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20822
+ rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20573
20823
  } catch (err) {
20574
20824
  log.warn("[renderChunk] failed to remove work dir", {
20575
20825
  workDir,
@@ -20604,7 +20854,7 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
20604
20854
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
20605
20855
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
20606
20856
  } finally {
20607
- rmSync20(workRoot, { recursive: true, force: true });
20857
+ rmSync18(workRoot, { recursive: true, force: true });
20608
20858
  }
20609
20859
  }
20610
20860
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
@@ -20624,7 +20874,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
20624
20874
  options
20625
20875
  );
20626
20876
  } finally {
20627
- rmSync20(workRoot, { recursive: true, force: true });
20877
+ rmSync18(workRoot, { recursive: true, force: true });
20628
20878
  }
20629
20879
  }
20630
20880