@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.
@@ -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
3058
  try {
3043
- metadata = await extractMediaMetadata(filePath);
3044
- } catch (error) {
3045
- throw withSrcContext(error);
3046
- }
3047
- } else {
3048
- 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 {
@@ -5710,16 +5764,17 @@ async function appendVideoAudioFromProbe(input) {
5710
5764
 
5711
5765
  // src/services/render/stages/audioStage.ts
5712
5766
  import { join as join11 } from "node:path";
5713
- import { rmSync as rmSync5 } from "node:fs";
5714
5767
  import {
5715
5768
  MIXED_AUDIO_FILENAME,
5716
5769
  audioFailureDiagnostics,
5717
- processCompositionAudio
5770
+ processCompositionAudio,
5771
+ removeScratch as removeScratch2,
5772
+ collectScratchLeftovers
5718
5773
  } from "@hyperframes/engine";
5719
5774
 
5720
5775
  // src/services/render/audioPadTrim.ts
5721
5776
  import { spawn } from "node:child_process";
5722
- import { mkdtempSync, rmSync as rmSync4 } from "node:fs";
5777
+ import { mkdtempSync } from "node:fs";
5723
5778
  import { dirname as dirname6, join as join10 } from "node:path";
5724
5779
  import {
5725
5780
  DEFAULT_CONFIG,
@@ -5728,6 +5783,7 @@ import {
5728
5783
  extractAudioMetadata as extractAudioMetadata3,
5729
5784
  getFfprobeBinary,
5730
5785
  ManagedChildProcess,
5786
+ removeScratch,
5731
5787
  renameWithRetry as renameWithRetry5,
5732
5788
  runFfmpeg as runFfmpeg5,
5733
5789
  trackChildProcess
@@ -5923,7 +5979,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
5923
5979
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
5924
5980
  };
5925
5981
  } finally {
5926
- for (const path of plan2.cleanupPaths) rmSync4(path, { force: true });
5982
+ for (const path of plan2.cleanupPaths) removeScratch(path);
5927
5983
  }
5928
5984
  let loweredDb = 0;
5929
5985
  if (probeTruePeak) {
@@ -5986,7 +6042,7 @@ async function enforceAacTruePeak(input) {
5986
6042
  }
5987
6043
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
5988
6044
  if (measuredPath !== correctedPath) return { success: true };
5989
- renameWithRetry5(correctedPath, input.audioPath);
6045
+ renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
5990
6046
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
5991
6047
  }
5992
6048
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -6027,7 +6083,7 @@ async function enforceAacTruePeak(input) {
6027
6083
  )}`
6028
6084
  };
6029
6085
  } finally {
6030
- if (scratchDir) rmSync4(scratchDir, { recursive: true, force: true });
6086
+ if (scratchDir) removeScratch(scratchDir);
6031
6087
  }
6032
6088
  }
6033
6089
  function failResult(outputPath, target, source, error) {
@@ -6172,7 +6228,15 @@ function assertAudioNotInterrupted(failures) {
6172
6228
  ])
6173
6229
  });
6174
6230
  }
6175
- async function runAudioStage(input) {
6231
+ function runAudioStage(input) {
6232
+ return collectScratchLeftovers(
6233
+ () => runAudioStageScoped(input),
6234
+ (left) => {
6235
+ if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
6236
+ }
6237
+ );
6238
+ }
6239
+ async function runAudioStageScoped(input) {
6176
6240
  const stageStart = Date.now();
6177
6241
  const pcmSourcePath = join11(input.workDir, "audio-source.wav");
6178
6242
  const duration = input.totalFrames * input.fps.den / input.fps.num;
@@ -6194,7 +6258,7 @@ async function runAudioStage(input) {
6194
6258
  }
6195
6259
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
6196
6260
  } finally {
6197
- rmSync5(pcmSourcePath, { force: true });
6261
+ removeScratch2(pcmSourcePath);
6198
6262
  }
6199
6263
  }
6200
6264
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -6653,7 +6717,7 @@ import {
6653
6717
  initializeSession as initializeSession7,
6654
6718
  spawnStreamingEncoder as spawnStreamingEncoder3
6655
6719
  } from "@hyperframes/engine";
6656
- import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
6720
+ import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
6657
6721
 
6658
6722
  // src/services/hdrImageTransferCache.ts
6659
6723
  import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
@@ -6683,7 +6747,7 @@ import {
6683
6747
  mkdirSync as mkdirSync5,
6684
6748
  readdirSync as readdirSync2,
6685
6749
  readFileSync as readFileSync5,
6686
- rmSync as rmSync6,
6750
+ rmSync as rmSync4,
6687
6751
  statSync as statSync4,
6688
6752
  writeFileSync as writeFileSync3
6689
6753
  } from "node:fs";
@@ -6822,7 +6886,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
6822
6886
  fileSize
6823
6887
  };
6824
6888
  } finally {
6825
- rmSync6(input.palettePath, { force: true });
6889
+ rmSync4(input.palettePath, { force: true });
6826
6890
  }
6827
6891
  }
6828
6892
  function startEncodeProgress(job, stage, onProgress) {
@@ -6846,6 +6910,8 @@ function videoEncoderOptions(input, engineCfg) {
6846
6910
  preset: preset.preset,
6847
6911
  quality: input.effectiveQuality,
6848
6912
  bitrate: input.effectiveBitrate,
6913
+ maxBitrate: input.maxBitrate,
6914
+ bufferSize: input.bufferSize,
6849
6915
  pixelFormat: preset.pixelFormat,
6850
6916
  vp9CpuUsed: engineCfg.vp9CpuUsed,
6851
6917
  useGpu: job.config.useGpu,
@@ -8468,7 +8534,7 @@ import { join as join20 } from "node:path";
8468
8534
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
8469
8535
 
8470
8536
  // src/services/distributed/mp4Stitch.ts
8471
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "node:fs";
8537
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync6 } from "node:fs";
8472
8538
  import { open } from "node:fs/promises";
8473
8539
  import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
8474
8540
  import { dirname as dirname10, join as join19 } from "node:path";
@@ -8846,7 +8912,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
8846
8912
  renameWithRetry6(join19(workDir, "out.mp4"), outputPath);
8847
8913
  } finally {
8848
8914
  out.destroy();
8849
- rmSync8(workDir, { recursive: true, force: true });
8915
+ rmSync6(workDir, { recursive: true, force: true });
8850
8916
  }
8851
8917
  }
8852
8918
 
@@ -9358,7 +9424,7 @@ var FREEZE_OWNED_PLAN_FILES = [
9358
9424
  ];
9359
9425
  function removeFreezeOwnedPlanFiles(planDir) {
9360
9426
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
9361
- rmSync9(join22(planDir, relativePath), { force: true });
9427
+ rmSync7(join22(planDir, relativePath), { force: true });
9362
9428
  }
9363
9429
  }
9364
9430
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -9418,6 +9484,9 @@ function buildLockedRenderConfig(input) {
9418
9484
  preset,
9419
9485
  crf: config.crf,
9420
9486
  bitrate: config.bitrate,
9487
+ maxBitrate: config.maxBitrate,
9488
+ bufferSize: config.bufferSize,
9489
+ keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
9421
9490
  ...locksVp9CpuUsed,
9422
9491
  // GOP === chunkSize so every chunk's first frame is an IDR keyframe and
9423
9492
  // ffmpeg concat-copy round-trips losslessly.
@@ -9569,7 +9638,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9569
9638
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
9570
9639
  }
9571
9640
  const workDir = join22(planDir, ".plan-work");
9572
- rmSync9(workDir, { recursive: true, force: true });
9641
+ rmSync7(workDir, { recursive: true, force: true });
9573
9642
  mkdirSync10(workDir, { recursive: true });
9574
9643
  const compiledDir = join22(workDir, "compiled");
9575
9644
  mkdirSync10(compiledDir, { recursive: true });
@@ -9662,7 +9731,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9662
9731
  validateDistributedDuration({
9663
9732
  duration: probeResult.duration,
9664
9733
  totalFrames,
9665
- fps: fpsToNumber10(job.config.fps)
9734
+ fps: fpsToNumber9(job.config.fps)
9666
9735
  });
9667
9736
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
9668
9737
  if (probeResult.probeSession) {
@@ -9776,15 +9845,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9776
9845
  logPlanStage(log, "extract", stageStarted);
9777
9846
  const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
9778
9847
  const videoFramesDst = join22(planDir, "video-frames");
9779
- if (existsSync15(videoFramesDst)) rmSync9(videoFramesDst, { recursive: true, force: true });
9848
+ if (existsSync15(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
9780
9849
  const videoSourcesDir = join22(planDir, "video-sources");
9781
- rmSync9(videoSourcesDir, { recursive: true, force: true });
9850
+ rmSync7(videoSourcesDir, { recursive: true, force: true });
9782
9851
  if (existsSync15(stagedVideoFrames)) {
9783
9852
  renameWithRetry7(stagedVideoFrames, videoFramesDst);
9784
9853
  } else {
9785
9854
  mkdirSync10(videoFramesDst, { recursive: true });
9786
9855
  }
9787
- if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
9856
+ if (existsSync15(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
9788
9857
  renameWithRetry7(compiledDir, finalCompiledDir);
9789
9858
  const shipVideoSource = createVideoSourceShipper({
9790
9859
  planDir,
@@ -9952,7 +10021,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9952
10021
  format: config.format
9953
10022
  };
9954
10023
  try {
9955
- rmSync9(workDir, { recursive: true, force: true });
10024
+ rmSync7(workDir, { recursive: true, force: true });
9956
10025
  } catch (err) {
9957
10026
  log.warn("[plan] failed to remove temp work dir", {
9958
10027
  workDir,
@@ -10022,7 +10091,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
10022
10091
  renameWithRetry7(mixed.audioOutputPath, outputPath);
10023
10092
  return { hasAudio: true };
10024
10093
  } finally {
10025
- rmSync9(c.workDir, { recursive: true, force: true });
10094
+ rmSync7(c.workDir, { recursive: true, force: true });
10026
10095
  }
10027
10096
  }
10028
10097
  async function plan(projectDir, config, planDir, options = {}) {
@@ -10051,7 +10120,7 @@ import {
10051
10120
  mkdtempSync as mkdtempSync4,
10052
10121
  readFileSync as readFileSync10,
10053
10122
  readdirSync as readdirSync7,
10054
- rmSync as rmSync11,
10123
+ rmSync as rmSync9,
10055
10124
  statSync as statSync7,
10056
10125
  writeFileSync as writeFileSync9
10057
10126
  } from "node:fs";
@@ -10067,7 +10136,7 @@ import {
10067
10136
  linkSync as linkSync2,
10068
10137
  mkdirSync as mkdirSync11,
10069
10138
  mkdtempSync as mkdtempSync3,
10070
- rmSync as rmSync10,
10139
+ rmSync as rmSync8,
10071
10140
  statSync as statSync6,
10072
10141
  writeFileSync as writeFileSync8
10073
10142
  } from "node:fs";
@@ -10162,7 +10231,7 @@ var LocalPlanV2ArtifactPublisher = class {
10162
10231
  }
10163
10232
  renameWithRetry8(temporaryPath, destinationPath);
10164
10233
  } finally {
10165
- rmSync10(stagingDir, { recursive: true, force: true });
10234
+ rmSync8(stagingDir, { recursive: true, force: true });
10166
10235
  }
10167
10236
  }
10168
10237
  async commitManifest(manifestBytes) {
@@ -10179,7 +10248,7 @@ var LocalPlanV2ArtifactPublisher = class {
10179
10248
  }
10180
10249
  async abort() {
10181
10250
  if (!this.#committed) {
10182
- rmSync10(this.temporaryDir, { recursive: true, force: true });
10251
+ rmSync8(this.temporaryDir, { recursive: true, force: true });
10183
10252
  }
10184
10253
  }
10185
10254
  };
@@ -10448,7 +10517,7 @@ function writeBlob(sourcePath, destinationPath) {
10448
10517
  copyFileSync8(sourcePath, temporaryPath);
10449
10518
  renameWithRetry9(temporaryPath, destinationPath);
10450
10519
  } finally {
10451
- rmSync11(temporaryDir, { recursive: true, force: true });
10520
+ rmSync9(temporaryDir, { recursive: true, force: true });
10452
10521
  }
10453
10522
  }
10454
10523
  function publishBlobFor(absolutePath, digests) {
@@ -10581,7 +10650,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
10581
10650
  renameWithRetry9(tempDir, planV2Dir);
10582
10651
  return resultFromManifest(planV2Dir, publication.manifest);
10583
10652
  } catch (error) {
10584
- rmSync11(tempDir, { recursive: true, force: true });
10653
+ rmSync9(tempDir, { recursive: true, force: true });
10585
10654
  throw error;
10586
10655
  }
10587
10656
  }
@@ -10648,7 +10717,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
10648
10717
  }
10649
10718
  throw error;
10650
10719
  } finally {
10651
- rmSync11(stagingRoot, { recursive: true, force: true });
10720
+ rmSync9(stagingRoot, { recursive: true, force: true });
10652
10721
  }
10653
10722
  }
10654
10723
  async function planV2(projectDir, config, planV2Dir) {
@@ -10865,7 +10934,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
10865
10934
  );
10866
10935
  renameWithRetry9(tempDir, destinationDir);
10867
10936
  } catch (error) {
10868
- rmSync11(tempDir, { recursive: true, force: true });
10937
+ rmSync9(tempDir, { recursive: true, force: true });
10869
10938
  throw error;
10870
10939
  }
10871
10940
  return {
@@ -10929,7 +10998,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
10929
10998
  }
10930
10999
 
10931
11000
  // src/services/distributed/planV2Execution.ts
10932
- import { mkdtempSync as mkdtempSync6, rmSync as rmSync14 } from "node:fs";
11001
+ import { mkdtempSync as mkdtempSync6, rmSync as rmSync12 } from "node:fs";
10933
11002
  import { tmpdir as tmpdir2 } from "node:os";
10934
11003
  import { join as join28 } from "node:path";
10935
11004
 
@@ -10941,12 +11010,13 @@ import {
10941
11010
  mkdtempSync as mkdtempSync5,
10942
11011
  readFileSync as readFileSync11,
10943
11012
  readdirSync as readdirSync8,
10944
- rmSync as rmSync12,
11013
+ rmSync as rmSync10,
10945
11014
  statSync as statSync8,
10946
11015
  writeFileSync as writeFileSync10
10947
11016
  } from "node:fs";
10948
11017
  import { dirname as dirname13, join as join26 } from "node:path";
10949
11018
  import {
11019
+ appendRateCapArgs as appendRateCapArgs2,
10950
11020
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
10951
11021
  describeFfmpegFailure as describeFfmpegFailure4,
10952
11022
  getEncoderPreset as getEncoderPreset5,
@@ -11020,7 +11090,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
11020
11090
  });
11021
11091
  } finally {
11022
11092
  try {
11023
- rmSync12(workDir, { recursive: true, force: true });
11093
+ rmSync10(workDir, { recursive: true, force: true });
11024
11094
  } catch (err) {
11025
11095
  log.warn("[assemble] failed to remove work dir", {
11026
11096
  workDir,
@@ -11082,6 +11152,9 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
11082
11152
  "-r",
11083
11153
  fpsArg
11084
11154
  ];
11155
+ if (encoder.keyframeIntervalFrames !== void 0)
11156
+ args.push("-g", String(encoder.keyframeIntervalFrames));
11157
+ appendRateCapArgs2(args, encoder);
11085
11158
  appendSoftwareEncoderThreadsArg(args, "libx264");
11086
11159
  return args;
11087
11160
  }
@@ -11218,7 +11291,7 @@ function readCfrEncoder(planDir, format) {
11218
11291
  return encoderJson;
11219
11292
  }
11220
11293
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
11221
- if (existsSync18(outputPath)) rmSync12(outputPath, { recursive: true, force: true });
11294
+ if (existsSync18(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
11222
11295
  mkdirSync13(outputPath, { recursive: true });
11223
11296
  let globalIdx = 0;
11224
11297
  for (const chunkDir of chunkPaths) {
@@ -11263,7 +11336,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
11263
11336
 
11264
11337
  // src/services/distributed/renderChunk.ts
11265
11338
  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";
11339
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
11267
11340
  import { cpus } from "node:os";
11268
11341
  import { join as join27 } from "node:path";
11269
11342
  import {
@@ -11718,7 +11791,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11718
11791
  // first frame is an IDR keyframe and concat-copy at assemble time
11719
11792
  // round-trips losslessly.
11720
11793
  lockGopForChunkConcat: !isPngSequence,
11721
- gopSize: framesInChunk
11794
+ gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
11795
+ maxBitrate: encoder.maxBitrate,
11796
+ bufferSize: encoder.bufferSize
11722
11797
  };
11723
11798
  const captureStarted = Date.now();
11724
11799
  captureMode = await runCaptureWithScreenshotFallback({
@@ -11790,7 +11865,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11790
11865
  return observedChunkCaptureMode(capturePerfs);
11791
11866
  },
11792
11867
  resetForScreenshotRetry: () => {
11793
- rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11868
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11794
11869
  mkdirSync14(framesDir, { recursive: true });
11795
11870
  capturePerfs.length = 0;
11796
11871
  captureAttempts.length = 0;
@@ -11833,8 +11908,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11833
11908
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
11834
11909
  timeoutMs: cfg.ffmpegProcessTimeout
11835
11910
  });
11836
- rmSync13(videoOnlyPath, { force: true });
11837
- rmSync13(audioScratchPath, { force: true });
11911
+ rmSync11(videoOnlyPath, { force: true });
11912
+ rmSync11(audioScratchPath, { force: true });
11838
11913
  }
11839
11914
  encodeStageMs = Date.now() - encodeStarted;
11840
11915
  } finally {
@@ -11884,8 +11959,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11884
11959
  writeFileSync11(perfPath, `${JSON.stringify(perfPayload, null, 2)}
11885
11960
  `, "utf-8");
11886
11961
  try {
11887
- rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11888
- rmSync13(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11962
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11963
+ rmSync11(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11889
11964
  } catch (err) {
11890
11965
  log.warn("[renderChunk] failed to remove work dir", {
11891
11966
  workDir,
@@ -11920,7 +11995,7 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
11920
11995
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
11921
11996
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
11922
11997
  } finally {
11923
- rmSync14(workRoot, { recursive: true, force: true });
11998
+ rmSync12(workRoot, { recursive: true, force: true });
11924
11999
  }
11925
12000
  }
11926
12001
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
@@ -11940,7 +12015,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
11940
12015
  options
11941
12016
  );
11942
12017
  } finally {
11943
- rmSync14(workRoot, { recursive: true, force: true });
12018
+ rmSync12(workRoot, { recursive: true, force: true });
11944
12019
  }
11945
12020
  }
11946
12021
 
@@ -12033,6 +12108,20 @@ function validateDistributedRenderConfig(config) {
12033
12108
  `must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
12034
12109
  );
12035
12110
  }
12111
+ try {
12112
+ validateDeliveryEncoderConfig({
12113
+ format: config.format,
12114
+ fps: { num: config.fps, den: 1 },
12115
+ keyframeIntervalSeconds: config.keyframeIntervalSeconds,
12116
+ maxBitrate: config.maxBitrate,
12117
+ bufferSize: config.bufferSize
12118
+ });
12119
+ } catch (error) {
12120
+ throw new InvalidConfigError(
12121
+ "config.maxBitrate",
12122
+ error instanceof Error ? error.message : String(error)
12123
+ );
12124
+ }
12036
12125
  if (config.chunkSize !== void 0) {
12037
12126
  if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
12038
12127
  throw new InvalidConfigError(