@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.
@@ -6,16 +6,17 @@ import {
6
6
  existsSync as existsSync15,
7
7
  linkSync,
8
8
  mkdirSync as mkdirSync10,
9
- rmSync as rmSync9,
9
+ rmSync as rmSync7,
10
10
  writeFileSync as writeFileSync7
11
11
  } from "node:fs";
12
12
  import { extname as extname4, isAbsolute as isAbsolute3, join as join22, relative as relative5, sep as sep2 } from "node:path";
13
13
  import {
14
- fpsToNumber as fpsToNumber10,
14
+ fpsToNumber as fpsToNumber9,
15
15
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
16
16
  } from "@hyperframes/core";
17
17
  import {
18
18
  getEncoderPreset as getEncoderPreset4,
19
+ keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
19
20
  normalizeVp9CpuUsed,
20
21
  resolveConfig as resolveConfig3,
21
22
  declareContentAddressedUrl,
@@ -69,7 +70,7 @@ import {
69
70
  mkdirSync as mkdirSync6,
70
71
  readFileSync as readFileSync6,
71
72
  readdirSync as readdirSync3,
72
- rmSync as rmSync7,
73
+ rmSync as rmSync5,
73
74
  statSync as statSync5,
74
75
  writeFileSync as writeFileSync4,
75
76
  copyFileSync as copyFileSync5,
@@ -77,7 +78,7 @@ import {
77
78
  } from "fs";
78
79
  import {
79
80
  chainCapture,
80
- fpsToNumber as fpsToNumber9,
81
+ fpsToNumber as fpsToNumber8,
81
82
  HF_COLOR_GRADING_ATTR,
82
83
  HF_VFX_ATTR,
83
84
  parseVfxChain,
@@ -822,7 +823,34 @@ function outputNeedsAlpha(format) {
822
823
  }
823
824
 
824
825
  // src/services/render/hlsConfig.ts
825
- import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
826
+ import { keyframeIntervalToGopSize } from "@hyperframes/engine";
827
+
828
+ // src/services/render/deliveryEncoder.ts
829
+ import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
830
+ function validateDeliveryEncoderConfig(config) {
831
+ const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
832
+ if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
833
+ return;
834
+ if (keyframeIntervalSeconds !== void 0) {
835
+ if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
836
+ throw new Error(
837
+ `keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
838
+ );
839
+ }
840
+ if (format === "hls") {
841
+ throw new Error(
842
+ "keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
843
+ );
844
+ }
845
+ }
846
+ if (format !== void 0 && format !== "mp4") {
847
+ throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
848
+ }
849
+ appendRateCapArgs([], { maxBitrate, bufferSize });
850
+ }
851
+ function chunkGopFrames(framesInChunk, intervalFrames) {
852
+ return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
853
+ }
826
854
 
827
855
  // src/services/render/shared.ts
828
856
  import {
@@ -1245,7 +1273,7 @@ import {
1245
1273
  import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1246
1274
 
1247
1275
  // src/services/render/perfSummary.ts
1248
- import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1276
+ import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
1249
1277
  function pushWorkerDedupPerfs(results, sink) {
1250
1278
  for (const r of results) {
1251
1279
  if (r.perf) sink.push(r.perf);
@@ -1324,7 +1352,7 @@ import {
1324
1352
  resolveVideoExtractionWindow,
1325
1353
  runFfmpeg
1326
1354
  } from "@hyperframes/engine";
1327
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1355
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1328
1356
  var BYTES_PER_MIB = 1024 * 1024;
1329
1357
  var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
1330
1358
 
@@ -1375,7 +1403,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
1375
1403
  }
1376
1404
 
1377
1405
  // src/services/render/captureCost.ts
1378
- import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
1406
+ import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1379
1407
  import {
1380
1408
  calculateOptimalWorkers,
1381
1409
  captureFrameToBuffer as captureFrameToBuffer3,
@@ -1785,7 +1813,7 @@ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
1785
1813
  // src/services/render/videoFrameCoverage.ts
1786
1814
  import { parseHTML } from "linkedom";
1787
1815
  import {
1788
- fpsToNumber as fpsToNumber6,
1816
+ fpsToNumber as fpsToNumber5,
1789
1817
  normalizeRateSpec,
1790
1818
  sourceTimeAt,
1791
1819
  toFps
@@ -2993,7 +3021,7 @@ function detectShaderTransitionUsage(html) {
2993
3021
  }
2994
3022
  return false;
2995
3023
  }
2996
- async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
3024
+ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
2997
3025
  let filePath = src;
2998
3026
  if (isHttpUrl(src)) {
2999
3027
  if (!existsSync6(downloadDir)) mkdirSync4(downloadDir, { recursive: true });
@@ -3020,45 +3048,52 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
3020
3048
  if (error instanceof Error && error.stack) wrapped.stack = error.stack;
3021
3049
  return wrapped;
3022
3050
  };
3023
- return withMediaProbeSlot(async () => {
3024
- let profile;
3051
+ const probe = () => withMediaProbeSlot(async () => {
3025
3052
  try {
3026
3053
  await assertMediaPayload(filePath, elementIdentity);
3027
- profile = await probeMediaProfile2(filePath);
3028
- } catch (error) {
3029
- if (tagName !== "video") {
3030
- if (error instanceof NotMediaPayloadError2) {
3031
- log?.warn(
3032
- `[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.`
3033
- );
3034
- }
3035
- return { duration: null, resolvedPath: filePath };
3054
+ const profile = await probeMediaProfile2(filePath);
3055
+ if (tagName === "video") {
3056
+ return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
3036
3057
  }
3037
- throw withSrcContext(error);
3038
- }
3039
- assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
3040
- let metadata;
3041
- if (tagName === "video") {
3042
- try {
3043
- metadata = await extractMediaMetadata(filePath);
3044
- } catch (error) {
3045
- throw withSrcContext(error);
3046
- }
3047
- } else {
3048
3058
  try {
3049
- metadata = await extractAudioMetadata(filePath);
3059
+ return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
3050
3060
  } catch {
3051
- return { duration: null, resolvedPath: filePath };
3061
+ return { profile, fileDuration: null };
3052
3062
  }
3063
+ } catch (error) {
3064
+ return { error };
3053
3065
  }
3054
- const fileDuration = metadata.durationSeconds;
3055
- const duration = resolveNaturalMediaTimelineDurationFromValues(
3056
- fileDuration,
3057
- mediaStart,
3058
- playbackRate
3059
- );
3060
- return { duration, resolvedPath: filePath };
3061
3066
  });
3067
+ const key = `${tagName}\0${filePath}`;
3068
+ let probed = probes?.get(key);
3069
+ if (!probed) {
3070
+ probed = probe();
3071
+ probes?.set(key, probed);
3072
+ }
3073
+ const outcome = await probed;
3074
+ if ("error" in outcome) {
3075
+ if (tagName !== "video") {
3076
+ if (outcome.error instanceof NotMediaPayloadError2) {
3077
+ log?.warn(
3078
+ `[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.`
3079
+ );
3080
+ }
3081
+ return { duration: null, resolvedPath: filePath };
3082
+ }
3083
+ throw withSrcContext(outcome.error);
3084
+ }
3085
+ assertAssetMediaTypeProfile(
3086
+ tagName === "video" ? "video" : "audio",
3087
+ outcome.profile,
3088
+ elementIdentity
3089
+ );
3090
+ if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
3091
+ const duration = resolveNaturalMediaTimelineDurationFromValues(
3092
+ outcome.fileDuration,
3093
+ mediaStart,
3094
+ playbackRate
3095
+ );
3096
+ return { duration, resolvedPath: filePath };
3062
3097
  }
3063
3098
  function markInferredVariableMediaDurations(html, unresolvedMedia) {
3064
3099
  const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
@@ -3072,7 +3107,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
3072
3107
  }
3073
3108
  return changed ? document2.toString() : html;
3074
3109
  }
3075
- async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
3110
+ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
3076
3111
  const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
3077
3112
  const mediaUnresolved = unresolved.filter(
3078
3113
  (el) => (el.tagName === "video" || el.tagName === "audio") && el.src
@@ -3089,7 +3124,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
3089
3124
  el.tagName,
3090
3125
  el.id,
3091
3126
  log,
3092
- urlByAbsentLocalPath
3127
+ urlByAbsentLocalPath,
3128
+ probes
3093
3129
  ).then(({ duration }) => ({ id: el.id, duration }))
3094
3130
  )
3095
3131
  );
@@ -3110,7 +3146,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
3110
3146
  downloadDir,
3111
3147
  el.tagName,
3112
3148
  el.id,
3113
- log
3149
+ log,
3150
+ void 0,
3151
+ probes
3114
3152
  );
3115
3153
  return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
3116
3154
  })
@@ -3132,7 +3170,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
3132
3170
  compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
3133
3171
  return { html: compiledHtml, unresolvedCompositions };
3134
3172
  }
3135
- async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
3173
+ async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
3136
3174
  const subCompositions = /* @__PURE__ */ new Map();
3137
3175
  const { document: document2 } = parseHTML5(html);
3138
3176
  const compEls = document2.querySelectorAll("[data-composition-src]");
@@ -3160,14 +3198,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
3160
3198
  dirname5(item.filePath),
3161
3199
  downloadDir,
3162
3200
  void 0,
3163
- urlByAbsentLocalPath
3201
+ urlByAbsentLocalPath,
3202
+ probes
3164
3203
  );
3165
3204
  const nested = await parseSubCompositions(
3166
3205
  compiledSub,
3167
3206
  projectDir,
3168
3207
  downloadDir,
3169
3208
  item.nestedVisited,
3170
- urlByAbsentLocalPath
3209
+ urlByAbsentLocalPath,
3210
+ probes
3171
3211
  );
3172
3212
  return {
3173
3213
  srcPath: item.srcPath,
@@ -3937,19 +3977,22 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3937
3977
  );
3938
3978
  const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
3939
3979
  assertSubCompositionsUsable(rawHtml, projectDir);
3980
+ const probes = /* @__PURE__ */ new Map();
3940
3981
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
3941
3982
  rawHtml,
3942
3983
  projectDir,
3943
3984
  downloadDir,
3944
3985
  options.log,
3945
- options.urlByAbsentLocalPath
3986
+ options.urlByAbsentLocalPath,
3987
+ probes
3946
3988
  );
3947
3989
  const { subCompositions } = await parseSubCompositions(
3948
3990
  compiledHtml,
3949
3991
  projectDir,
3950
3992
  downloadDir,
3951
3993
  void 0,
3952
- options.urlByAbsentLocalPath
3994
+ options.urlByAbsentLocalPath,
3995
+ probes
3953
3996
  );
3954
3997
  const fullHtml = ensureFullDocument(compiledHtml);
3955
3998
  const inlinedHtml = inlineSubCompositions(
@@ -4052,9 +4095,12 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
4052
4095
  }
4053
4096
  const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
4054
4097
  const masterFxChain = parseMasterFxChain(html);
4098
+ const advised = /* @__PURE__ */ new Set();
4055
4099
  for (const video of videos) {
4056
4100
  if (isHttpUrl(video.src)) continue;
4057
4101
  const videoPath = resolve6(projectDir, video.src);
4102
+ if (advised.has(videoPath)) continue;
4103
+ advised.add(videoPath);
4058
4104
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
4059
4105
  (async () => {
4060
4106
  const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
@@ -4605,13 +4651,14 @@ import {
4605
4651
  } from "@hyperframes/engine";
4606
4652
  import {
4607
4653
  MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
4608
- fpsToNumber as fpsToNumber7,
4654
+ fpsToNumber as fpsToNumber6,
4609
4655
  normalizeRateSpec as normalizeRateSpec3,
4610
4656
  readMediaStart as readMediaStart2,
4611
4657
  shiftRateLane as shiftRateLane2,
4612
4658
  sourceTimeAt as sourceTimeAt3,
4613
4659
  timeAtSourceTime
4614
4660
  } from "@hyperframes/core";
4661
+ import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
4615
4662
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
4616
4663
 
4617
4664
  // src/services/render/stages/probeFailures.ts
@@ -4649,8 +4696,15 @@ function hasAutoStartVideos(html) {
4649
4696
  const { document: document2 } = parseHTML6(html);
4650
4697
  return document2.querySelector("video[data-hf-auto-start]") !== null;
4651
4698
  }
4699
+ function variableHasValue(element, variableId, variables) {
4700
+ if (variables && Object.hasOwn(variables, variableId)) return true;
4701
+ for (let scope = element.parentElement; scope; scope = scope.parentElement) {
4702
+ if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
4703
+ if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
4704
+ }
4705
+ return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
4706
+ }
4652
4707
  function hasVariableBoundMedia(html, variables) {
4653
- if (!variables || Object.keys(variables).length === 0) return false;
4654
4708
  const { document: document2 } = parseHTML6(html);
4655
4709
  return Array.from(
4656
4710
  document2.querySelectorAll(
@@ -4658,7 +4712,7 @@ function hasVariableBoundMedia(html, variables) {
4658
4712
  )
4659
4713
  ).some((element) => {
4660
4714
  const variableId = element.getAttribute("data-var-src")?.trim();
4661
- return Boolean(variableId && Object.hasOwn(variables, variableId));
4715
+ return Boolean(variableId && variableHasValue(element, variableId, variables));
4662
4716
  });
4663
4717
  }
4664
4718
  function authoredMediaStarts(html) {
@@ -5084,7 +5138,7 @@ async function runProbeStage(input) {
5084
5138
  session.page,
5085
5139
  composition.audios.map((audio) => audio.id),
5086
5140
  composition.duration,
5087
- fpsToNumber7(job.config.fps)
5141
+ fpsToNumber6(job.config.fps)
5088
5142
  );
5089
5143
  assertNotAborted();
5090
5144
  if (automation.length > 0) {
@@ -5147,7 +5201,7 @@ async function runProbeStage(input) {
5147
5201
  }
5148
5202
  const browserProbeMs = Date.now() - probeStart;
5149
5203
  const duration = composition.duration;
5150
- const totalFrames = durationToFrameCount(duration, fpsToNumber7(job.config.fps));
5204
+ const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
5151
5205
  if (duration <= 0) {
5152
5206
  const diagnostics = [];
5153
5207
  try {
@@ -5392,10 +5446,11 @@ var VideoExtractionStageError = class extends Error {
5392
5446
  kind: failure.kind,
5393
5447
  affectedElementCount: failure.count
5394
5448
  }))
5395
- )) {
5449
+ ), ffmpegReason) {
5396
5450
  const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
5397
5451
  const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
5398
- super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
5452
+ const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
5453
+ super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
5399
5454
  this.code = code;
5400
5455
  this.retryable = retryable;
5401
5456
  this.failures = failures;
@@ -5442,7 +5497,8 @@ function buildVideoExtractionStageError(result) {
5442
5497
  retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
5443
5498
  retryable,
5444
5499
  failures,
5445
- extractionFailureMetadataFromResult(result)
5500
+ extractionFailureMetadataFromResult(result),
5501
+ result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
5446
5502
  );
5447
5503
  }
5448
5504
  function buildHdrProbeStageError(failures) {
@@ -5710,16 +5766,17 @@ async function appendVideoAudioFromProbe(input) {
5710
5766
 
5711
5767
  // src/services/render/stages/audioStage.ts
5712
5768
  import { join as join11 } from "node:path";
5713
- import { rmSync as rmSync5 } from "node:fs";
5714
5769
  import {
5715
5770
  MIXED_AUDIO_FILENAME,
5716
5771
  audioFailureDiagnostics,
5717
- processCompositionAudio
5772
+ processCompositionAudio,
5773
+ removeScratch as removeScratch2,
5774
+ collectScratchLeftovers
5718
5775
  } from "@hyperframes/engine";
5719
5776
 
5720
5777
  // src/services/render/audioPadTrim.ts
5721
5778
  import { spawn } from "node:child_process";
5722
- import { mkdtempSync, rmSync as rmSync4 } from "node:fs";
5779
+ import { mkdtempSync } from "node:fs";
5723
5780
  import { dirname as dirname6, join as join10 } from "node:path";
5724
5781
  import {
5725
5782
  DEFAULT_CONFIG,
@@ -5728,6 +5785,7 @@ import {
5728
5785
  extractAudioMetadata as extractAudioMetadata3,
5729
5786
  getFfprobeBinary,
5730
5787
  ManagedChildProcess,
5788
+ removeScratch,
5731
5789
  renameWithRetry as renameWithRetry5,
5732
5790
  runFfmpeg as runFfmpeg5,
5733
5791
  trackChildProcess
@@ -5923,7 +5981,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
5923
5981
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
5924
5982
  };
5925
5983
  } finally {
5926
- for (const path of plan2.cleanupPaths) rmSync4(path, { force: true });
5984
+ for (const path of plan2.cleanupPaths) removeScratch(path);
5927
5985
  }
5928
5986
  let loweredDb = 0;
5929
5987
  if (probeTruePeak) {
@@ -5986,7 +6044,7 @@ async function enforceAacTruePeak(input) {
5986
6044
  }
5987
6045
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
5988
6046
  if (measuredPath !== correctedPath) return { success: true };
5989
- renameWithRetry5(correctedPath, input.audioPath);
6047
+ renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
5990
6048
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
5991
6049
  }
5992
6050
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -6027,7 +6085,7 @@ async function enforceAacTruePeak(input) {
6027
6085
  )}`
6028
6086
  };
6029
6087
  } finally {
6030
- if (scratchDir) rmSync4(scratchDir, { recursive: true, force: true });
6088
+ if (scratchDir) removeScratch(scratchDir);
6031
6089
  }
6032
6090
  }
6033
6091
  function failResult(outputPath, target, source, error) {
@@ -6172,7 +6230,15 @@ function assertAudioNotInterrupted(failures) {
6172
6230
  ])
6173
6231
  });
6174
6232
  }
6175
- async function runAudioStage(input) {
6233
+ function runAudioStage(input) {
6234
+ return collectScratchLeftovers(
6235
+ () => runAudioStageScoped(input),
6236
+ (left) => {
6237
+ if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
6238
+ }
6239
+ );
6240
+ }
6241
+ async function runAudioStageScoped(input) {
6176
6242
  const stageStart = Date.now();
6177
6243
  const pcmSourcePath = join11(input.workDir, "audio-source.wav");
6178
6244
  const duration = input.totalFrames * input.fps.den / input.fps.num;
@@ -6194,7 +6260,7 @@ async function runAudioStage(input) {
6194
6260
  }
6195
6261
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
6196
6262
  } finally {
6197
- rmSync5(pcmSourcePath, { force: true });
6263
+ removeScratch2(pcmSourcePath);
6198
6264
  }
6199
6265
  }
6200
6266
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -6437,6 +6503,7 @@ async function runCaptureStage(input) {
6437
6503
  dedupPerfs,
6438
6504
  pageErrorLines: workerPageErrors,
6439
6505
  openAttempt: input.openAttempt,
6506
+ verifyFirstSession: input.verifyFirstSession,
6440
6507
  onProgress: (progress) => {
6441
6508
  if (progress.latestWorkerPhase) {
6442
6509
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -6653,7 +6720,7 @@ import {
6653
6720
  initializeSession as initializeSession7,
6654
6721
  spawnStreamingEncoder as spawnStreamingEncoder3
6655
6722
  } from "@hyperframes/engine";
6656
- import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
6723
+ import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
6657
6724
 
6658
6725
  // src/services/hdrImageTransferCache.ts
6659
6726
  import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
@@ -6683,7 +6750,7 @@ import {
6683
6750
  mkdirSync as mkdirSync5,
6684
6751
  readdirSync as readdirSync2,
6685
6752
  readFileSync as readFileSync5,
6686
- rmSync as rmSync6,
6753
+ rmSync as rmSync4,
6687
6754
  statSync as statSync4,
6688
6755
  writeFileSync as writeFileSync3
6689
6756
  } from "node:fs";
@@ -6822,7 +6889,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
6822
6889
  fileSize
6823
6890
  };
6824
6891
  } finally {
6825
- rmSync6(input.palettePath, { force: true });
6892
+ rmSync4(input.palettePath, { force: true });
6826
6893
  }
6827
6894
  }
6828
6895
  function startEncodeProgress(job, stage, onProgress) {
@@ -6846,6 +6913,8 @@ function videoEncoderOptions(input, engineCfg) {
6846
6913
  preset: preset.preset,
6847
6914
  quality: input.effectiveQuality,
6848
6915
  bitrate: input.effectiveBitrate,
6916
+ maxBitrate: input.maxBitrate,
6917
+ bufferSize: input.bufferSize,
6849
6918
  pixelFormat: preset.pixelFormat,
6850
6919
  vp9CpuUsed: engineCfg.vp9CpuUsed,
6851
6920
  useGpu: job.config.useGpu,
@@ -7305,7 +7374,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
7305
7374
  });
7306
7375
  } : void 0,
7307
7376
  void 0,
7308
- options.cfg
7377
+ options.cfg,
7378
+ { verifyFirstSession: options.verifyFirstSession }
7309
7379
  );
7310
7380
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
7311
7381
  if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
@@ -8468,7 +8538,7 @@ import { join as join20 } from "node:path";
8468
8538
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
8469
8539
 
8470
8540
  // src/services/distributed/mp4Stitch.ts
8471
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "node:fs";
8541
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync6 } from "node:fs";
8472
8542
  import { open } from "node:fs/promises";
8473
8543
  import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
8474
8544
  import { dirname as dirname10, join as join19 } from "node:path";
@@ -8846,7 +8916,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
8846
8916
  renameWithRetry6(join19(workDir, "out.mp4"), outputPath);
8847
8917
  } finally {
8848
8918
  out.destroy();
8849
- rmSync8(workDir, { recursive: true, force: true });
8919
+ rmSync6(workDir, { recursive: true, force: true });
8850
8920
  }
8851
8921
  }
8852
8922
 
@@ -9358,7 +9428,7 @@ var FREEZE_OWNED_PLAN_FILES = [
9358
9428
  ];
9359
9429
  function removeFreezeOwnedPlanFiles(planDir) {
9360
9430
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
9361
- rmSync9(join22(planDir, relativePath), { force: true });
9431
+ rmSync7(join22(planDir, relativePath), { force: true });
9362
9432
  }
9363
9433
  }
9364
9434
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -9418,6 +9488,9 @@ function buildLockedRenderConfig(input) {
9418
9488
  preset,
9419
9489
  crf: config.crf,
9420
9490
  bitrate: config.bitrate,
9491
+ maxBitrate: config.maxBitrate,
9492
+ bufferSize: config.bufferSize,
9493
+ keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
9421
9494
  ...locksVp9CpuUsed,
9422
9495
  // GOP === chunkSize so every chunk's first frame is an IDR keyframe and
9423
9496
  // ffmpeg concat-copy round-trips losslessly.
@@ -9569,7 +9642,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9569
9642
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
9570
9643
  }
9571
9644
  const workDir = join22(planDir, ".plan-work");
9572
- rmSync9(workDir, { recursive: true, force: true });
9645
+ rmSync7(workDir, { recursive: true, force: true });
9573
9646
  mkdirSync10(workDir, { recursive: true });
9574
9647
  const compiledDir = join22(workDir, "compiled");
9575
9648
  mkdirSync10(compiledDir, { recursive: true });
@@ -9662,7 +9735,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9662
9735
  validateDistributedDuration({
9663
9736
  duration: probeResult.duration,
9664
9737
  totalFrames,
9665
- fps: fpsToNumber10(job.config.fps)
9738
+ fps: fpsToNumber9(job.config.fps)
9666
9739
  });
9667
9740
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
9668
9741
  if (probeResult.probeSession) {
@@ -9776,15 +9849,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9776
9849
  logPlanStage(log, "extract", stageStarted);
9777
9850
  const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
9778
9851
  const videoFramesDst = join22(planDir, "video-frames");
9779
- if (existsSync15(videoFramesDst)) rmSync9(videoFramesDst, { recursive: true, force: true });
9852
+ if (existsSync15(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
9780
9853
  const videoSourcesDir = join22(planDir, "video-sources");
9781
- rmSync9(videoSourcesDir, { recursive: true, force: true });
9854
+ rmSync7(videoSourcesDir, { recursive: true, force: true });
9782
9855
  if (existsSync15(stagedVideoFrames)) {
9783
9856
  renameWithRetry7(stagedVideoFrames, videoFramesDst);
9784
9857
  } else {
9785
9858
  mkdirSync10(videoFramesDst, { recursive: true });
9786
9859
  }
9787
- if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
9860
+ if (existsSync15(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
9788
9861
  renameWithRetry7(compiledDir, finalCompiledDir);
9789
9862
  const shipVideoSource = createVideoSourceShipper({
9790
9863
  planDir,
@@ -9952,7 +10025,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9952
10025
  format: config.format
9953
10026
  };
9954
10027
  try {
9955
- rmSync9(workDir, { recursive: true, force: true });
10028
+ rmSync7(workDir, { recursive: true, force: true });
9956
10029
  } catch (err) {
9957
10030
  log.warn("[plan] failed to remove temp work dir", {
9958
10031
  workDir,
@@ -10022,7 +10095,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
10022
10095
  renameWithRetry7(mixed.audioOutputPath, outputPath);
10023
10096
  return { hasAudio: true };
10024
10097
  } finally {
10025
- rmSync9(c.workDir, { recursive: true, force: true });
10098
+ rmSync7(c.workDir, { recursive: true, force: true });
10026
10099
  }
10027
10100
  }
10028
10101
  async function plan(projectDir, config, planDir, options = {}) {
@@ -10051,7 +10124,7 @@ import {
10051
10124
  mkdtempSync as mkdtempSync4,
10052
10125
  readFileSync as readFileSync10,
10053
10126
  readdirSync as readdirSync7,
10054
- rmSync as rmSync11,
10127
+ rmSync as rmSync9,
10055
10128
  statSync as statSync7,
10056
10129
  writeFileSync as writeFileSync9
10057
10130
  } from "node:fs";
@@ -10067,7 +10140,7 @@ import {
10067
10140
  linkSync as linkSync2,
10068
10141
  mkdirSync as mkdirSync11,
10069
10142
  mkdtempSync as mkdtempSync3,
10070
- rmSync as rmSync10,
10143
+ rmSync as rmSync8,
10071
10144
  statSync as statSync6,
10072
10145
  writeFileSync as writeFileSync8
10073
10146
  } from "node:fs";
@@ -10162,7 +10235,7 @@ var LocalPlanV2ArtifactPublisher = class {
10162
10235
  }
10163
10236
  renameWithRetry8(temporaryPath, destinationPath);
10164
10237
  } finally {
10165
- rmSync10(stagingDir, { recursive: true, force: true });
10238
+ rmSync8(stagingDir, { recursive: true, force: true });
10166
10239
  }
10167
10240
  }
10168
10241
  async commitManifest(manifestBytes) {
@@ -10179,7 +10252,7 @@ var LocalPlanV2ArtifactPublisher = class {
10179
10252
  }
10180
10253
  async abort() {
10181
10254
  if (!this.#committed) {
10182
- rmSync10(this.temporaryDir, { recursive: true, force: true });
10255
+ rmSync8(this.temporaryDir, { recursive: true, force: true });
10183
10256
  }
10184
10257
  }
10185
10258
  };
@@ -10448,7 +10521,7 @@ function writeBlob(sourcePath, destinationPath) {
10448
10521
  copyFileSync8(sourcePath, temporaryPath);
10449
10522
  renameWithRetry9(temporaryPath, destinationPath);
10450
10523
  } finally {
10451
- rmSync11(temporaryDir, { recursive: true, force: true });
10524
+ rmSync9(temporaryDir, { recursive: true, force: true });
10452
10525
  }
10453
10526
  }
10454
10527
  function publishBlobFor(absolutePath, digests) {
@@ -10581,7 +10654,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
10581
10654
  renameWithRetry9(tempDir, planV2Dir);
10582
10655
  return resultFromManifest(planV2Dir, publication.manifest);
10583
10656
  } catch (error) {
10584
- rmSync11(tempDir, { recursive: true, force: true });
10657
+ rmSync9(tempDir, { recursive: true, force: true });
10585
10658
  throw error;
10586
10659
  }
10587
10660
  }
@@ -10648,7 +10721,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
10648
10721
  }
10649
10722
  throw error;
10650
10723
  } finally {
10651
- rmSync11(stagingRoot, { recursive: true, force: true });
10724
+ rmSync9(stagingRoot, { recursive: true, force: true });
10652
10725
  }
10653
10726
  }
10654
10727
  async function planV2(projectDir, config, planV2Dir) {
@@ -10865,7 +10938,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
10865
10938
  );
10866
10939
  renameWithRetry9(tempDir, destinationDir);
10867
10940
  } catch (error) {
10868
- rmSync11(tempDir, { recursive: true, force: true });
10941
+ rmSync9(tempDir, { recursive: true, force: true });
10869
10942
  throw error;
10870
10943
  }
10871
10944
  return {
@@ -10929,7 +11002,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
10929
11002
  }
10930
11003
 
10931
11004
  // src/services/distributed/planV2Execution.ts
10932
- import { mkdtempSync as mkdtempSync6, rmSync as rmSync14 } from "node:fs";
11005
+ import { mkdtempSync as mkdtempSync6, rmSync as rmSync12 } from "node:fs";
10933
11006
  import { tmpdir as tmpdir2 } from "node:os";
10934
11007
  import { join as join28 } from "node:path";
10935
11008
 
@@ -10941,12 +11014,13 @@ import {
10941
11014
  mkdtempSync as mkdtempSync5,
10942
11015
  readFileSync as readFileSync11,
10943
11016
  readdirSync as readdirSync8,
10944
- rmSync as rmSync12,
11017
+ rmSync as rmSync10,
10945
11018
  statSync as statSync8,
10946
11019
  writeFileSync as writeFileSync10
10947
11020
  } from "node:fs";
10948
11021
  import { dirname as dirname13, join as join26 } from "node:path";
10949
11022
  import {
11023
+ appendRateCapArgs as appendRateCapArgs2,
10950
11024
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
10951
11025
  describeFfmpegFailure as describeFfmpegFailure4,
10952
11026
  getEncoderPreset as getEncoderPreset5,
@@ -11020,7 +11094,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
11020
11094
  });
11021
11095
  } finally {
11022
11096
  try {
11023
- rmSync12(workDir, { recursive: true, force: true });
11097
+ rmSync10(workDir, { recursive: true, force: true });
11024
11098
  } catch (err) {
11025
11099
  log.warn("[assemble] failed to remove work dir", {
11026
11100
  workDir,
@@ -11082,6 +11156,9 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
11082
11156
  "-r",
11083
11157
  fpsArg
11084
11158
  ];
11159
+ if (encoder.keyframeIntervalFrames !== void 0)
11160
+ args.push("-g", String(encoder.keyframeIntervalFrames));
11161
+ appendRateCapArgs2(args, encoder);
11085
11162
  appendSoftwareEncoderThreadsArg(args, "libx264");
11086
11163
  return args;
11087
11164
  }
@@ -11218,7 +11295,7 @@ function readCfrEncoder(planDir, format) {
11218
11295
  return encoderJson;
11219
11296
  }
11220
11297
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
11221
- if (existsSync18(outputPath)) rmSync12(outputPath, { recursive: true, force: true });
11298
+ if (existsSync18(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
11222
11299
  mkdirSync13(outputPath, { recursive: true });
11223
11300
  let globalIdx = 0;
11224
11301
  for (const chunkDir of chunkPaths) {
@@ -11263,7 +11340,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
11263
11340
 
11264
11341
  // src/services/distributed/renderChunk.ts
11265
11342
  import { randomBytes } from "node:crypto";
11266
- import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync13, writeFileSync as writeFileSync11 } from "node:fs";
11343
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
11267
11344
  import { cpus } from "node:os";
11268
11345
  import { join as join27 } from "node:path";
11269
11346
  import {
@@ -11359,16 +11436,18 @@ function observedChunkCaptureMode(perfs) {
11359
11436
  }
11360
11437
  return mode;
11361
11438
  }
11439
+ var PROBE_SELECTED_SCREENSHOT = "[renderChunk] probe selected screenshot capture";
11362
11440
  function shouldRetryChunkCaptureWithScreenshot(error) {
11363
11441
  const failure = classifyCaptureFailure4(error);
11364
11442
  if (failure.kind === "cancelled" || failure.kind === "memory_exhaustion") return false;
11365
- 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);
11443
+ 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);
11366
11444
  }
11367
11445
  async function runCaptureWithScreenshotFallback(input) {
11368
11446
  try {
11369
11447
  return await input.run(input.forceScreenshot);
11370
11448
  } catch (error) {
11371
- if (input.forceScreenshot || !shouldRetryChunkCaptureWithScreenshot(error)) throw error;
11449
+ if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
11450
+ if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
11372
11451
  input.onFallback?.(error);
11373
11452
  await input.resetForScreenshotRetry();
11374
11453
  return await input.run(true);
@@ -11577,7 +11656,24 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11577
11656
  let extraction;
11578
11657
  const overlap = { current: null };
11579
11658
  try {
11580
- if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
11659
+ const verifyInWorker = chunkWorkerCount > 1 && !forceScreenshotForChunk;
11660
+ let boxesMeasured;
11661
+ let boxesFailed;
11662
+ const boxesReady = new Promise((resolve10, reject) => {
11663
+ boxesMeasured = resolve10;
11664
+ boxesFailed = reject;
11665
+ });
11666
+ boxesReady.catch(() => {
11667
+ });
11668
+ const measureBoxes = async (page) => {
11669
+ shownBoxes = await measureVideoShownBoxes(
11670
+ page,
11671
+ [...shownFrames.keys()],
11672
+ chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
11673
+ );
11674
+ boxesMeasured();
11675
+ };
11676
+ if (chunkWorkerCount === 1) {
11581
11677
  const bootStarted = Date.now();
11582
11678
  session = await createVerifiedDistributedCaptureSession(
11583
11679
  fileServer.url,
@@ -11586,11 +11682,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11586
11682
  cfg
11587
11683
  );
11588
11684
  sessionBootMs = Date.now() - bootStarted;
11589
- shownBoxes = await measureVideoShownBoxes(
11590
- session.page,
11591
- [...shownFrames.keys()],
11592
- chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
11593
- );
11685
+ await measureBoxes(session.page);
11594
11686
  const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
11595
11687
  const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
11596
11688
  if (browserSelectedScreenshot) {
@@ -11599,10 +11691,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11599
11691
  "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk",
11600
11692
  { chunkIndex }
11601
11693
  );
11602
- if (chunkWorkerCount > 1) {
11603
- await closeCaptureSession10(session);
11604
- session = null;
11605
- }
11606
11694
  } else if (beginFrameStalled) {
11607
11695
  forceScreenshotForChunk = true;
11608
11696
  log.warn(
@@ -11612,41 +11700,66 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11612
11700
  await closeCaptureSession10(session).catch(() => {
11613
11701
  });
11614
11702
  session = null;
11615
- if (chunkWorkerCount === 1) {
11616
- const screenshotBootStarted = Date.now();
11617
- const screenshotCfg = {
11618
- ...cfg,
11619
- forceScreenshot: true,
11620
- forceScreenshotExplicitlyOptedOut: false
11621
- };
11622
- session = await createVerifiedDistributedCaptureSession(
11623
- fileServer.url,
11624
- framesDir,
11625
- captureOptions,
11626
- screenshotCfg
11703
+ const screenshotBootStarted = Date.now();
11704
+ const screenshotCfg = {
11705
+ ...cfg,
11706
+ forceScreenshot: true,
11707
+ forceScreenshotExplicitlyOptedOut: false
11708
+ };
11709
+ session = await createVerifiedDistributedCaptureSession(
11710
+ fileServer.url,
11711
+ framesDir,
11712
+ captureOptions,
11713
+ screenshotCfg
11714
+ );
11715
+ sessionBootMs += Date.now() - screenshotBootStarted;
11716
+ }
11717
+ } else if (!verifyInWorker) {
11718
+ boxesMeasured();
11719
+ }
11720
+ let verification;
11721
+ const verifyFirstWorker = (first) => verification ??= (async () => {
11722
+ sessionBootMs = Date.now() - verifyStarted;
11723
+ try {
11724
+ await measureBoxes(first.page);
11725
+ const selectedScreenshot = first.launchCaptureMode === "screenshot";
11726
+ if (selectedScreenshot || await beginFrameSessionNeedsScreenshotFallback(first)) {
11727
+ forceScreenshotForChunk = true;
11728
+ log.warn(
11729
+ selectedScreenshot ? "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk" : "[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
11730
+ { chunkIndex }
11627
11731
  );
11628
- sessionBootMs += Date.now() - screenshotBootStarted;
11732
+ throw new Error(PROBE_SELECTED_SCREENSHOT);
11629
11733
  }
11630
- } else if (chunkWorkerCount > 1) {
11631
- await closeCaptureSession10(session);
11632
- session = null;
11734
+ } catch (error) {
11735
+ if (!forceScreenshotForChunk) {
11736
+ verifyFailure = error;
11737
+ boxesFailed(error);
11738
+ }
11739
+ throw error;
11633
11740
  }
11634
- }
11635
- const extractStarted = Date.now();
11636
- const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
11637
- planDir,
11638
- planVideos,
11639
- frames: chunkFrameNumbers,
11640
- fps: chunkFps,
11641
- outputRoot: videoFramesDir,
11642
- cfg,
11643
- remoteSources,
11644
- shownBoxes
11645
- }).then((frames) => {
11646
- videoExtractMs = Date.now() - extractStarted;
11647
- return frames;
11648
- }) : Promise.resolve([]);
11741
+ })();
11742
+ let verifyStarted = 0;
11743
+ let verifyFailure;
11744
+ const deferredVideoFrames = planVideos ? boxesReady.then(() => {
11745
+ const extractStarted = Date.now();
11746
+ return extractDeferredVideoFramesForChunk({
11747
+ planDir,
11748
+ planVideos,
11749
+ frames: chunkFrameNumbers,
11750
+ fps: chunkFps,
11751
+ outputRoot: videoFramesDir,
11752
+ cfg,
11753
+ remoteSources,
11754
+ shownBoxes
11755
+ }).then((frames) => {
11756
+ videoExtractMs = Date.now() - extractStarted;
11757
+ return frames;
11758
+ });
11759
+ }) : boxesReady.then(() => []);
11649
11760
  extraction = deferredVideoFrames;
11761
+ deferredVideoFrames.catch(() => {
11762
+ });
11650
11763
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
11651
11764
  (deferred) => createFrameLookupTable5(
11652
11765
  planVideos.videos,
@@ -11718,11 +11831,14 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11718
11831
  // first frame is an IDR keyframe and concat-copy at assemble time
11719
11832
  // round-trips losslessly.
11720
11833
  lockGopForChunkConcat: !isPngSequence,
11721
- gopSize: framesInChunk
11834
+ gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
11835
+ maxBitrate: encoder.maxBitrate,
11836
+ bufferSize: encoder.bufferSize
11722
11837
  };
11723
11838
  const captureStarted = Date.now();
11724
11839
  captureMode = await runCaptureWithScreenshotFallback({
11725
11840
  forceScreenshot: forceScreenshotForChunk,
11841
+ canRetry: () => verifyFailure === void 0,
11726
11842
  // fallow-ignore-next-line complexity
11727
11843
  run: async (forceScreenshot) => {
11728
11844
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : {
@@ -11752,6 +11868,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11752
11868
  }
11753
11869
  const captureSession = session;
11754
11870
  session = null;
11871
+ verifyStarted = Date.now();
11755
11872
  overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
11756
11873
  await observeRenderStage(
11757
11874
  new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
@@ -11779,7 +11896,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11779
11896
  frameRange: range,
11780
11897
  onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
11781
11898
  interleaveFrames: interleavesCapture(overlap.current !== null, options),
11782
- openAttempt
11899
+ openAttempt,
11900
+ verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
11783
11901
  }),
11784
11902
  { stall: frameStallRule(job) }
11785
11903
  ).catch(async (captureError) => {
@@ -11790,7 +11908,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11790
11908
  return observedChunkCaptureMode(capturePerfs);
11791
11909
  },
11792
11910
  resetForScreenshotRetry: () => {
11793
- rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11911
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11912
+ boxesMeasured();
11794
11913
  mkdirSync14(framesDir, { recursive: true });
11795
11914
  capturePerfs.length = 0;
11796
11915
  captureAttempts.length = 0;
@@ -11806,6 +11925,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11806
11925
  );
11807
11926
  }
11808
11927
  }).catch(async (captureError) => {
11928
+ if (verifyFailure !== void 0) throw verifyFailure;
11929
+ boxesFailed(captureError);
11809
11930
  await deferredVideoFrames;
11810
11931
  throw captureError;
11811
11932
  });
@@ -11833,8 +11954,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11833
11954
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
11834
11955
  timeoutMs: cfg.ffmpegProcessTimeout
11835
11956
  });
11836
- rmSync13(videoOnlyPath, { force: true });
11837
- rmSync13(audioScratchPath, { force: true });
11957
+ rmSync11(videoOnlyPath, { force: true });
11958
+ rmSync11(audioScratchPath, { force: true });
11838
11959
  }
11839
11960
  encodeStageMs = Date.now() - encodeStarted;
11840
11961
  } finally {
@@ -11884,8 +12005,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11884
12005
  writeFileSync11(perfPath, `${JSON.stringify(perfPayload, null, 2)}
11885
12006
  `, "utf-8");
11886
12007
  try {
11887
- rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11888
- rmSync13(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12008
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12009
+ rmSync11(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11889
12010
  } catch (err) {
11890
12011
  log.warn("[renderChunk] failed to remove work dir", {
11891
12012
  workDir,
@@ -11920,7 +12041,7 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
11920
12041
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
11921
12042
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
11922
12043
  } finally {
11923
- rmSync14(workRoot, { recursive: true, force: true });
12044
+ rmSync12(workRoot, { recursive: true, force: true });
11924
12045
  }
11925
12046
  }
11926
12047
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
@@ -11940,7 +12061,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
11940
12061
  options
11941
12062
  );
11942
12063
  } finally {
11943
- rmSync14(workRoot, { recursive: true, force: true });
12064
+ rmSync12(workRoot, { recursive: true, force: true });
11944
12065
  }
11945
12066
  }
11946
12067
 
@@ -12033,6 +12154,20 @@ function validateDistributedRenderConfig(config) {
12033
12154
  `must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
12034
12155
  );
12035
12156
  }
12157
+ try {
12158
+ validateDeliveryEncoderConfig({
12159
+ format: config.format,
12160
+ fps: { num: config.fps, den: 1 },
12161
+ keyframeIntervalSeconds: config.keyframeIntervalSeconds,
12162
+ maxBitrate: config.maxBitrate,
12163
+ bufferSize: config.bufferSize
12164
+ });
12165
+ } catch (error) {
12166
+ throw new InvalidConfigError(
12167
+ "config.maxBitrate",
12168
+ error instanceof Error ? error.message : String(error)
12169
+ );
12170
+ }
12036
12171
  if (config.chunkSize !== void 0) {
12037
12172
  if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
12038
12173
  throw new InvalidConfigError(