@hyperframes/producer 0.8.180 → 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,
@@ -10356,36 +10408,97 @@ import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSyn
10356
10408
  import { dirname as dirname9, join as join25 } from "node:path";
10357
10409
  import {
10358
10410
  DEFAULT_CONFIG,
10359
- describeFfmpegFailure,
10360
- extractAudioMetadata as extractAudioMetadata2,
10411
+ describeFfmpegFailure as describeFfmpegFailure2,
10412
+ encodeAac,
10413
+ extractAudioMetadata as extractAudioMetadata3,
10361
10414
  getFfprobeBinary,
10362
10415
  ManagedChildProcess,
10363
- runFfmpeg as runFfmpeg4,
10416
+ runFfmpeg as runFfmpeg5,
10364
10417
  trackChildProcess
10365
10418
  } from "@hyperframes/engine";
10366
10419
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
10420
+
10421
+ // src/services/render/audioTruePeak.ts
10422
+ import {
10423
+ aacSegmentCount,
10424
+ describeFfmpegFailure,
10425
+ extractAudioMetadata as extractAudioMetadata2,
10426
+ runFfmpeg as runFfmpeg4
10427
+ } from "@hyperframes/engine";
10428
+ var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10429
+ function checkedDbfs(value) {
10430
+ if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10431
+ throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
10432
+ }
10433
+ return value;
10434
+ }
10435
+ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
10436
+ let firstBlock;
10437
+ let laterBlocks = Number.NEGATIVE_INFINITY;
10438
+ let partialLine = "";
10439
+ const readLines = (chunk) => {
10440
+ const lines = (partialLine + chunk).split(/\r|\n/);
10441
+ partialLine = lines.pop() ?? "";
10442
+ for (const line of lines) {
10443
+ const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
10444
+ if (!ftpk) continue;
10445
+ const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
10446
+ if (firstBlock === void 0) firstBlock = block;
10447
+ else laterBlocks = Math.max(laterBlocks, block);
10448
+ }
10449
+ };
10450
+ const result = await runFfmpeg4(
10451
+ ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
10452
+ { signal, timeout: timeoutMs, onStderr: readLines }
10453
+ );
10454
+ if (!result.success) {
10455
+ throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
10456
+ }
10457
+ const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10458
+ return {
10459
+ peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
10460
+ firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
10461
+ laterBlocks: checkedDbfs(laterBlocks)
10462
+ };
10463
+ }
10464
+ async function measureTruePeakDbfs(audioPath, options) {
10465
+ const { timeoutMs, signal } = options;
10466
+ const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
10467
+ const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
10468
+ const block = 4800;
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;
10472
+ const peers = new AbortController();
10473
+ const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
10474
+ const scans = await Promise.all(
10475
+ Array.from({ length: count }, (_, k) => {
10476
+ const start = k === 0 ? 0 : cut(k) - block;
10477
+ const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
10478
+ return scanTruePeak(
10479
+ audioPath,
10480
+ `atrim=start_sample=${start}${end},`,
10481
+ timeoutMs,
10482
+ segmentSignal
10483
+ );
10484
+ })
10485
+ ).catch((error) => {
10486
+ peers.abort();
10487
+ throw error;
10488
+ });
10489
+ const last = scans[count - 1];
10490
+ const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
10491
+ if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
10492
+ return wholeFile();
10493
+ }
10494
+
10495
+ // src/services/render/audioPadTrim.ts
10367
10496
  var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10368
10497
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10369
10498
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
10370
10499
  var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
10371
- function buildAacTruePeakCorrectionArgs(inputPath, outputPath, targetDurationSeconds, attenuationDb) {
10372
- return [
10373
- "-i",
10374
- inputPath,
10375
- "-map",
10376
- "0:a:0",
10377
- "-vn",
10378
- "-af",
10379
- `volume=${attenuationDb.toFixed(3)}dB`,
10380
- "-t",
10381
- formatSeconds(targetDurationSeconds),
10382
- "-c:a",
10383
- "aac",
10384
- "-b:a",
10385
- "192k",
10386
- "-y",
10387
- outputPath
10388
- ];
10500
+ function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
10501
+ return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10389
10502
  }
10390
10503
  function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
10391
10504
  const delta = targetDurationSeconds - sourceDurationSeconds;
@@ -10400,53 +10513,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
10400
10513
  if (delta > 0) {
10401
10514
  return {
10402
10515
  operation: "pad",
10403
- steps: [
10404
- {
10405
- kind: "normalize",
10406
- args: [
10407
- "-i",
10408
- audioPath,
10409
- "-af",
10410
- `apad,atrim=0:${targetSec}`,
10411
- "-t",
10412
- targetSec,
10413
- "-c:a",
10414
- "aac",
10415
- "-b:a",
10416
- "192k",
10417
- "-y",
10418
- outputPath
10419
- ]
10420
- }
10421
- ],
10516
+ steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
10422
10517
  cleanupPaths: []
10423
10518
  };
10424
10519
  }
10425
10520
  return {
10426
10521
  operation: "trim",
10427
- steps: [
10428
- {
10429
- kind: "trim",
10430
- args: [
10431
- "-i",
10432
- audioPath,
10433
- "-af",
10434
- `atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
10435
- // `atrim` limits decoded samples but does not cap muxer timestamps
10436
- // introduced by encoder delay/flush. The output duration contract
10437
- // is enforced at the container boundary as well; without `-t`, a
10438
- // long primed source can retain its original tail after re-encode.
10439
- "-t",
10440
- targetSec,
10441
- "-c:a",
10442
- "aac",
10443
- "-b:a",
10444
- "192k",
10445
- "-y",
10446
- outputPath
10447
- ]
10448
- }
10449
- ],
10522
+ steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
10450
10523
  cleanupPaths: []
10451
10524
  };
10452
10525
  }
@@ -10463,7 +10536,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10463
10536
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10464
10537
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10465
10538
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10466
- const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => defaultProbeAudioTruePeakDbfs(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 }));
10467
10541
  const [videoResult, audioResult] = await Promise.allSettled([
10468
10542
  probeVideo(input.videoPath),
10469
10543
  probeAudio(input.audioPath, input.signal)
@@ -10510,7 +10584,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10510
10584
  );
10511
10585
  try {
10512
10586
  for (const step of plan.steps) {
10513
- 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);
10514
10588
  if (!ffmpegResult.success) {
10515
10589
  return {
10516
10590
  success: false,
@@ -10542,7 +10616,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10542
10616
  targetDurationSeconds,
10543
10617
  signal: input.signal,
10544
10618
  probeTruePeak,
10545
- runner
10619
+ encode
10546
10620
  });
10547
10621
  if (!correction.success) {
10548
10622
  return {
@@ -10590,13 +10664,11 @@ async function enforceAacTruePeak(input) {
10590
10664
  }
10591
10665
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
10592
10666
  attenuationDb += correctionTargetDbfs - truePeakDbfs;
10593
- const result = await input.runner(
10594
- buildAacTruePeakCorrectionArgs(
10595
- input.audioPath,
10596
- correctedPath,
10597
- input.targetDurationSeconds,
10598
- attenuationDb
10599
- )
10667
+ const result = await input.encode(
10668
+ input.audioPath,
10669
+ correctedPath,
10670
+ input.targetDurationSeconds,
10671
+ aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
10600
10672
  );
10601
10673
  if (!result.success) {
10602
10674
  return {
@@ -10691,7 +10763,7 @@ function parseFrameRate(rate) {
10691
10763
  return { fpsNum, fpsDen };
10692
10764
  }
10693
10765
  async function defaultProbeAudioInfo(audioPath, signal) {
10694
- const metadata = await extractAudioMetadata2(audioPath, { signal });
10766
+ const metadata = await extractAudioMetadata3(audioPath, { signal });
10695
10767
  return {
10696
10768
  durationSeconds: metadata.durationSeconds,
10697
10769
  sampleRate: metadata.sampleRate,
@@ -10699,28 +10771,23 @@ async function defaultProbeAudioInfo(audioPath, signal) {
10699
10771
  audioCodec: metadata.audioCodec
10700
10772
  };
10701
10773
  }
10702
- async function defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal) {
10703
- const result = await runFfmpeg4(
10704
- ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"],
10705
- { signal, timeout: timeoutMs }
10706
- );
10707
- if (!result.success) {
10708
- throw new Error(`[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`);
10709
- }
10710
- const matches = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10711
- const token = matches.at(-1)?.[1]?.toLowerCase();
10712
- const value = token === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10713
- if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10714
- throw new Error("[audioPadTrim] FFmpeg did not report AAC true peak");
10715
- }
10716
- return value;
10717
- }
10718
10774
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
10719
- const result = await runFfmpeg4(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) {
10720
10787
  if (result.success) return { success: true };
10721
10788
  return {
10722
10789
  success: false,
10723
- error: `[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`,
10790
+ error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
10724
10791
  failureReason: result.failureReason
10725
10792
  };
10726
10793
  }
@@ -10904,7 +10971,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
10904
10971
  const reason = UNSUPPORTED_REASON2[route];
10905
10972
  if (reason === null) return;
10906
10973
  throw new Error(
10907
- `[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.`
10908
10975
  );
10909
10976
  }
10910
10977
 
@@ -11121,7 +11188,7 @@ function getNextRetryWorkerCount(currentWorkers) {
11121
11188
  return Math.max(1, Math.floor(currentWorkers / 2));
11122
11189
  }
11123
11190
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11124
- if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
11191
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11125
11192
  return join26(dirname10(outputPath), `work-${jobId}-`);
11126
11193
  }
11127
11194
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
@@ -11615,6 +11682,9 @@ function resolveDeShortBand(args) {
11615
11682
  function shouldPreferSingleWorkerDrawElement(args) {
11616
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;
11617
11684
  }
11685
+ function isProbeDeGated(session) {
11686
+ return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
11687
+ }
11618
11688
  function resolveInversionRetryPlan(args) {
11619
11689
  if (args.deWorkerInversion !== "inverted") return null;
11620
11690
  const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
@@ -11696,7 +11766,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
11696
11766
  }
11697
11767
  function resolveParallelCaptureMode(args) {
11698
11768
  const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
11699
- 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";
11700
11771
  }
11701
11772
  function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
11702
11773
  const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
@@ -12304,6 +12375,7 @@ async function executeRenderPipeline(input) {
12304
12375
  );
12305
12376
  const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
12306
12377
  const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
12378
+ if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
12307
12379
  const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
12308
12380
  const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
12309
12381
  const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
@@ -12344,7 +12416,7 @@ async function executeRenderPipeline(input) {
12344
12416
  singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
12345
12417
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12346
12418
  supersampling: deviceScaleFactor !== 1,
12347
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12419
+ probeDeGated: isProbeDeGated(probeSession),
12348
12420
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
12349
12421
  process.env.HF_DE_PARALLEL_STREAM === "true"
12350
12422
  };
@@ -12353,14 +12425,14 @@ async function executeRenderPipeline(input) {
12353
12425
  ...deInversionArgs,
12354
12426
  minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
12355
12427
  });
12356
- const deShortBand = resolveDeShortBand({
12428
+ let deShortBand = resolveDeShortBand({
12357
12429
  invertAtBaseFloor,
12358
12430
  invertAtBandFloor,
12359
12431
  bandEnabled: deShortBandEnabled,
12360
12432
  bandOpen: deShortBandOpen,
12361
12433
  elementCountSource: compositionElementCountSource
12362
12434
  });
12363
- const deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12435
+ let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12364
12436
  const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
12365
12437
  const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
12366
12438
  const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
@@ -12369,7 +12441,7 @@ async function executeRenderPipeline(input) {
12369
12441
  const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
12370
12442
  const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
12371
12443
  const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
12372
- const deParallelRouterEligible = shouldPreferParallelDrawElement({
12444
+ let deParallelRouterEligible = shouldPreferParallelDrawElement({
12373
12445
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12374
12446
  requestedWorkers: job.config.workers,
12375
12447
  useDrawElement: cfg.useDrawElement,
@@ -12380,7 +12452,7 @@ async function executeRenderPipeline(input) {
12380
12452
  minFrames: deParallelMinFrames,
12381
12453
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12382
12454
  supersampling: deviceScaleFactor !== 1,
12383
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12455
+ probeDeGated: isProbeDeGated(probeSession),
12384
12456
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12385
12457
  routerEnabled: deParallelRouterEnabled,
12386
12458
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12396,6 +12468,35 @@ async function executeRenderPipeline(input) {
12396
12468
  totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
12397
12469
  minMemoryMb: deParallelMinMemoryMb
12398
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;
12399
12500
  let workerCount;
12400
12501
  const captureStageObservationData = (extra = {}) => ({
12401
12502
  stagePhase: "capturing",
@@ -12583,7 +12684,7 @@ async function executeRenderPipeline(input) {
12583
12684
  }
12584
12685
  const parallelCaptureMode = resolveParallelCaptureMode({
12585
12686
  platform: process.platform,
12586
- headlessShell: resolveHeadlessShellPath(cfg) !== null,
12687
+ headlessShellPath: resolveHeadlessShellPath(cfg),
12587
12688
  forceScreenshot: captureForceScreenshot,
12588
12689
  deviceScaleFactor: captureOptions.deviceScaleFactor
12589
12690
  });
@@ -13355,7 +13456,9 @@ async function executeRenderPipeline(input) {
13355
13456
  outputFormat,
13356
13457
  durationSeconds: job.duration,
13357
13458
  fps: fpsToNumber9(job.config.fps),
13358
- expectedFrames: captureTotalFrames
13459
+ expectedFrames: captureTotalFrames,
13460
+ width: outputWidth,
13461
+ height: outputHeight
13359
13462
  })
13360
13463
  );
13361
13464
  const totalElapsed = Date.now() - pipelineStart;
@@ -13391,6 +13494,7 @@ async function executeRenderPipeline(input) {
13391
13494
  drawElement: {
13392
13495
  compileGate: deCompileGate,
13393
13496
  clampReason: deClampReason,
13497
+ probeGateReason: deProbeGateReason,
13394
13498
  workerInversion: deWorkerInversion,
13395
13499
  preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
13396
13500
  compositionElementCount,
@@ -13792,9 +13896,9 @@ import {
13792
13896
  // src/services/distributed/planFrameCost.ts
13793
13897
  import {
13794
13898
  closeCaptureSession as closeCaptureSession9,
13795
- createCaptureSession as createCaptureSession8,
13899
+ createCaptureSession as createCaptureSession9,
13796
13900
  createFrameLookupTable as createFrameLookupTable3,
13797
- initializeSession as initializeSession8
13901
+ initializeSession as initializeSession9
13798
13902
  } from "@hyperframes/engine";
13799
13903
 
13800
13904
  // src/services/distributed/chunkFrames.ts
@@ -13853,7 +13957,7 @@ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
13853
13957
  var FIELD_DELIMITER = Buffer.from([0]);
13854
13958
 
13855
13959
  // src/services/distributed/chunkAudio.ts
13856
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure2, runFfmpeg as runFfmpeg5 } from "@hyperframes/engine";
13960
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
13857
13961
 
13858
13962
  // src/services/distributed/mp4Stitch.ts
13859
13963
  var SCAN_BYTES = 64 * 1024;
@@ -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[]>;