@hyperframes/producer 0.8.206 → 0.8.207

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 };
6817
- }
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);
6864
+ const profile = await probeMediaProfile2(filePath);
6865
+ if (tagName === "video") {
6866
+ return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
6827
6867
  }
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 {
@@ -9464,16 +9547,17 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
9464
9547
 
9465
9548
  // src/services/render/stages/audioStage.ts
9466
9549
  import { join as join20 } from "node:path";
9467
- import { rmSync as rmSync10 } from "node:fs";
9468
9550
  import {
9469
9551
  MIXED_AUDIO_FILENAME,
9470
9552
  audioFailureDiagnostics,
9471
- processCompositionAudio
9553
+ processCompositionAudio,
9554
+ removeScratch as removeScratch2,
9555
+ collectScratchLeftovers
9472
9556
  } from "@hyperframes/engine";
9473
9557
 
9474
9558
  // src/services/render/audioPadTrim.ts
9475
9559
  import { spawn } from "node:child_process";
9476
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync9 } from "node:fs";
9560
+ import { mkdtempSync as mkdtempSync3 } from "node:fs";
9477
9561
  import { dirname as dirname8, join as join19 } from "node:path";
9478
9562
  import {
9479
9563
  DEFAULT_CONFIG,
@@ -9482,6 +9566,7 @@ import {
9482
9566
  extractAudioMetadata as extractAudioMetadata3,
9483
9567
  getFfprobeBinary,
9484
9568
  ManagedChildProcess,
9569
+ removeScratch,
9485
9570
  renameWithRetry as renameWithRetry5,
9486
9571
  runFfmpeg as runFfmpeg5,
9487
9572
  trackChildProcess
@@ -9677,7 +9762,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9677
9762
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
9678
9763
  };
9679
9764
  } finally {
9680
- for (const path of plan2.cleanupPaths) rmSync9(path, { force: true });
9765
+ for (const path of plan2.cleanupPaths) removeScratch(path);
9681
9766
  }
9682
9767
  let loweredDb = 0;
9683
9768
  if (probeTruePeak) {
@@ -9740,7 +9825,7 @@ async function enforceAacTruePeak(input) {
9740
9825
  }
9741
9826
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
9742
9827
  if (measuredPath !== correctedPath) return { success: true };
9743
- renameWithRetry5(correctedPath, input.audioPath);
9828
+ renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
9744
9829
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
9745
9830
  }
9746
9831
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -9781,7 +9866,7 @@ async function enforceAacTruePeak(input) {
9781
9866
  )}`
9782
9867
  };
9783
9868
  } finally {
9784
- if (scratchDir) rmSync9(scratchDir, { recursive: true, force: true });
9869
+ if (scratchDir) removeScratch(scratchDir);
9785
9870
  }
9786
9871
  }
9787
9872
  function failResult(outputPath, target, source, error) {
@@ -9926,7 +10011,15 @@ function assertAudioNotInterrupted(failures) {
9926
10011
  ])
9927
10012
  });
9928
10013
  }
9929
- async function runAudioStage(input) {
10014
+ function runAudioStage(input) {
10015
+ return collectScratchLeftovers(
10016
+ () => runAudioStageScoped(input),
10017
+ (left) => {
10018
+ if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
10019
+ }
10020
+ );
10021
+ }
10022
+ async function runAudioStageScoped(input) {
9930
10023
  const stageStart = Date.now();
9931
10024
  const pcmSourcePath = join20(input.workDir, "audio-source.wav");
9932
10025
  const duration = input.totalFrames * input.fps.den / input.fps.num;
@@ -9948,7 +10041,7 @@ async function runAudioStage(input) {
9948
10041
  }
9949
10042
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9950
10043
  } finally {
9951
- rmSync10(pcmSourcePath, { force: true });
10044
+ removeScratch2(pcmSourcePath);
9952
10045
  }
9953
10046
  }
9954
10047
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -10428,7 +10521,7 @@ import {
10428
10521
  initializeSession as initializeSession7,
10429
10522
  spawnStreamingEncoder as spawnStreamingEncoder3
10430
10523
  } from "@hyperframes/engine";
10431
- import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
10524
+ import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
10432
10525
 
10433
10526
  // src/services/hdrImageTransferCache.ts
10434
10527
  import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
@@ -11102,6 +11195,7 @@ async function runCaptureHdrStage(input) {
11102
11195
  preset,
11103
11196
  effectiveQuality,
11104
11197
  effectiveBitrate,
11198
+ deliveryEncoder,
11105
11199
  fileServer,
11106
11200
  buildCaptureOptions,
11107
11201
  createRenderVideoFrameInjector,
@@ -11163,7 +11257,7 @@ async function runCaptureHdrStage(input) {
11163
11257
  }
11164
11258
  return map;
11165
11259
  });
11166
- const fpsDecimal = fpsToNumber8(job.config.fps);
11260
+ const fpsDecimal = fpsToNumber7(job.config.fps);
11167
11261
  const transitionRanges = transitionMeta.map((t) => ({
11168
11262
  ...t,
11169
11263
  startFrame: Math.floor(t.time * fpsDecimal),
@@ -11191,6 +11285,7 @@ async function runCaptureHdrStage(input) {
11191
11285
  preset: preset.preset,
11192
11286
  quality: effectiveQuality,
11193
11287
  bitrate: effectiveBitrate,
11288
+ ...deliveryEncoder,
11194
11289
  pixelFormat: preset.pixelFormat,
11195
11290
  hdr: preset.hdr,
11196
11291
  rawInputFormat: "rgb48le"
@@ -11250,7 +11345,7 @@ async function runCaptureHdrStage(input) {
11250
11345
  beforeCaptureHook: domSession.onBeforeCapture,
11251
11346
  width,
11252
11347
  height,
11253
- fps: fpsToNumber8(job.config.fps),
11348
+ fps: fpsToNumber7(job.config.fps),
11254
11349
  compositeTransfer,
11255
11350
  nativeHdrImageIds,
11256
11351
  hdrImageBuffers,
@@ -11394,7 +11489,7 @@ import {
11394
11489
  mkdirSync as mkdirSync9,
11395
11490
  readdirSync as readdirSync4,
11396
11491
  readFileSync as readFileSync8,
11397
- rmSync as rmSync11,
11492
+ rmSync as rmSync9,
11398
11493
  statSync as statSync7,
11399
11494
  writeFileSync as writeFileSync5
11400
11495
  } from "node:fs";
@@ -11533,7 +11628,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11533
11628
  fileSize
11534
11629
  };
11535
11630
  } finally {
11536
- rmSync11(input.palettePath, { force: true });
11631
+ rmSync9(input.palettePath, { force: true });
11537
11632
  }
11538
11633
  }
11539
11634
  function startEncodeProgress(job, stage, onProgress) {
@@ -11557,6 +11652,8 @@ function videoEncoderOptions(input, engineCfg) {
11557
11652
  preset: preset.preset,
11558
11653
  quality: input.effectiveQuality,
11559
11654
  bitrate: input.effectiveBitrate,
11655
+ maxBitrate: input.maxBitrate,
11656
+ bufferSize: input.bufferSize,
11560
11657
  pixelFormat: preset.pixelFormat,
11561
11658
  vp9CpuUsed: engineCfg.vp9CpuUsed,
11562
11659
  useGpu: job.config.useGpu,
@@ -12831,6 +12928,7 @@ function resolveRenderDebugDir() {
12831
12928
  }
12832
12929
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12833
12930
  validateHlsRenderConfig(job.config);
12931
+ validateDeliveryEncoderConfig(job.config);
12834
12932
  const debugDir = resolveRenderDebugDir();
12835
12933
  const outputDir = dirname11(outputPath);
12836
12934
  if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
@@ -12851,7 +12949,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12851
12949
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12852
12950
  return;
12853
12951
  }
12854
- rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12952
+ rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12855
12953
  });
12856
12954
  try {
12857
12955
  await executeRenderPipeline({
@@ -12909,11 +13007,14 @@ async function executeRenderPipeline(input) {
12909
13007
  const isGif = outputFormat === "gif";
12910
13008
  const isHls = outputFormat === "hls";
12911
13009
  const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
12912
- const hlsEncoderGopLock = resolveHlsEncoderGopLock(
12913
- outputFormat,
12914
- job.config.fps,
12915
- job.config.hlsSegmentSeconds
12916
- );
13010
+ const deliveryEncoder = resolveDeliveryEncoderOptions({
13011
+ format: outputFormat,
13012
+ fps: job.config.fps,
13013
+ hlsSegmentSeconds: job.config.hlsSegmentSeconds,
13014
+ keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
13015
+ maxBitrate: job.config.maxBitrate,
13016
+ bufferSize: job.config.bufferSize
13017
+ });
12917
13018
  const artifactTransaction = new ArtifactTransaction(
12918
13019
  outputPath,
12919
13020
  isPngSequence || isHls ? "directory" : "file",
@@ -13153,7 +13254,7 @@ async function executeRenderPipeline(input) {
13153
13254
  validateRenderDuration({
13154
13255
  duration: probeResult.duration,
13155
13256
  totalFrames,
13156
- fps: fpsToNumber9(job.config.fps)
13257
+ fps: fpsToNumber8(job.config.fps)
13157
13258
  });
13158
13259
  perfStages.browserProbeMs = probeResult.browserProbeMs;
13159
13260
  perfStages.compileMs = Date.now() - stage1Start;
@@ -13996,6 +14097,7 @@ async function executeRenderPipeline(input) {
13996
14097
  preset,
13997
14098
  effectiveQuality,
13998
14099
  effectiveBitrate,
14100
+ deliveryEncoder,
13999
14101
  fileServer: activeFileServer,
14000
14102
  buildCaptureOptions,
14001
14103
  createRenderVideoFrameInjector,
@@ -14020,9 +14122,7 @@ async function executeRenderPipeline(input) {
14020
14122
  } else {
14021
14123
  let streamingHandled = false;
14022
14124
  if (capturePlan.kind === "sdr_segmented") {
14023
- const segmentedPlan = capturePlan;
14024
14125
  const captureFrameStart = Date.now();
14025
- resetCaptureAttemptProgress(job);
14026
14126
  const segmentFrames = resolveSegmentFrames(process.env);
14027
14127
  const segmentImageFormat = captureOptions.format || "jpeg";
14028
14128
  const segmentPlanHash = computeSegmentPlanHash({
@@ -14037,6 +14137,9 @@ async function executeRenderPipeline(input) {
14037
14137
  preset: preset.preset,
14038
14138
  quality: effectiveQuality,
14039
14139
  bitrate: effectiveBitrate,
14140
+ maxBitrate: deliveryEncoder.maxBitrate,
14141
+ bufferSize: deliveryEncoder.bufferSize,
14142
+ gopSize: deliveryEncoder.gopSize,
14040
14143
  pixelFormat: preset.pixelFormat,
14041
14144
  imageFormat: segmentImageFormat,
14042
14145
  useGpu: job.config.useGpu === true,
@@ -14062,71 +14165,129 @@ async function executeRenderPipeline(input) {
14062
14165
  });
14063
14166
  }
14064
14167
  if (completedSegments.size === 0) {
14065
- rmSync12(segmentDir, { recursive: true, force: true });
14168
+ rmSync10(segmentDir, { recursive: true, force: true });
14066
14169
  }
14067
14170
  } else {
14068
- rmSync12(segmentDir, { recursive: true, force: true });
14171
+ rmSync10(segmentDir, { recursive: true, force: true });
14069
14172
  }
14070
- const segmentManifest = readSegmentManifest(segmentDir) ?? {
14173
+ const emptySegmentManifest = () => ({
14071
14174
  version: 1,
14072
14175
  planHash: segmentPlanHash,
14073
14176
  totalFrames,
14074
14177
  segmentFrames,
14075
14178
  completed: []
14179
+ });
14180
+ let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
14181
+ const invokeSegmented = () => {
14182
+ if (capturePlan.kind !== "sdr_segmented") {
14183
+ throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
14184
+ }
14185
+ const segmentedPlan = capturePlan;
14186
+ resetCaptureAttemptProgress(job);
14187
+ return observeRenderStage(
14188
+ observability,
14189
+ "capture_segmented",
14190
+ captureStageObservationData(),
14191
+ () => runCaptureSegmentedStage({
14192
+ fileServer: activeFileServer,
14193
+ workDir,
14194
+ framesDir,
14195
+ videoOnlyPath,
14196
+ job,
14197
+ totalFrames,
14198
+ cfg,
14199
+ plan: segmentedPlan,
14200
+ log,
14201
+ probeSession,
14202
+ outputFormat,
14203
+ streamingEncoderOptions: {
14204
+ fps: job.config.fps,
14205
+ width: captureCompositionWidth ?? width,
14206
+ height: captureCompositionHeight ?? height,
14207
+ codec: preset.codec,
14208
+ preset: preset.preset,
14209
+ quality: effectiveQuality,
14210
+ bitrate: effectiveBitrate,
14211
+ pixelFormat: preset.pixelFormat,
14212
+ vp9CpuUsed: cfg.vp9CpuUsed,
14213
+ useGpu: job.config.useGpu,
14214
+ imageFormat: segmentImageFormat,
14215
+ hdr: preset.hdr,
14216
+ // Each segment sets its own GOP (at most the interval); hls never reaches this route.
14217
+ ...deliveryEncoder
14218
+ },
14219
+ buildCaptureOptions,
14220
+ createRenderVideoFrameInjector,
14221
+ abortSignal: executionSignal,
14222
+ assertNotAborted,
14223
+ onProgress,
14224
+ dedupPerfs,
14225
+ segmentFrames,
14226
+ segmentDir,
14227
+ workerCount: segmentedPlan.workerCount,
14228
+ browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
14229
+ completedSegments,
14230
+ onSegmentComplete: (entry) => {
14231
+ segmentManifest.completed = [
14232
+ ...segmentManifest.completed.filter((e) => e.index !== entry.index),
14233
+ { ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
14234
+ ];
14235
+ writeSegmentManifest(segmentDir, segmentManifest);
14236
+ },
14237
+ updateCaptureObservability
14238
+ })
14239
+ );
14076
14240
  };
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
- );
14241
+ let segmentedRes;
14242
+ try {
14243
+ segmentedRes = await invokeSegmented();
14244
+ } catch (err) {
14245
+ const isVerifyError = isDrawElementVerificationError(err);
14246
+ const isDeStall = isDeRendererStallError(err);
14247
+ if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
14248
+ throw err;
14249
+ }
14250
+ deSelfVerifyFallback = isVerifyError;
14251
+ const t = deVerifyFallbackTelemetry(err);
14252
+ deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
14253
+ deFallbackFailedDb = t.failedDb;
14254
+ deFallbackFrameIndex = t.frameIndex;
14255
+ deFallbackThresholdDb = t.thresholdDb;
14256
+ log.warn(
14257
+ "[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
14258
+ {
14259
+ error: err instanceof Error ? err.message : String(err)
14260
+ }
14261
+ );
14262
+ observability.checkpoint(
14263
+ "capture_segmented",
14264
+ "drawElement capture failed; retrying with a fresh screenshot session"
14265
+ );
14266
+ if (probeSession) {
14267
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
14268
+ const orphaned = probeSession;
14269
+ probeSession = null;
14270
+ await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
14271
+ }
14272
+ rmSync10(segmentDir, { recursive: true, force: true });
14273
+ completedSegments = /* @__PURE__ */ new Set();
14274
+ segmentManifest = emptySegmentManifest();
14275
+ dedupPerfs.length = 0;
14276
+ capturePlan = replanAfterFailure(capturePlan, {
14277
+ kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
14278
+ });
14279
+ syncCapturePlan();
14280
+ updateCaptureObservability({
14281
+ forceScreenshot: capturePlan.forceScreenshot,
14282
+ deSelfVerifyFallback,
14283
+ deFallbackReason,
14284
+ deFallbackFailedDb,
14285
+ deFallbackFrameIndex,
14286
+ deFallbackThresholdDb
14287
+ });
14288
+ segmentedRes = await invokeSegmented();
14289
+ observability.clearFailure("capture_segmented");
14290
+ }
14130
14291
  if (segmentedRes.success) {
14131
14292
  streamingHandled = true;
14132
14293
  workerCount = segmentedRes.workerCount;
@@ -14146,7 +14307,7 @@ async function executeRenderPipeline(input) {
14146
14307
  );
14147
14308
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
14148
14309
  if (job.config.keepSegments !== true) {
14149
- rmSync12(segmentDir, { recursive: true, force: true });
14310
+ rmSync10(segmentDir, { recursive: true, force: true });
14150
14311
  }
14151
14312
  } else {
14152
14313
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -14199,7 +14360,7 @@ async function executeRenderPipeline(input) {
14199
14360
  // assemble stage's `-c copy` segmentation cuts exactly on
14200
14361
  // the segment boundary. Every other format leaves the
14201
14362
  // encoder's open-GOP output untouched.
14202
- ...hlsEncoderGopLock
14363
+ ...deliveryEncoder
14203
14364
  },
14204
14365
  buildCaptureOptions,
14205
14366
  createRenderVideoFrameInjector,
@@ -14362,7 +14523,7 @@ async function executeRenderPipeline(input) {
14362
14523
  engineConfig: cfg,
14363
14524
  // Same value the streaming encoder above spreads — the disk and
14364
14525
  // chunked-concat encoders need an identical GOP lock for HLS.
14365
- ...hlsEncoderGopLock,
14526
+ ...deliveryEncoder,
14366
14527
  abortSignal: executionSignal,
14367
14528
  assertNotAborted,
14368
14529
  onProgress
@@ -14418,7 +14579,7 @@ async function executeRenderPipeline(input) {
14418
14579
  "capture_disk",
14419
14580
  "drawElement capture failed; retrying with a fresh screenshot session"
14420
14581
  );
14421
- rmSync12(framesDir, { recursive: true, force: true });
14582
+ rmSync10(framesDir, { recursive: true, force: true });
14422
14583
  mkdirSync10(framesDir, { recursive: true });
14423
14584
  resetCaptureAttemptProgress(job);
14424
14585
  dedupPerfs.length = 0;
@@ -14516,7 +14677,7 @@ async function executeRenderPipeline(input) {
14516
14677
  buildArtifactExpectation({
14517
14678
  outputFormat,
14518
14679
  durationSeconds: job.duration,
14519
- fps: fpsToNumber9(job.config.fps),
14680
+ fps: fpsToNumber8(job.config.fps),
14520
14681
  expectedFrames: captureTotalFrames,
14521
14682
  width: outputWidth,
14522
14683
  height: outputHeight
@@ -14735,16 +14896,17 @@ import {
14735
14896
  existsSync as existsSync21,
14736
14897
  linkSync,
14737
14898
  mkdirSync as mkdirSync14,
14738
- rmSync as rmSync14,
14899
+ rmSync as rmSync12,
14739
14900
  writeFileSync as writeFileSync9
14740
14901
  } from "node:fs";
14741
14902
  import { extname as extname6, isAbsolute as isAbsolute3, join as join35, relative as relative5, sep as sep2 } from "node:path";
14742
14903
  import {
14743
- fpsToNumber as fpsToNumber10,
14904
+ fpsToNumber as fpsToNumber9,
14744
14905
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
14745
14906
  } from "@hyperframes/core";
14746
14907
  import {
14747
14908
  getEncoderPreset as getEncoderPreset4,
14909
+ keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
14748
14910
  normalizeVp9CpuUsed,
14749
14911
  resolveConfig as resolveConfig3,
14750
14912
  declareContentAddressedUrl,
@@ -15770,7 +15932,7 @@ import { join as join33 } from "node:path";
15770
15932
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
15771
15933
 
15772
15934
  // src/services/distributed/mp4Stitch.ts
15773
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as rmSync13 } from "node:fs";
15935
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
15774
15936
  import { open } from "node:fs/promises";
15775
15937
  import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
15776
15938
  import { dirname as dirname13, join as join32 } from "node:path";
@@ -16148,7 +16310,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
16148
16310
  renameWithRetry6(join32(workDir, "out.mp4"), outputPath);
16149
16311
  } finally {
16150
16312
  out.destroy();
16151
- rmSync13(workDir, { recursive: true, force: true });
16313
+ rmSync11(workDir, { recursive: true, force: true });
16152
16314
  }
16153
16315
  }
16154
16316
 
@@ -16656,7 +16818,7 @@ var FREEZE_OWNED_PLAN_FILES = [
16656
16818
  ];
16657
16819
  function removeFreezeOwnedPlanFiles(planDir) {
16658
16820
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
16659
- rmSync14(join35(planDir, relativePath), { force: true });
16821
+ rmSync12(join35(planDir, relativePath), { force: true });
16660
16822
  }
16661
16823
  }
16662
16824
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -16716,6 +16878,9 @@ function buildLockedRenderConfig(input) {
16716
16878
  preset,
16717
16879
  crf: config.crf,
16718
16880
  bitrate: config.bitrate,
16881
+ maxBitrate: config.maxBitrate,
16882
+ bufferSize: config.bufferSize,
16883
+ keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
16719
16884
  ...locksVp9CpuUsed,
16720
16885
  // GOP === chunkSize so every chunk's first frame is an IDR keyframe and
16721
16886
  // ffmpeg concat-copy round-trips losslessly.
@@ -16867,7 +17032,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16867
17032
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
16868
17033
  }
16869
17034
  const workDir = join35(planDir, ".plan-work");
16870
- rmSync14(workDir, { recursive: true, force: true });
17035
+ rmSync12(workDir, { recursive: true, force: true });
16871
17036
  mkdirSync14(workDir, { recursive: true });
16872
17037
  const compiledDir = join35(workDir, "compiled");
16873
17038
  mkdirSync14(compiledDir, { recursive: true });
@@ -16960,7 +17125,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16960
17125
  validateDistributedDuration({
16961
17126
  duration: probeResult.duration,
16962
17127
  totalFrames,
16963
- fps: fpsToNumber10(job.config.fps)
17128
+ fps: fpsToNumber9(job.config.fps)
16964
17129
  });
16965
17130
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
16966
17131
  if (probeResult.probeSession) {
@@ -17074,15 +17239,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
17074
17239
  logPlanStage(log, "extract", stageStarted);
17075
17240
  const stagedVideoFrames = join35(compiledDir, "__hyperframes_video_frames");
17076
17241
  const videoFramesDst = join35(planDir, "video-frames");
17077
- if (existsSync21(videoFramesDst)) rmSync14(videoFramesDst, { recursive: true, force: true });
17242
+ if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
17078
17243
  const videoSourcesDir = join35(planDir, "video-sources");
17079
- rmSync14(videoSourcesDir, { recursive: true, force: true });
17244
+ rmSync12(videoSourcesDir, { recursive: true, force: true });
17080
17245
  if (existsSync21(stagedVideoFrames)) {
17081
17246
  renameWithRetry7(stagedVideoFrames, videoFramesDst);
17082
17247
  } else {
17083
17248
  mkdirSync14(videoFramesDst, { recursive: true });
17084
17249
  }
17085
- if (existsSync21(finalCompiledDir)) rmSync14(finalCompiledDir, { recursive: true, force: true });
17250
+ if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
17086
17251
  renameWithRetry7(compiledDir, finalCompiledDir);
17087
17252
  const shipVideoSource = createVideoSourceShipper({
17088
17253
  planDir,
@@ -17250,7 +17415,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
17250
17415
  format: config.format
17251
17416
  };
17252
17417
  try {
17253
- rmSync14(workDir, { recursive: true, force: true });
17418
+ rmSync12(workDir, { recursive: true, force: true });
17254
17419
  } catch (err) {
17255
17420
  log.warn("[plan] failed to remove temp work dir", {
17256
17421
  workDir,
@@ -17401,6 +17566,20 @@ function validateDistributedRenderConfig(config) {
17401
17566
  `must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
17402
17567
  );
17403
17568
  }
17569
+ try {
17570
+ validateDeliveryEncoderConfig({
17571
+ format: config.format,
17572
+ fps: { num: config.fps, den: 1 },
17573
+ keyframeIntervalSeconds: config.keyframeIntervalSeconds,
17574
+ maxBitrate: config.maxBitrate,
17575
+ bufferSize: config.bufferSize
17576
+ });
17577
+ } catch (error) {
17578
+ throw new InvalidConfigError(
17579
+ "config.maxBitrate",
17580
+ error instanceof Error ? error.message : String(error)
17581
+ );
17582
+ }
17404
17583
  if (config.chunkSize !== void 0) {
17405
17584
  if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
17406
17585
  throw new InvalidConfigError(
@@ -17580,6 +17759,12 @@ function assertOptionalInteger(options, field, min = 0) {
17580
17759
  throw new Error(`Render request ${field} must be an integer >= ${min}`);
17581
17760
  }
17582
17761
  }
17762
+ function assertOptionalPositiveNumber(options, field) {
17763
+ const value = options[field];
17764
+ if (value !== void 0 && !(typeof value === "number" && value > 0)) {
17765
+ throw new Error(`Render request ${field} must be a positive number`);
17766
+ }
17767
+ }
17583
17768
  function assertOptionalString(options, field) {
17584
17769
  if (options[field] !== void 0 && typeof options[field] !== "string") {
17585
17770
  throw new Error(`Render request ${field} must be a string`);
@@ -17627,10 +17812,11 @@ function assertRequestOptionScalars(options) {
17627
17812
  assertOptionalInteger(options, "workers", 1);
17628
17813
  assertOptionalInteger(options, "maxRenderMs");
17629
17814
  assertOptionalInteger(options, "crf");
17815
+ assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
17630
17816
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
17631
17817
  assertOptionalBoolean(options, field);
17632
17818
  }
17633
- for (const field of ["entryFile", "videoBitrate"]) {
17819
+ for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
17634
17820
  assertOptionalString(options, field);
17635
17821
  }
17636
17822
  assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
@@ -17733,6 +17919,9 @@ function distributedConfigFromRequest(request, runtime = {}) {
17733
17919
  crf: options.crf,
17734
17920
  pageMotionBlur: options.pageMotionBlur,
17735
17921
  bitrate: options.videoBitrate,
17922
+ keyframeIntervalSeconds: options.keyframeIntervalSeconds,
17923
+ maxBitrate: options.maxBitrate,
17924
+ bufferSize: options.bufferSize,
17736
17925
  videoFrameFormat: options.videoFrameFormat,
17737
17926
  outputResolution: options.outputResolution,
17738
17927
  outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
@@ -17780,6 +17969,9 @@ function renderRequestFromDistributedConfig(input) {
17780
17969
  ...optionalProperty("crf", config.crf),
17781
17970
  ...optionalProperty("pageMotionBlur", config.pageMotionBlur),
17782
17971
  ...optionalProperty("videoBitrate", config.bitrate),
17972
+ ...optionalProperty("keyframeIntervalSeconds", config.keyframeIntervalSeconds),
17973
+ ...optionalProperty("maxBitrate", config.maxBitrate),
17974
+ ...optionalProperty("bufferSize", config.bufferSize),
17783
17975
  ...optionalProperty("videoFrameFormat", config.videoFrameFormat),
17784
17976
  ...optionalProperty("outputResolution", config.outputResolution),
17785
17977
  ...optionalProperty("outputResolutionAspectAgnostic", config.outputResolutionAspectAgnostic),
@@ -17825,7 +18017,7 @@ import {
17825
18017
  statSync as statSync10,
17826
18018
  mkdtempSync as mkdtempSync5,
17827
18019
  writeFileSync as writeFileSync10,
17828
- rmSync as rmSync15,
18020
+ rmSync as rmSync13,
17829
18021
  createReadStream as createReadStream5
17830
18022
  } from "node:fs";
17831
18023
  import { resolve as resolve11, dirname as dirname15, join as join38 } from "node:path";
@@ -18131,7 +18323,7 @@ function parseRenderOptions(body) {
18131
18323
  const format = parseServerFormat(body.format);
18132
18324
  const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
18133
18325
  const encoder = parseEncoderOptions(body, format ?? "mp4");
18134
- const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
18326
+ const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
18135
18327
  const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
18136
18328
  return {
18137
18329
  outputPath,
@@ -18141,6 +18333,9 @@ function parseRenderOptions(body) {
18141
18333
  maxRenderMs,
18142
18334
  crf,
18143
18335
  videoBitrate,
18336
+ keyframeIntervalSeconds,
18337
+ maxBitrate,
18338
+ bufferSize,
18144
18339
  useGpu,
18145
18340
  debug,
18146
18341
  strictness,
@@ -18175,6 +18370,9 @@ function buildRenderJobConfig(input, outputPath, log) {
18175
18370
  maxRenderMs: input.maxRenderMs,
18176
18371
  crf: input.crf,
18177
18372
  videoBitrate: input.videoBitrate,
18373
+ keyframeIntervalSeconds: input.keyframeIntervalSeconds,
18374
+ maxBitrate: input.maxBitrate,
18375
+ bufferSize: input.bufferSize,
18178
18376
  useGpu: input.useGpu,
18179
18377
  debug: input.debug,
18180
18378
  strictness: input.strictness,
@@ -18321,7 +18519,7 @@ function createArtifactStore(ttlMs) {
18321
18519
  function cleanupTempDir(dir, log) {
18322
18520
  if (!dir) return;
18323
18521
  try {
18324
- rmSync15(dir, { recursive: true, force: true });
18522
+ rmSync13(dir, { recursive: true, force: true });
18325
18523
  } catch (error) {
18326
18524
  log.warn("Failed to cleanup temp project dir", {
18327
18525
  cleanupProjectDir: dir,
@@ -18735,7 +18933,7 @@ import {
18735
18933
  mkdtempSync as mkdtempSync7,
18736
18934
  readFileSync as readFileSync14,
18737
18935
  readdirSync as readdirSync9,
18738
- rmSync as rmSync17,
18936
+ rmSync as rmSync15,
18739
18937
  statSync as statSync12,
18740
18938
  writeFileSync as writeFileSync12
18741
18939
  } from "node:fs";
@@ -18751,7 +18949,7 @@ import {
18751
18949
  linkSync as linkSync2,
18752
18950
  mkdirSync as mkdirSync16,
18753
18951
  mkdtempSync as mkdtempSync6,
18754
- rmSync as rmSync16,
18952
+ rmSync as rmSync14,
18755
18953
  statSync as statSync11,
18756
18954
  writeFileSync as writeFileSync11
18757
18955
  } from "node:fs";
@@ -18846,7 +19044,7 @@ var LocalPlanV2ArtifactPublisher = class {
18846
19044
  }
18847
19045
  renameWithRetry8(temporaryPath, destinationPath);
18848
19046
  } finally {
18849
- rmSync16(stagingDir, { recursive: true, force: true });
19047
+ rmSync14(stagingDir, { recursive: true, force: true });
18850
19048
  }
18851
19049
  }
18852
19050
  async commitManifest(manifestBytes) {
@@ -18863,7 +19061,7 @@ var LocalPlanV2ArtifactPublisher = class {
18863
19061
  }
18864
19062
  async abort() {
18865
19063
  if (!this.#committed) {
18866
- rmSync16(this.temporaryDir, { recursive: true, force: true });
19064
+ rmSync14(this.temporaryDir, { recursive: true, force: true });
18867
19065
  }
18868
19066
  }
18869
19067
  };
@@ -19132,7 +19330,7 @@ function writeBlob(sourcePath, destinationPath) {
19132
19330
  copyFileSync8(sourcePath, temporaryPath);
19133
19331
  renameWithRetry9(temporaryPath, destinationPath);
19134
19332
  } finally {
19135
- rmSync17(temporaryDir, { recursive: true, force: true });
19333
+ rmSync15(temporaryDir, { recursive: true, force: true });
19136
19334
  }
19137
19335
  }
19138
19336
  function publishBlobFor(absolutePath, digests) {
@@ -19265,7 +19463,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
19265
19463
  renameWithRetry9(tempDir, planV2Dir);
19266
19464
  return resultFromManifest(planV2Dir, publication.manifest);
19267
19465
  } catch (error) {
19268
- rmSync17(tempDir, { recursive: true, force: true });
19466
+ rmSync15(tempDir, { recursive: true, force: true });
19269
19467
  throw error;
19270
19468
  }
19271
19469
  }
@@ -19332,7 +19530,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
19332
19530
  }
19333
19531
  throw error;
19334
19532
  } finally {
19335
- rmSync17(stagingRoot, { recursive: true, force: true });
19533
+ rmSync15(stagingRoot, { recursive: true, force: true });
19336
19534
  }
19337
19535
  }
19338
19536
  async function planV2(projectDir, config, planV2Dir) {
@@ -19549,7 +19747,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
19549
19747
  );
19550
19748
  renameWithRetry9(tempDir, destinationDir);
19551
19749
  } catch (error) {
19552
- rmSync17(tempDir, { recursive: true, force: true });
19750
+ rmSync15(tempDir, { recursive: true, force: true });
19553
19751
  throw error;
19554
19752
  }
19555
19753
  return {
@@ -19613,7 +19811,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
19613
19811
  }
19614
19812
 
19615
19813
  // src/services/distributed/planV2Execution.ts
19616
- import { mkdtempSync as mkdtempSync9, rmSync as rmSync20 } from "node:fs";
19814
+ import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
19617
19815
  import { tmpdir as tmpdir5 } from "node:os";
19618
19816
  import { join as join44 } from "node:path";
19619
19817
 
@@ -19625,12 +19823,13 @@ import {
19625
19823
  mkdtempSync as mkdtempSync8,
19626
19824
  readFileSync as readFileSync15,
19627
19825
  readdirSync as readdirSync10,
19628
- rmSync as rmSync18,
19826
+ rmSync as rmSync16,
19629
19827
  statSync as statSync13,
19630
19828
  writeFileSync as writeFileSync13
19631
19829
  } from "node:fs";
19632
19830
  import { dirname as dirname18, join as join42 } from "node:path";
19633
19831
  import {
19832
+ appendRateCapArgs as appendRateCapArgs2,
19634
19833
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
19635
19834
  describeFfmpegFailure as describeFfmpegFailure4,
19636
19835
  getEncoderPreset as getEncoderPreset5,
@@ -19704,7 +19903,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
19704
19903
  });
19705
19904
  } finally {
19706
19905
  try {
19707
- rmSync18(workDir, { recursive: true, force: true });
19906
+ rmSync16(workDir, { recursive: true, force: true });
19708
19907
  } catch (err) {
19709
19908
  log.warn("[assemble] failed to remove work dir", {
19710
19909
  workDir,
@@ -19766,6 +19965,9 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
19766
19965
  "-r",
19767
19966
  fpsArg
19768
19967
  ];
19968
+ if (encoder.keyframeIntervalFrames !== void 0)
19969
+ args.push("-g", String(encoder.keyframeIntervalFrames));
19970
+ appendRateCapArgs2(args, encoder);
19769
19971
  appendSoftwareEncoderThreadsArg(args, "libx264");
19770
19972
  return args;
19771
19973
  }
@@ -19902,7 +20104,7 @@ function readCfrEncoder(planDir, format) {
19902
20104
  return encoderJson;
19903
20105
  }
19904
20106
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
19905
- if (existsSync27(outputPath)) rmSync18(outputPath, { recursive: true, force: true });
20107
+ if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
19906
20108
  mkdirSync18(outputPath, { recursive: true });
19907
20109
  let globalIdx = 0;
19908
20110
  for (const chunkDir of chunkPaths) {
@@ -19947,7 +20149,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19947
20149
 
19948
20150
  // src/services/distributed/renderChunk.ts
19949
20151
  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";
20152
+ import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync16, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync14 } from "node:fs";
19951
20153
  import { cpus as cpus3 } from "node:os";
19952
20154
  import { join as join43 } from "node:path";
19953
20155
  import {
@@ -20402,7 +20604,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20402
20604
  // first frame is an IDR keyframe and concat-copy at assemble time
20403
20605
  // round-trips losslessly.
20404
20606
  lockGopForChunkConcat: !isPngSequence,
20405
- gopSize: framesInChunk
20607
+ gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
20608
+ maxBitrate: encoder.maxBitrate,
20609
+ bufferSize: encoder.bufferSize
20406
20610
  };
20407
20611
  const captureStarted = Date.now();
20408
20612
  captureMode = await runCaptureWithScreenshotFallback({
@@ -20474,7 +20678,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20474
20678
  return observedChunkCaptureMode(capturePerfs);
20475
20679
  },
20476
20680
  resetForScreenshotRetry: () => {
20477
- rmSync19(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20681
+ rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20478
20682
  mkdirSync19(framesDir, { recursive: true });
20479
20683
  capturePerfs.length = 0;
20480
20684
  captureAttempts.length = 0;
@@ -20517,8 +20721,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20517
20721
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
20518
20722
  timeoutMs: cfg.ffmpegProcessTimeout
20519
20723
  });
20520
- rmSync19(videoOnlyPath, { force: true });
20521
- rmSync19(audioScratchPath, { force: true });
20724
+ rmSync17(videoOnlyPath, { force: true });
20725
+ rmSync17(audioScratchPath, { force: true });
20522
20726
  }
20523
20727
  encodeStageMs = Date.now() - encodeStarted;
20524
20728
  } finally {
@@ -20568,8 +20772,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20568
20772
  writeFileSync14(perfPath, `${JSON.stringify(perfPayload, null, 2)}
20569
20773
  `, "utf-8");
20570
20774
  try {
20571
- rmSync19(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20572
- rmSync19(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20775
+ rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20776
+ rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20573
20777
  } catch (err) {
20574
20778
  log.warn("[renderChunk] failed to remove work dir", {
20575
20779
  workDir,
@@ -20604,7 +20808,7 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
20604
20808
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
20605
20809
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
20606
20810
  } finally {
20607
- rmSync20(workRoot, { recursive: true, force: true });
20811
+ rmSync18(workRoot, { recursive: true, force: true });
20608
20812
  }
20609
20813
  }
20610
20814
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
@@ -20624,7 +20828,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
20624
20828
  options
20625
20829
  );
20626
20830
  } finally {
20627
- rmSync20(workRoot, { recursive: true, force: true });
20831
+ rmSync18(workRoot, { recursive: true, force: true });
20628
20832
  }
20629
20833
  }
20630
20834