@hyperframes/producer 0.8.181 → 0.8.183

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.
@@ -48,6 +48,8 @@ import {
48
48
  extractMediaMetadata as extractMediaMetadata4,
49
49
  resolveConfig as resolveConfig2,
50
50
  closeCaptureSession as closeCaptureSession8,
51
+ createCaptureSession as createCaptureSession8,
52
+ initializeSession as initializeSession8,
51
53
  createVideoFrameInjector,
52
54
  getEncoderPreset as getEncoderPreset3,
53
55
  distributeFrames as distributeFrames2,
@@ -1421,7 +1423,7 @@ function pidScope() {
1421
1423
  return {
1422
1424
  host: hostname(),
1423
1425
  // Without /proc (macOS, Windows) the boot time, to the minute, stands in for the boot id.
1424
- boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? String(Math.round(Date.now() / 6e4 - uptime() / 60)),
1426
+ boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? attempt(() => String(Math.round(Date.now() / 6e4 - uptime() / 60))),
1425
1427
  pidns: attempt(() => readlinkSync("/proc/self/ns/pid"))
1426
1428
  };
1427
1429
  }
@@ -1439,9 +1441,14 @@ function alive(pid) {
1439
1441
  return error.code === "EPERM";
1440
1442
  }
1441
1443
  }
1442
- function inThisPidScope(owner) {
1444
+ function knownPidScope() {
1443
1445
  const scope = pidScope();
1444
- return owner.host === scope.host && owner.boot === scope.boot && owner.pidns === scope.pidns;
1446
+ const unknown = scope.boot === void 0 || process.platform === "linux" && scope.pidns === void 0;
1447
+ return unknown ? void 0 : scope;
1448
+ }
1449
+ function inThisPidScope(owner) {
1450
+ const scope = knownPidScope();
1451
+ return scope !== void 0 && owner.host === scope.host && owner.boot === scope.boot && owner.pidns === scope.pidns;
1445
1452
  }
1446
1453
  function ownerState(dir) {
1447
1454
  let owner;
@@ -1506,7 +1513,9 @@ async function defaultArtifactDurationProbe(path) {
1506
1513
  const meta = await extractMediaMetadata(path);
1507
1514
  return {
1508
1515
  durationSeconds: meta.durationSeconds,
1509
- frames: meta.frames
1516
+ frames: meta.frames,
1517
+ width: meta.width,
1518
+ height: meta.height
1510
1519
  };
1511
1520
  }
1512
1521
  function buildArtifactExpectation(input) {
@@ -1515,7 +1524,9 @@ function buildArtifactExpectation(input) {
1515
1524
  const expectation = {
1516
1525
  expectedDurationSeconds: input.durationSeconds,
1517
1526
  fps: input.fps,
1518
- expectedFrames: input.expectedFrames
1527
+ expectedFrames: input.expectedFrames,
1528
+ expectedWidth: input.width,
1529
+ expectedHeight: input.height
1519
1530
  };
1520
1531
  if (input.outputFormat !== "hls") return expectation;
1521
1532
  return {
@@ -1583,6 +1594,16 @@ function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPa
1583
1594
  `Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
1584
1595
  );
1585
1596
  }
1597
+ function assertSizeMatches(expected, probed, stagingPath) {
1598
+ const { expectedWidth: width, expectedHeight: height } = expected;
1599
+ if (width === void 0 || height === void 0) return;
1600
+ if (probed.width === void 0 || probed.height === void 0) return;
1601
+ const fits = (want, got) => got === want || got === want + want % 2;
1602
+ if (fits(width, probed.width) && fits(height, probed.height)) return;
1603
+ throw new Error(
1604
+ `Render artifact has the wrong size: expected ${width}x${height}, probed ${probed.width}x${probed.height}. Artifact: ${stagingPath}`
1605
+ );
1606
+ }
1586
1607
  var ArtifactTransaction = class {
1587
1608
  constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
1588
1609
  this.kind = kind;
@@ -1653,6 +1674,7 @@ var ArtifactTransaction = class {
1653
1674
  if (expected.expectedFrames !== void 0) {
1654
1675
  assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
1655
1676
  }
1677
+ assertSizeMatches(expected, probed, probePath);
1656
1678
  }
1657
1679
  async runDurationProbe(probePath) {
1658
1680
  try {
@@ -3201,7 +3223,9 @@ function aggregateDrawElement(perfs, de) {
3201
3223
  if (perfs.length === 0) return void 0;
3202
3224
  const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
3203
3225
  const gateReasons = [
3204
- ...new Set(perfs.map((p) => p.deGateReason).filter((r) => !!r))
3226
+ ...new Set(
3227
+ [de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
3228
+ )
3205
3229
  ].sort();
3206
3230
  const gpuRenderers = [
3207
3231
  ...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
@@ -8665,6 +8689,7 @@ async function runAudioStage(input) {
8665
8689
  audios,
8666
8690
  masterFxChain,
8667
8691
  urlByAbsentLocalPath,
8692
+ keepEncoderInputAt,
8668
8693
  abortSignal,
8669
8694
  assertNotAborted,
8670
8695
  log
@@ -8684,7 +8709,13 @@ async function runAudioStage(input) {
8684
8709
  audioOutputPath,
8685
8710
  duration,
8686
8711
  abortSignal,
8687
- { ffmpegProcessTimeout, audioGain, masterFxChain, urlByAbsentLocalPath },
8712
+ {
8713
+ ffmpegProcessTimeout,
8714
+ audioGain,
8715
+ masterFxChain,
8716
+ urlByAbsentLocalPath,
8717
+ keepEncoderInputAt
8718
+ },
8688
8719
  compiledDir
8689
8720
  );
8690
8721
  } catch (err) {
@@ -8798,13 +8829,29 @@ function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, free
8798
8829
  }
8799
8830
  return { available: false, estimatedBytes, freeBytes };
8800
8831
  }
8801
- function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis) {
8832
+ function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
8833
+ const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
8834
+ if (workerCount > 1 && oneWorker.enabled) {
8835
+ return `This render stores frames because it captures with ${workerCount} workers. Free up disk space, or re-run this render with --workers 1 to stream frames into the encoder instead. One worker is slower, so keep the default worker count for renders that fit on disk.`;
8836
+ }
8837
+ switch (oneWorker.reason) {
8838
+ case "format_excluded":
8839
+ return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
8840
+ case "disabled_by_config":
8841
+ return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
8842
+ case "duration_cap":
8843
+ return "The streaming duration cap (PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED=true) sends renders longer than PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS to disk at any worker count. Free up disk space, unset the cap, or use --low-memory-mode, which streams past it on one worker.";
8844
+ default:
8845
+ return "Free up disk space.";
8846
+ }
8847
+ }
8848
+ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
8802
8849
  const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
8803
8850
  return new Error(
8804
- `Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha). mp4/mov renders stream instead: always at --workers 1, and multi-worker on Linux BeginFrame capture. This render landed on disk because the output is png-sequence or gif, because it is multi-worker screenshot capture (opt in with HF_CAPTURE_PARALLEL_STREAM=true), or because streaming was switched off: HF_CAPTURE_PARALLEL_STREAM=false, PRODUCER_ENABLE_STREAMING_ENCODE=false, or the duration cap (PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED=true) on a render longer than PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS. HDR and shader-transition renders never reach this check; they run their own compositor. Re-run with --workers 1, use --low-memory-mode, or free up disk space.`
8851
+ `Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha). ${remedy}`
8805
8852
  );
8806
8853
  }
8807
- function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes) {
8854
+ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy, freeDiskBytes) {
8808
8855
  const headroom = inspectDiskCaptureHeadroom(
8809
8856
  framesDir,
8810
8857
  totalFrames,
@@ -8816,7 +8863,8 @@ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeD
8816
8863
  headroom.estimatedBytes,
8817
8864
  headroom.freeBytes,
8818
8865
  framesDir,
8819
- "estimate"
8866
+ "estimate",
8867
+ remedy
8820
8868
  );
8821
8869
  }
8822
8870
  function measureCaptureFrameBytes(workDir, framesDir) {
@@ -8836,7 +8884,7 @@ function measureCaptureFrameBytes(workDir, framesDir) {
8836
8884
  return total;
8837
8885
  }
8838
8886
  function createDiskCaptureProjection(input) {
8839
- const { framesDir, totalFrames, measureBytes, freeDiskBytes = freeDiskBytesAt } = input;
8887
+ const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
8840
8888
  let checked = false;
8841
8889
  return (capturedFrames) => {
8842
8890
  if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
@@ -8848,7 +8896,7 @@ function createDiskCaptureProjection(input) {
8848
8896
  if (remaining <= 0 || freeBytes === null) return;
8849
8897
  const needBytes = Math.ceil(measured / capturedFrames * remaining);
8850
8898
  if (needBytes > freeBytes * 0.9) {
8851
- throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured");
8899
+ throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
8852
8900
  }
8853
8901
  };
8854
8902
  }
@@ -8890,11 +8938,13 @@ async function runCaptureStage(input) {
8890
8938
  }
8891
8939
  }
8892
8940
  const captureOptions = buildCaptureOptions();
8893
- assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions);
8941
+ const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
8942
+ assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy);
8894
8943
  const checkDiskProjection = createDiskCaptureProjection({
8895
8944
  framesDir,
8896
8945
  totalFrames,
8897
- measureBytes: () => measureCaptureFrameBytes(workDir, framesDir)
8946
+ measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
8947
+ remedy
8898
8948
  });
8899
8949
  if (workerCount > 1) {
8900
8950
  const attempts = await executeDiskCaptureWithAdaptiveRetry({
@@ -10105,8 +10155,10 @@ function fpsToFfmpegArg2(fps) {
10105
10155
  }
10106
10156
  var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
10107
10157
  var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
10158
+ var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
10108
10159
  function framesInput(input, fpsArg) {
10109
10160
  return [
10161
+ ...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
10110
10162
  "-framerate",
10111
10163
  fpsArg,
10112
10164
  ...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
@@ -10357,7 +10409,8 @@ import { dirname as dirname9, join as join25 } from "node:path";
10357
10409
  import {
10358
10410
  DEFAULT_CONFIG,
10359
10411
  describeFfmpegFailure as describeFfmpegFailure2,
10360
- extractAudioMetadata as extractAudioMetadata2,
10412
+ encodeAac,
10413
+ extractAudioMetadata as extractAudioMetadata3,
10361
10414
  getFfprobeBinary,
10362
10415
  ManagedChildProcess,
10363
10416
  runFfmpeg as runFfmpeg5,
@@ -10366,7 +10419,12 @@ import {
10366
10419
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
10367
10420
 
10368
10421
  // src/services/render/audioTruePeak.ts
10369
- import { aacSegmentCount, describeFfmpegFailure, runFfmpeg as runFfmpeg4 } from "@hyperframes/engine";
10422
+ import {
10423
+ aacSegmentCount,
10424
+ describeFfmpegFailure,
10425
+ extractAudioMetadata as extractAudioMetadata2,
10426
+ runFfmpeg as runFfmpeg4
10427
+ } from "@hyperframes/engine";
10370
10428
  var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10371
10429
  function checkedDbfs(value) {
10372
10430
  if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
@@ -10404,12 +10462,13 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
10404
10462
  };
10405
10463
  }
10406
10464
  async function measureTruePeakDbfs(audioPath, options) {
10407
- const { timeoutMs, signal, segments } = options;
10465
+ const { timeoutMs, signal } = options;
10408
10466
  const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
10467
+ const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
10409
10468
  const block = 4800;
10410
- const count = segments?.sampleRate === 48e3 ? aacSegmentCount(segments.durationSeconds) : 1;
10411
- if (!segments || count === 1) return wholeFile();
10412
- const cut = (k) => k * Math.floor(segments.durationSeconds * 10 / count) * block;
10469
+ const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
10470
+ if (count === 1) return wholeFile();
10471
+ const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
10413
10472
  const peers = new AbortController();
10414
10473
  const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
10415
10474
  const scans = await Promise.all(
@@ -10438,24 +10497,8 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10438
10497
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10439
10498
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
10440
10499
  var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
10441
- function buildAacTruePeakCorrectionArgs(inputPath, outputPath, targetDurationSeconds, attenuationDb) {
10442
- return [
10443
- "-i",
10444
- inputPath,
10445
- "-map",
10446
- "0:a:0",
10447
- "-vn",
10448
- "-af",
10449
- `volume=${attenuationDb.toFixed(3)}dB`,
10450
- "-t",
10451
- formatSeconds(targetDurationSeconds),
10452
- "-c:a",
10453
- "aac",
10454
- "-b:a",
10455
- "192k",
10456
- "-y",
10457
- outputPath
10458
- ];
10500
+ function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
10501
+ return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10459
10502
  }
10460
10503
  function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
10461
10504
  const delta = targetDurationSeconds - sourceDurationSeconds;
@@ -10470,53 +10513,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
10470
10513
  if (delta > 0) {
10471
10514
  return {
10472
10515
  operation: "pad",
10473
- steps: [
10474
- {
10475
- kind: "normalize",
10476
- args: [
10477
- "-i",
10478
- audioPath,
10479
- "-af",
10480
- `apad,atrim=0:${targetSec}`,
10481
- "-t",
10482
- targetSec,
10483
- "-c:a",
10484
- "aac",
10485
- "-b:a",
10486
- "192k",
10487
- "-y",
10488
- outputPath
10489
- ]
10490
- }
10491
- ],
10516
+ steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
10492
10517
  cleanupPaths: []
10493
10518
  };
10494
10519
  }
10495
10520
  return {
10496
10521
  operation: "trim",
10497
- steps: [
10498
- {
10499
- kind: "trim",
10500
- args: [
10501
- "-i",
10502
- audioPath,
10503
- "-af",
10504
- `atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
10505
- // `atrim` limits decoded samples but does not cap muxer timestamps
10506
- // introduced by encoder delay/flush. The output duration contract
10507
- // is enforced at the container boundary as well; without `-t`, a
10508
- // long primed source can retain its original tail after re-encode.
10509
- "-t",
10510
- targetSec,
10511
- "-c:a",
10512
- "aac",
10513
- "-b:a",
10514
- "192k",
10515
- "-y",
10516
- outputPath
10517
- ]
10518
- }
10519
- ],
10522
+ steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
10520
10523
  cleanupPaths: []
10521
10524
  };
10522
10525
  }
@@ -10533,7 +10536,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10533
10536
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10534
10537
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10535
10538
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10536
- const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10539
+ const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
10540
+ const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10537
10541
  const [videoResult, audioResult] = await Promise.allSettled([
10538
10542
  probeVideo(input.videoPath),
10539
10543
  probeAudio(input.audioPath, input.signal)
@@ -10580,7 +10584,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10580
10584
  );
10581
10585
  try {
10582
10586
  for (const step of plan.steps) {
10583
- const ffmpegResult = await runner(step.args);
10587
+ const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
10584
10588
  if (!ffmpegResult.success) {
10585
10589
  return {
10586
10590
  success: false,
@@ -10612,7 +10616,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10612
10616
  targetDurationSeconds,
10613
10617
  signal: input.signal,
10614
10618
  probeTruePeak,
10615
- runner
10619
+ encode
10616
10620
  });
10617
10621
  if (!correction.success) {
10618
10622
  return {
@@ -10660,13 +10664,11 @@ async function enforceAacTruePeak(input) {
10660
10664
  }
10661
10665
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
10662
10666
  attenuationDb += correctionTargetDbfs - truePeakDbfs;
10663
- const result = await input.runner(
10664
- buildAacTruePeakCorrectionArgs(
10665
- input.audioPath,
10666
- correctedPath,
10667
- input.targetDurationSeconds,
10668
- attenuationDb
10669
- )
10667
+ const result = await input.encode(
10668
+ input.audioPath,
10669
+ correctedPath,
10670
+ input.targetDurationSeconds,
10671
+ aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
10670
10672
  );
10671
10673
  if (!result.success) {
10672
10674
  return {
@@ -10761,7 +10763,7 @@ function parseFrameRate(rate) {
10761
10763
  return { fpsNum, fpsDen };
10762
10764
  }
10763
10765
  async function defaultProbeAudioInfo(audioPath, signal) {
10764
- const metadata = await extractAudioMetadata2(audioPath, { signal });
10766
+ const metadata = await extractAudioMetadata3(audioPath, { signal });
10765
10767
  return {
10766
10768
  durationSeconds: metadata.durationSeconds,
10767
10769
  sampleRate: metadata.sampleRate,
@@ -10770,7 +10772,18 @@ async function defaultProbeAudioInfo(audioPath, signal) {
10770
10772
  };
10771
10773
  }
10772
10774
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
10773
- const result = await runFfmpeg5(args, { signal, timeout: timeoutMs });
10775
+ return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
10776
+ }
10777
+ function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
10778
+ return async (inputPath, outputPath, durationSeconds, filter) => {
10779
+ const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
10780
+ return toStepResult(
10781
+ await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
10782
+ timeoutMs
10783
+ );
10784
+ };
10785
+ }
10786
+ function toStepResult(result, timeoutMs) {
10774
10787
  if (result.success) return { success: true };
10775
10788
  return {
10776
10789
  success: false,
@@ -10958,7 +10971,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
10958
10971
  const reason = UNSUPPORTED_REASON2[route];
10959
10972
  if (reason === null) return;
10960
10973
  throw new Error(
10961
- `[Render] --resolution other than the composition's size is not supported on the "${route}" capture route: ${reason}. Render at the composition's size, or as mp4 with page-side compositing.`
10974
+ `[Render] --resolution other than the composition's size is not supported on the "${route}" capture route: ${reason}. Render at the composition's size; or, for HDR footage, add --sdr to render this size in standard colour; or, for shader transitions, render as mp4.`
10962
10975
  );
10963
10976
  }
10964
10977
 
@@ -11175,7 +11188,7 @@ function getNextRetryWorkerCount(currentWorkers) {
11175
11188
  return Math.max(1, Math.floor(currentWorkers / 2));
11176
11189
  }
11177
11190
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11178
- if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
11191
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11179
11192
  return join26(dirname10(outputPath), `work-${jobId}-`);
11180
11193
  }
11181
11194
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
@@ -11669,6 +11682,9 @@ function resolveDeShortBand(args) {
11669
11682
  function shouldPreferSingleWorkerDrawElement(args) {
11670
11683
  return args.workerCount > 1 && typeof args.requestedWorkers !== "number" && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
11671
11684
  }
11685
+ function isProbeDeGated(session) {
11686
+ return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
11687
+ }
11672
11688
  function resolveInversionRetryPlan(args) {
11673
11689
  if (args.deWorkerInversion !== "inverted") return null;
11674
11690
  const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
@@ -11750,7 +11766,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
11750
11766
  }
11751
11767
  function resolveParallelCaptureMode(args) {
11752
11768
  const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
11753
- return args.headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
11769
+ const headlessShell = args.headlessShellPath !== void 0;
11770
+ return headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
11754
11771
  }
11755
11772
  function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
11756
11773
  const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
@@ -12358,6 +12375,7 @@ async function executeRenderPipeline(input) {
12358
12375
  );
12359
12376
  const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
12360
12377
  const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
12378
+ if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
12361
12379
  const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
12362
12380
  const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
12363
12381
  const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
@@ -12398,7 +12416,7 @@ async function executeRenderPipeline(input) {
12398
12416
  singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
12399
12417
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12400
12418
  supersampling: deviceScaleFactor !== 1,
12401
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12419
+ probeDeGated: isProbeDeGated(probeSession),
12402
12420
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
12403
12421
  process.env.HF_DE_PARALLEL_STREAM === "true"
12404
12422
  };
@@ -12407,14 +12425,14 @@ async function executeRenderPipeline(input) {
12407
12425
  ...deInversionArgs,
12408
12426
  minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
12409
12427
  });
12410
- const deShortBand = resolveDeShortBand({
12428
+ let deShortBand = resolveDeShortBand({
12411
12429
  invertAtBaseFloor,
12412
12430
  invertAtBandFloor,
12413
12431
  bandEnabled: deShortBandEnabled,
12414
12432
  bandOpen: deShortBandOpen,
12415
12433
  elementCountSource: compositionElementCountSource
12416
12434
  });
12417
- const deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12435
+ let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12418
12436
  const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
12419
12437
  const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
12420
12438
  const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
@@ -12423,7 +12441,7 @@ async function executeRenderPipeline(input) {
12423
12441
  const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
12424
12442
  const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
12425
12443
  const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
12426
- const deParallelRouterEligible = shouldPreferParallelDrawElement({
12444
+ let deParallelRouterEligible = shouldPreferParallelDrawElement({
12427
12445
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12428
12446
  requestedWorkers: job.config.workers,
12429
12447
  useDrawElement: cfg.useDrawElement,
@@ -12434,7 +12452,7 @@ async function executeRenderPipeline(input) {
12434
12452
  minFrames: deParallelMinFrames,
12435
12453
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12436
12454
  supersampling: deviceScaleFactor !== 1,
12437
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12455
+ probeDeGated: isProbeDeGated(probeSession),
12438
12456
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12439
12457
  routerEnabled: deParallelRouterEnabled,
12440
12458
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12450,6 +12468,35 @@ async function executeRenderPipeline(input) {
12450
12468
  totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
12451
12469
  minMemoryMb: deParallelMinMemoryMb
12452
12470
  });
12471
+ if (probeSession === null && (deInversionEligible || deParallelRouterEligible)) {
12472
+ try {
12473
+ probeSession = await createCaptureSession8(
12474
+ activeFileServer.url,
12475
+ framesDir,
12476
+ buildCaptureOptions(),
12477
+ null,
12478
+ { ...cfg, forceScreenshot: captureForceScreenshot }
12479
+ );
12480
+ await initializeSession8(probeSession);
12481
+ } catch (error) {
12482
+ assertNotAborted();
12483
+ if (probeSession) {
12484
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
12485
+ await closeCaptureSession8(probeSession).catch(() => {
12486
+ });
12487
+ probeSession = null;
12488
+ }
12489
+ log.warn("[Render] Fast capture gate check failed; routing without it.", {
12490
+ error: error instanceof Error ? error.message : String(error)
12491
+ });
12492
+ }
12493
+ if (isProbeDeGated(probeSession)) {
12494
+ deInversionEligible = false;
12495
+ deParallelRouterEligible = false;
12496
+ deShortBand = void 0;
12497
+ }
12498
+ }
12499
+ const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
12453
12500
  let workerCount;
12454
12501
  const captureStageObservationData = (extra = {}) => ({
12455
12502
  stagePhase: "capturing",
@@ -12637,7 +12684,7 @@ async function executeRenderPipeline(input) {
12637
12684
  }
12638
12685
  const parallelCaptureMode = resolveParallelCaptureMode({
12639
12686
  platform: process.platform,
12640
- headlessShell: resolveHeadlessShellPath(cfg) !== null,
12687
+ headlessShellPath: resolveHeadlessShellPath(cfg),
12641
12688
  forceScreenshot: captureForceScreenshot,
12642
12689
  deviceScaleFactor: captureOptions.deviceScaleFactor
12643
12690
  });
@@ -13409,7 +13456,9 @@ async function executeRenderPipeline(input) {
13409
13456
  outputFormat,
13410
13457
  durationSeconds: job.duration,
13411
13458
  fps: fpsToNumber9(job.config.fps),
13412
- expectedFrames: captureTotalFrames
13459
+ expectedFrames: captureTotalFrames,
13460
+ width: outputWidth,
13461
+ height: outputHeight
13413
13462
  })
13414
13463
  );
13415
13464
  const totalElapsed = Date.now() - pipelineStart;
@@ -13445,6 +13494,7 @@ async function executeRenderPipeline(input) {
13445
13494
  drawElement: {
13446
13495
  compileGate: deCompileGate,
13447
13496
  clampReason: deClampReason,
13497
+ probeGateReason: deProbeGateReason,
13448
13498
  workerInversion: deWorkerInversion,
13449
13499
  preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
13450
13500
  compositionElementCount,
@@ -13846,9 +13896,9 @@ import {
13846
13896
  // src/services/distributed/planFrameCost.ts
13847
13897
  import {
13848
13898
  closeCaptureSession as closeCaptureSession9,
13849
- createCaptureSession as createCaptureSession8,
13899
+ createCaptureSession as createCaptureSession9,
13850
13900
  createFrameLookupTable as createFrameLookupTable3,
13851
- initializeSession as initializeSession8
13901
+ initializeSession as initializeSession9
13852
13902
  } from "@hyperframes/engine";
13853
13903
 
13854
13904
  // src/services/distributed/chunkFrames.ts
@@ -68,6 +68,7 @@ export declare function assemble(planDir: string, chunkOutputs: AssembleChunkPat
68
68
  */
69
69
  cfr?: boolean;
70
70
  }): Promise<AssembleResult>;
71
- /** Chunks that carry their audio are stitched under one index, the same bytes as the S3 stitch. */
71
+ /** Chunks carrying audio, or with it as a separate file, stitch under one index like S3's. */
72
72
  export declare function chunksAreStitchedMp4(planDir: string, cfr: boolean): boolean;
73
+ export declare function stitchesSeparateAudio(planDir: string): boolean;
73
74
  export {};
@@ -1,4 +1,4 @@
1
- import { type BeforeCaptureHook, BROWSER_GPU_NOT_SOFTWARE, type CaptureOptions, type EngineConfig, type ExtractedFrames, type FrameLookupTable } from "@hyperframes/engine";
1
+ import { type BeforeCaptureHook, BROWSER_GPU_NOT_SOFTWARE, type CaptureOptions, type EngineConfig, type ExtractedFrames, type FrameLookupTable, type ShownBox } from "@hyperframes/engine";
2
2
  import type { LockedRenderConfig } from "../render/stages/freezePlan.js";
3
3
  import type { PlanV2RemoteSources } from "./planV2.js";
4
4
  import { type DistributedFormat, INVALID_VIDEO_METADATA, type PlanVideosJson } from "./shared.js";
@@ -39,6 +39,10 @@ export declare function buildChunkCaptureOptions(input: {
39
39
  videoMetadataHints?: CaptureOptions["videoMetadataHints"];
40
40
  }): CaptureOptions;
41
41
  /** Extracts the given frames of deferred videos under the names a full extraction gives them. */
42
+ export declare function shownDeferredFrameIndexes(planVideos: PlanVideosJson, frames: Iterable<number>, fps: {
43
+ num: number;
44
+ den: number;
45
+ }): Map<string, Set<number>>;
42
46
  export declare function extractDeferredVideoFramesForChunk(input: {
43
47
  planDir: string;
44
48
  planVideos: PlanVideosJson;
@@ -50,4 +54,5 @@ export declare function extractDeferredVideoFramesForChunk(input: {
50
54
  outputRoot: string;
51
55
  cfg: Pick<EngineConfig, "ffmpegProcessTimeout">;
52
56
  remoteSources?: PlanV2RemoteSources;
57
+ shownBoxes?: ReadonlyMap<string, ShownBox>;
53
58
  }): Promise<ExtractedFrames[]>;
@@ -31,9 +31,9 @@ export interface TrackTable {
31
31
  }
32
32
  /** Each track's sample tables, expanded to one entry per sample (and per chunk). */
33
33
  export declare function readTracks(moov: Buffer): TrackTable[];
34
- export declare function buildStitchedHeader(layouts: readonly Mp4Layout[]): Buffer;
34
+ export declare function buildStitchedHeader(layouts: readonly Mp4Layout[], audio?: Mp4Layout): Buffer;
35
35
  export declare function fileRange(path: string): ReadRange;
36
- /** Local join: the stitched header, then each chunk's mdat payload in order. Like the ffmpeg
37
- * join, it writes in a work folder beside `outputPath` and renames only a finished file there. */
38
- export declare function writeStitchedMp4(chunkPaths: readonly string[], outputPath: string): Promise<void>;
36
+ /** Local join: the header, then `audioPath`'s and each chunk's payload. Like the ffmpeg join, it
37
+ * writes in a work folder beside `outputPath` and renames only a finished file there. */
38
+ export declare function writeStitchedMp4(chunkPaths: readonly string[], outputPath: string, audioPath?: string): Promise<void>;
39
39
  export {};
@@ -211,6 +211,8 @@ export interface BuildLocalExecutionPlanOptions {
211
211
  readonly chunksCarryAudio?: boolean;
212
212
  /** With `chunksCarryAudio`, also write each chunk's audio packets to their own file. Plan v2 only. */
213
213
  readonly sliceChunkAudio?: boolean;
214
+ /** Leave the audio to {@link buildPlanAudio}; chunks render video only. mp4 without `cfr` only. */
215
+ readonly deferAudio?: boolean;
214
216
  }
215
217
  export interface RemoteProjectFile {
216
218
  readonly url: string;
@@ -401,7 +403,16 @@ export declare function resolveDistributedEngineConfig(config: DistributedRender
401
403
  * Build the shared local execution representation used by both transport
402
404
  * protocols. See the module docstring for the directory layout.
403
405
  */
406
+ /** Whether the plan leaves its audio to {@link buildPlanAudio}. */
407
+ export declare function plansSeparateAudio(config: Pick<DistributedRenderConfig, "format" | "cfr">, options: Pick<BuildLocalExecutionPlanOptions, "deferAudio">): boolean;
404
408
  export declare function buildLocalExecutionPlan(projectDir: string, config: DistributedRenderConfig, executionPlanDir: string, options?: Readonly<BuildLocalExecutionPlanOptions>): Promise<LocalExecutionPlan>;
409
+ /**
410
+ * The audio of a plan made with `deferAudio`: the mix and true-peak hold that plan would have
411
+ * made, written to `outputPath` (on `workRoot`'s filesystem) at exactly the video's length.
412
+ */
413
+ export declare function buildPlanAudio(projectDir: string, config: DistributedRenderConfig, workRoot: string, outputPath: string, options?: Pick<BuildLocalExecutionPlanOptions, "remoteFiles">): Promise<{
414
+ hasAudio: boolean;
415
+ }>;
405
416
  /**
406
417
  * Legacy v1 transport wrapper around {@link buildLocalExecutionPlan}.
407
418
  *
@@ -91,6 +91,7 @@ export interface PlanV2WithPublisherOptions {
91
91
  /** See {@link BuildLocalExecutionPlanOptions.remoteFiles}; needs a publisher with `referenceStoredBlob`. */
92
92
  readonly remoteFiles?: BuildLocalExecutionPlanOptions["remoteFiles"];
93
93
  readonly chunksCarryAudio?: boolean;
94
+ readonly deferAudio?: boolean;
94
95
  }
95
96
  export interface PlanV2MaterializationResult {
96
97
  readonly planDir: string;
@@ -22,9 +22,6 @@ export declare function listSiteFiles(projectDir: string): string[];
22
22
  * same-bytes rename still yields a fresh id. We trim to 16 chars because the
23
23
  * full 64 isn't useful in an object key for legibility. Pass `files` when the
24
24
  * caller already listed them, so the hashed set is the shipped set.
25
- *
26
- * Reads are synchronous: project trees are typically tens of MB at most
27
- * (HTML/CSS/JS plus a few composition assets), so the simpler shape wins over
28
- * a streaming pipeline.
25
+ * Files are read in fixed-size chunks, so no file is ever held whole in memory.
29
26
  */
30
27
  export declare function hashProjectDir(projectDir: string, files?: readonly string[]): string;