@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.
package/dist/index.js CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  extractMediaMetadata as extractMediaMetadata4,
29
29
  resolveConfig as resolveConfig2,
30
30
  closeCaptureSession as closeCaptureSession8,
31
+ createCaptureSession as createCaptureSession8,
32
+ initializeSession as initializeSession8,
31
33
  createVideoFrameInjector,
32
34
  getEncoderPreset as getEncoderPreset3,
33
35
  distributeFrames as distributeFrames2,
@@ -1402,7 +1404,7 @@ function pidScope() {
1402
1404
  return {
1403
1405
  host: hostname(),
1404
1406
  // Without /proc (macOS, Windows) the boot time, to the minute, stands in for the boot id.
1405
- boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? String(Math.round(Date.now() / 6e4 - uptime() / 60)),
1407
+ boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? attempt(() => String(Math.round(Date.now() / 6e4 - uptime() / 60))),
1406
1408
  pidns: attempt(() => readlinkSync("/proc/self/ns/pid"))
1407
1409
  };
1408
1410
  }
@@ -1420,9 +1422,14 @@ function alive(pid) {
1420
1422
  return error.code === "EPERM";
1421
1423
  }
1422
1424
  }
1423
- function inThisPidScope(owner) {
1425
+ function knownPidScope() {
1424
1426
  const scope = pidScope();
1425
- return owner.host === scope.host && owner.boot === scope.boot && owner.pidns === scope.pidns;
1427
+ const unknown = scope.boot === void 0 || process.platform === "linux" && scope.pidns === void 0;
1428
+ return unknown ? void 0 : scope;
1429
+ }
1430
+ function inThisPidScope(owner) {
1431
+ const scope = knownPidScope();
1432
+ return scope !== void 0 && owner.host === scope.host && owner.boot === scope.boot && owner.pidns === scope.pidns;
1426
1433
  }
1427
1434
  function ownerState(dir) {
1428
1435
  let owner;
@@ -1487,7 +1494,9 @@ async function defaultArtifactDurationProbe(path) {
1487
1494
  const meta = await extractMediaMetadata(path);
1488
1495
  return {
1489
1496
  durationSeconds: meta.durationSeconds,
1490
- frames: meta.frames
1497
+ frames: meta.frames,
1498
+ width: meta.width,
1499
+ height: meta.height
1491
1500
  };
1492
1501
  }
1493
1502
  function buildArtifactExpectation(input) {
@@ -1496,7 +1505,9 @@ function buildArtifactExpectation(input) {
1496
1505
  const expectation = {
1497
1506
  expectedDurationSeconds: input.durationSeconds,
1498
1507
  fps: input.fps,
1499
- expectedFrames: input.expectedFrames
1508
+ expectedFrames: input.expectedFrames,
1509
+ expectedWidth: input.width,
1510
+ expectedHeight: input.height
1500
1511
  };
1501
1512
  if (input.outputFormat !== "hls") return expectation;
1502
1513
  return {
@@ -1564,6 +1575,16 @@ function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPa
1564
1575
  `Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
1565
1576
  );
1566
1577
  }
1578
+ function assertSizeMatches(expected, probed, stagingPath) {
1579
+ const { expectedWidth: width, expectedHeight: height } = expected;
1580
+ if (width === void 0 || height === void 0) return;
1581
+ if (probed.width === void 0 || probed.height === void 0) return;
1582
+ const fits = (want, got) => got === want || got === want + want % 2;
1583
+ if (fits(width, probed.width) && fits(height, probed.height)) return;
1584
+ throw new Error(
1585
+ `Render artifact has the wrong size: expected ${width}x${height}, probed ${probed.width}x${probed.height}. Artifact: ${stagingPath}`
1586
+ );
1587
+ }
1567
1588
  var ArtifactTransaction = class {
1568
1589
  constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
1569
1590
  this.kind = kind;
@@ -1634,6 +1655,7 @@ var ArtifactTransaction = class {
1634
1655
  if (expected.expectedFrames !== void 0) {
1635
1656
  assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
1636
1657
  }
1658
+ assertSizeMatches(expected, probed, probePath);
1637
1659
  }
1638
1660
  async runDurationProbe(probePath) {
1639
1661
  try {
@@ -3182,7 +3204,9 @@ function aggregateDrawElement(perfs, de) {
3182
3204
  if (perfs.length === 0) return void 0;
3183
3205
  const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
3184
3206
  const gateReasons = [
3185
- ...new Set(perfs.map((p) => p.deGateReason).filter((r) => !!r))
3207
+ ...new Set(
3208
+ [de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
3209
+ )
3186
3210
  ].sort();
3187
3211
  const gpuRenderers = [
3188
3212
  ...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
@@ -8682,6 +8706,7 @@ async function runAudioStage(input) {
8682
8706
  audios,
8683
8707
  masterFxChain,
8684
8708
  urlByAbsentLocalPath,
8709
+ keepEncoderInputAt,
8685
8710
  abortSignal,
8686
8711
  assertNotAborted,
8687
8712
  log
@@ -8701,7 +8726,13 @@ async function runAudioStage(input) {
8701
8726
  audioOutputPath,
8702
8727
  duration,
8703
8728
  abortSignal,
8704
- { ffmpegProcessTimeout, audioGain, masterFxChain, urlByAbsentLocalPath },
8729
+ {
8730
+ ffmpegProcessTimeout,
8731
+ audioGain,
8732
+ masterFxChain,
8733
+ urlByAbsentLocalPath,
8734
+ keepEncoderInputAt
8735
+ },
8705
8736
  compiledDir
8706
8737
  );
8707
8738
  } catch (err) {
@@ -8815,13 +8846,29 @@ function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, free
8815
8846
  }
8816
8847
  return { available: false, estimatedBytes, freeBytes };
8817
8848
  }
8818
- function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis) {
8849
+ function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
8850
+ const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
8851
+ if (workerCount > 1 && oneWorker.enabled) {
8852
+ 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.`;
8853
+ }
8854
+ switch (oneWorker.reason) {
8855
+ case "format_excluded":
8856
+ return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
8857
+ case "disabled_by_config":
8858
+ return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
8859
+ case "duration_cap":
8860
+ 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.";
8861
+ default:
8862
+ return "Free up disk space.";
8863
+ }
8864
+ }
8865
+ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
8819
8866
  const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
8820
8867
  return new Error(
8821
- `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.`
8868
+ `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}`
8822
8869
  );
8823
8870
  }
8824
- function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes) {
8871
+ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy, freeDiskBytes) {
8825
8872
  const headroom = inspectDiskCaptureHeadroom(
8826
8873
  framesDir,
8827
8874
  totalFrames,
@@ -8833,7 +8880,8 @@ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeD
8833
8880
  headroom.estimatedBytes,
8834
8881
  headroom.freeBytes,
8835
8882
  framesDir,
8836
- "estimate"
8883
+ "estimate",
8884
+ remedy
8837
8885
  );
8838
8886
  }
8839
8887
  function measureCaptureFrameBytes(workDir, framesDir) {
@@ -8853,7 +8901,7 @@ function measureCaptureFrameBytes(workDir, framesDir) {
8853
8901
  return total;
8854
8902
  }
8855
8903
  function createDiskCaptureProjection(input) {
8856
- const { framesDir, totalFrames, measureBytes, freeDiskBytes = freeDiskBytesAt } = input;
8904
+ const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
8857
8905
  let checked = false;
8858
8906
  return (capturedFrames) => {
8859
8907
  if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
@@ -8865,7 +8913,7 @@ function createDiskCaptureProjection(input) {
8865
8913
  if (remaining <= 0 || freeBytes === null) return;
8866
8914
  const needBytes = Math.ceil(measured / capturedFrames * remaining);
8867
8915
  if (needBytes > freeBytes * 0.9) {
8868
- throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured");
8916
+ throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
8869
8917
  }
8870
8918
  };
8871
8919
  }
@@ -8907,11 +8955,13 @@ async function runCaptureStage(input) {
8907
8955
  }
8908
8956
  }
8909
8957
  const captureOptions = buildCaptureOptions();
8910
- assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions);
8958
+ const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
8959
+ assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy);
8911
8960
  const checkDiskProjection = createDiskCaptureProjection({
8912
8961
  framesDir,
8913
8962
  totalFrames,
8914
- measureBytes: () => measureCaptureFrameBytes(workDir, framesDir)
8963
+ measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
8964
+ remedy
8915
8965
  });
8916
8966
  if (workerCount > 1) {
8917
8967
  const attempts = await executeDiskCaptureWithAdaptiveRetry({
@@ -10122,8 +10172,10 @@ function fpsToFfmpegArg2(fps) {
10122
10172
  }
10123
10173
  var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
10124
10174
  var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
10175
+ var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
10125
10176
  function framesInput(input, fpsArg) {
10126
10177
  return [
10178
+ ...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
10127
10179
  "-framerate",
10128
10180
  fpsArg,
10129
10181
  ...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
@@ -10374,7 +10426,8 @@ import { dirname as dirname9, join as join25 } from "node:path";
10374
10426
  import {
10375
10427
  DEFAULT_CONFIG,
10376
10428
  describeFfmpegFailure as describeFfmpegFailure2,
10377
- extractAudioMetadata as extractAudioMetadata2,
10429
+ encodeAac,
10430
+ extractAudioMetadata as extractAudioMetadata3,
10378
10431
  getFfprobeBinary,
10379
10432
  ManagedChildProcess,
10380
10433
  runFfmpeg as runFfmpeg5,
@@ -10383,7 +10436,12 @@ import {
10383
10436
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
10384
10437
 
10385
10438
  // src/services/render/audioTruePeak.ts
10386
- import { aacSegmentCount, describeFfmpegFailure, runFfmpeg as runFfmpeg4 } from "@hyperframes/engine";
10439
+ import {
10440
+ aacSegmentCount,
10441
+ describeFfmpegFailure,
10442
+ extractAudioMetadata as extractAudioMetadata2,
10443
+ runFfmpeg as runFfmpeg4
10444
+ } from "@hyperframes/engine";
10387
10445
  var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10388
10446
  function checkedDbfs(value) {
10389
10447
  if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
@@ -10421,12 +10479,13 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
10421
10479
  };
10422
10480
  }
10423
10481
  async function measureTruePeakDbfs(audioPath, options) {
10424
- const { timeoutMs, signal, segments } = options;
10482
+ const { timeoutMs, signal } = options;
10425
10483
  const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
10484
+ const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
10426
10485
  const block = 4800;
10427
- const count = segments?.sampleRate === 48e3 ? aacSegmentCount(segments.durationSeconds) : 1;
10428
- if (!segments || count === 1) return wholeFile();
10429
- const cut = (k) => k * Math.floor(segments.durationSeconds * 10 / count) * block;
10486
+ const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
10487
+ if (count === 1) return wholeFile();
10488
+ const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
10430
10489
  const peers = new AbortController();
10431
10490
  const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
10432
10491
  const scans = await Promise.all(
@@ -10455,24 +10514,8 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10455
10514
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10456
10515
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
10457
10516
  var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
10458
- function buildAacTruePeakCorrectionArgs(inputPath, outputPath, targetDurationSeconds, attenuationDb) {
10459
- return [
10460
- "-i",
10461
- inputPath,
10462
- "-map",
10463
- "0:a:0",
10464
- "-vn",
10465
- "-af",
10466
- `volume=${attenuationDb.toFixed(3)}dB`,
10467
- "-t",
10468
- formatSeconds(targetDurationSeconds),
10469
- "-c:a",
10470
- "aac",
10471
- "-b:a",
10472
- "192k",
10473
- "-y",
10474
- outputPath
10475
- ];
10517
+ function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
10518
+ return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10476
10519
  }
10477
10520
  function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
10478
10521
  const delta = targetDurationSeconds - sourceDurationSeconds;
@@ -10487,53 +10530,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
10487
10530
  if (delta > 0) {
10488
10531
  return {
10489
10532
  operation: "pad",
10490
- steps: [
10491
- {
10492
- kind: "normalize",
10493
- args: [
10494
- "-i",
10495
- audioPath,
10496
- "-af",
10497
- `apad,atrim=0:${targetSec}`,
10498
- "-t",
10499
- targetSec,
10500
- "-c:a",
10501
- "aac",
10502
- "-b:a",
10503
- "192k",
10504
- "-y",
10505
- outputPath
10506
- ]
10507
- }
10508
- ],
10533
+ steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
10509
10534
  cleanupPaths: []
10510
10535
  };
10511
10536
  }
10512
10537
  return {
10513
10538
  operation: "trim",
10514
- steps: [
10515
- {
10516
- kind: "trim",
10517
- args: [
10518
- "-i",
10519
- audioPath,
10520
- "-af",
10521
- `atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
10522
- // `atrim` limits decoded samples but does not cap muxer timestamps
10523
- // introduced by encoder delay/flush. The output duration contract
10524
- // is enforced at the container boundary as well; without `-t`, a
10525
- // long primed source can retain its original tail after re-encode.
10526
- "-t",
10527
- targetSec,
10528
- "-c:a",
10529
- "aac",
10530
- "-b:a",
10531
- "192k",
10532
- "-y",
10533
- outputPath
10534
- ]
10535
- }
10536
- ],
10539
+ steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
10537
10540
  cleanupPaths: []
10538
10541
  };
10539
10542
  }
@@ -10550,7 +10553,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10550
10553
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10551
10554
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10552
10555
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10553
- const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10556
+ const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
10557
+ const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10554
10558
  const [videoResult, audioResult] = await Promise.allSettled([
10555
10559
  probeVideo(input.videoPath),
10556
10560
  probeAudio(input.audioPath, input.signal)
@@ -10597,7 +10601,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10597
10601
  );
10598
10602
  try {
10599
10603
  for (const step of plan2.steps) {
10600
- const ffmpegResult = await runner(step.args);
10604
+ const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
10601
10605
  if (!ffmpegResult.success) {
10602
10606
  return {
10603
10607
  success: false,
@@ -10629,7 +10633,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10629
10633
  targetDurationSeconds,
10630
10634
  signal: input.signal,
10631
10635
  probeTruePeak,
10632
- runner
10636
+ encode
10633
10637
  });
10634
10638
  if (!correction.success) {
10635
10639
  return {
@@ -10659,12 +10663,8 @@ async function constrainAacTruePeak(input) {
10659
10663
  audioPath: input.audioPath,
10660
10664
  targetDurationSeconds: input.targetDurationSeconds,
10661
10665
  signal: input.signal,
10662
- probeTruePeak: async (audioPath, signal) => {
10663
- const { sampleRate } = await extractAudioMetadata2(audioPath, { signal });
10664
- const segments = { durationSeconds: input.targetDurationSeconds, sampleRate };
10665
- return measureTruePeakDbfs(audioPath, { timeoutMs, signal, segments });
10666
- },
10667
- runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
10666
+ probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
10667
+ encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
10668
10668
  });
10669
10669
  }
10670
10670
  async function enforceAacTruePeak(input) {
@@ -10691,13 +10691,11 @@ async function enforceAacTruePeak(input) {
10691
10691
  }
10692
10692
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
10693
10693
  attenuationDb += correctionTargetDbfs - truePeakDbfs;
10694
- const result = await input.runner(
10695
- buildAacTruePeakCorrectionArgs(
10696
- input.audioPath,
10697
- correctedPath,
10698
- input.targetDurationSeconds,
10699
- attenuationDb
10700
- )
10694
+ const result = await input.encode(
10695
+ input.audioPath,
10696
+ correctedPath,
10697
+ input.targetDurationSeconds,
10698
+ aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
10701
10699
  );
10702
10700
  if (!result.success) {
10703
10701
  return {
@@ -10792,7 +10790,7 @@ function parseFrameRate(rate) {
10792
10790
  return { fpsNum, fpsDen };
10793
10791
  }
10794
10792
  async function defaultProbeAudioInfo(audioPath, signal) {
10795
- const metadata = await extractAudioMetadata2(audioPath, { signal });
10793
+ const metadata = await extractAudioMetadata3(audioPath, { signal });
10796
10794
  return {
10797
10795
  durationSeconds: metadata.durationSeconds,
10798
10796
  sampleRate: metadata.sampleRate,
@@ -10801,7 +10799,18 @@ async function defaultProbeAudioInfo(audioPath, signal) {
10801
10799
  };
10802
10800
  }
10803
10801
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
10804
- const result = await runFfmpeg5(args, { signal, timeout: timeoutMs });
10802
+ return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
10803
+ }
10804
+ function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
10805
+ return async (inputPath, outputPath, durationSeconds, filter) => {
10806
+ const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
10807
+ return toStepResult(
10808
+ await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
10809
+ timeoutMs
10810
+ );
10811
+ };
10812
+ }
10813
+ function toStepResult(result, timeoutMs) {
10805
10814
  if (result.success) return { success: true };
10806
10815
  return {
10807
10816
  success: false,
@@ -10989,7 +10998,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
10989
10998
  const reason = UNSUPPORTED_REASON2[route];
10990
10999
  if (reason === null) return;
10991
11000
  throw new Error(
10992
- `[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.`
11001
+ `[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.`
10993
11002
  );
10994
11003
  }
10995
11004
 
@@ -11206,7 +11215,7 @@ function getNextRetryWorkerCount(currentWorkers) {
11206
11215
  return Math.max(1, Math.floor(currentWorkers / 2));
11207
11216
  }
11208
11217
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11209
- if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
11218
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11210
11219
  return join26(dirname10(outputPath), `work-${jobId}-`);
11211
11220
  }
11212
11221
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
@@ -11700,6 +11709,9 @@ function resolveDeShortBand(args) {
11700
11709
  function shouldPreferSingleWorkerDrawElement(args) {
11701
11710
  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;
11702
11711
  }
11712
+ function isProbeDeGated(session) {
11713
+ return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
11714
+ }
11703
11715
  function resolveInversionRetryPlan(args) {
11704
11716
  if (args.deWorkerInversion !== "inverted") return null;
11705
11717
  const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
@@ -11781,7 +11793,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
11781
11793
  }
11782
11794
  function resolveParallelCaptureMode(args) {
11783
11795
  const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
11784
- return args.headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
11796
+ const headlessShell = args.headlessShellPath !== void 0;
11797
+ return headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
11785
11798
  }
11786
11799
  function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
11787
11800
  const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
@@ -12389,6 +12402,7 @@ async function executeRenderPipeline(input) {
12389
12402
  );
12390
12403
  const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
12391
12404
  const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
12405
+ if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
12392
12406
  const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
12393
12407
  const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
12394
12408
  const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
@@ -12429,7 +12443,7 @@ async function executeRenderPipeline(input) {
12429
12443
  singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
12430
12444
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12431
12445
  supersampling: deviceScaleFactor !== 1,
12432
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12446
+ probeDeGated: isProbeDeGated(probeSession),
12433
12447
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
12434
12448
  process.env.HF_DE_PARALLEL_STREAM === "true"
12435
12449
  };
@@ -12438,14 +12452,14 @@ async function executeRenderPipeline(input) {
12438
12452
  ...deInversionArgs,
12439
12453
  minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
12440
12454
  });
12441
- const deShortBand = resolveDeShortBand({
12455
+ let deShortBand = resolveDeShortBand({
12442
12456
  invertAtBaseFloor,
12443
12457
  invertAtBandFloor,
12444
12458
  bandEnabled: deShortBandEnabled,
12445
12459
  bandOpen: deShortBandOpen,
12446
12460
  elementCountSource: compositionElementCountSource
12447
12461
  });
12448
- const deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12462
+ let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12449
12463
  const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
12450
12464
  const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
12451
12465
  const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
@@ -12454,7 +12468,7 @@ async function executeRenderPipeline(input) {
12454
12468
  const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
12455
12469
  const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
12456
12470
  const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
12457
- const deParallelRouterEligible = shouldPreferParallelDrawElement({
12471
+ let deParallelRouterEligible = shouldPreferParallelDrawElement({
12458
12472
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12459
12473
  requestedWorkers: job.config.workers,
12460
12474
  useDrawElement: cfg.useDrawElement,
@@ -12465,7 +12479,7 @@ async function executeRenderPipeline(input) {
12465
12479
  minFrames: deParallelMinFrames,
12466
12480
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12467
12481
  supersampling: deviceScaleFactor !== 1,
12468
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12482
+ probeDeGated: isProbeDeGated(probeSession),
12469
12483
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12470
12484
  routerEnabled: deParallelRouterEnabled,
12471
12485
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12481,6 +12495,35 @@ async function executeRenderPipeline(input) {
12481
12495
  totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
12482
12496
  minMemoryMb: deParallelMinMemoryMb
12483
12497
  });
12498
+ if (probeSession === null && (deInversionEligible || deParallelRouterEligible)) {
12499
+ try {
12500
+ probeSession = await createCaptureSession8(
12501
+ activeFileServer.url,
12502
+ framesDir,
12503
+ buildCaptureOptions(),
12504
+ null,
12505
+ { ...cfg, forceScreenshot: captureForceScreenshot }
12506
+ );
12507
+ await initializeSession8(probeSession);
12508
+ } catch (error) {
12509
+ assertNotAborted();
12510
+ if (probeSession) {
12511
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
12512
+ await closeCaptureSession8(probeSession).catch(() => {
12513
+ });
12514
+ probeSession = null;
12515
+ }
12516
+ log.warn("[Render] Fast capture gate check failed; routing without it.", {
12517
+ error: error instanceof Error ? error.message : String(error)
12518
+ });
12519
+ }
12520
+ if (isProbeDeGated(probeSession)) {
12521
+ deInversionEligible = false;
12522
+ deParallelRouterEligible = false;
12523
+ deShortBand = void 0;
12524
+ }
12525
+ }
12526
+ const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
12484
12527
  let workerCount;
12485
12528
  const captureStageObservationData = (extra = {}) => ({
12486
12529
  stagePhase: "capturing",
@@ -12668,7 +12711,7 @@ async function executeRenderPipeline(input) {
12668
12711
  }
12669
12712
  const parallelCaptureMode = resolveParallelCaptureMode({
12670
12713
  platform: process.platform,
12671
- headlessShell: resolveHeadlessShellPath(cfg) !== null,
12714
+ headlessShellPath: resolveHeadlessShellPath(cfg),
12672
12715
  forceScreenshot: captureForceScreenshot,
12673
12716
  deviceScaleFactor: captureOptions.deviceScaleFactor
12674
12717
  });
@@ -13440,7 +13483,9 @@ async function executeRenderPipeline(input) {
13440
13483
  outputFormat,
13441
13484
  durationSeconds: job.duration,
13442
13485
  fps: fpsToNumber9(job.config.fps),
13443
- expectedFrames: captureTotalFrames
13486
+ expectedFrames: captureTotalFrames,
13487
+ width: outputWidth,
13488
+ height: outputHeight
13444
13489
  })
13445
13490
  );
13446
13491
  const totalElapsed = Date.now() - pipelineStart;
@@ -13476,6 +13521,7 @@ async function executeRenderPipeline(input) {
13476
13521
  drawElement: {
13477
13522
  compileGate: deCompileGate,
13478
13523
  clampReason: deClampReason,
13524
+ probeGateReason: deProbeGateReason,
13479
13525
  workerInversion: deWorkerInversion,
13480
13526
  preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
13481
13527
  compositionElementCount,
@@ -13673,9 +13719,9 @@ import {
13673
13719
  import { join as join29 } from "node:path";
13674
13720
  import {
13675
13721
  closeCaptureSession as closeCaptureSession9,
13676
- createCaptureSession as createCaptureSession8,
13722
+ createCaptureSession as createCaptureSession9,
13677
13723
  createFrameLookupTable as createFrameLookupTable3,
13678
- initializeSession as initializeSession8
13724
+ initializeSession as initializeSession9
13679
13725
  } from "@hyperframes/engine";
13680
13726
 
13681
13727
  // src/services/distributed/chunkFrames.ts
@@ -14073,6 +14119,29 @@ function buildChunkCaptureOptions(input) {
14073
14119
  requiresWebGpu: compositionRequiresWebGpu3(input.html)
14074
14120
  };
14075
14121
  }
14122
+ function shownDeferredFrameIndexes(planVideos, frames, fps) {
14123
+ const shown = /* @__PURE__ */ new Map();
14124
+ const deferred = planVideos.extracted.filter((video) => video.deferredRange);
14125
+ if (deferred.length === 0) return shown;
14126
+ const table = createFrameLookupTable2(
14127
+ planVideos.videos,
14128
+ deferred.map((video) => ({
14129
+ ...video,
14130
+ outputDir: "",
14131
+ framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
14132
+ })),
14133
+ resolveRenderFpsConfig(fps).value
14134
+ );
14135
+ for (const frame of frames) {
14136
+ const globalTime = frame * fps.den / fps.num;
14137
+ for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
14138
+ const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
14139
+ indexes.add(payload.frameIndex);
14140
+ shown.set(videoId, indexes);
14141
+ }
14142
+ }
14143
+ return shown;
14144
+ }
14076
14145
  async function extractDeferredVideoFramesForChunk(input) {
14077
14146
  const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
14078
14147
  const remoteSources = input.remoteSources ?? {};
@@ -14089,24 +14158,7 @@ async function extractDeferredVideoFramesForChunk(input) {
14089
14158
  });
14090
14159
  }
14091
14160
  if (deferred.length === 0) return [];
14092
- const table = createFrameLookupTable2(
14093
- input.planVideos.videos,
14094
- deferred.map((video) => ({
14095
- ...video,
14096
- outputDir: input.outputRoot,
14097
- framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
14098
- })),
14099
- resolveRenderFpsConfig(input.fps).value
14100
- );
14101
- const shown = /* @__PURE__ */ new Map();
14102
- for (const frame of input.frames) {
14103
- const globalTime = frame * input.fps.den / input.fps.num;
14104
- for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
14105
- const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
14106
- indexes.add(payload.frameIndex);
14107
- shown.set(videoId, indexes);
14108
- }
14109
- }
14161
+ const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
14110
14162
  return Promise.all(
14111
14163
  deferred.map(async (video) => {
14112
14164
  const outputDir = join28(input.outputRoot, video.videoId);
@@ -14132,7 +14184,8 @@ async function extractDeferredVideoFramesForChunk(input) {
14132
14184
  fps: input.fps,
14133
14185
  outputDir: input.outputRoot,
14134
14186
  format: range.format,
14135
- frameRanges: contiguousRuns(indexes)
14187
+ frameRanges: contiguousRuns(indexes),
14188
+ shownBox: input.shownBoxes?.get(video.videoId)
14136
14189
  },
14137
14190
  void 0,
14138
14191
  input.cfg
@@ -14179,7 +14232,7 @@ async function measurePlanSecondsPerFrame(input) {
14179
14232
  });
14180
14233
  let session = null;
14181
14234
  try {
14182
- session = await createCaptureSession8(
14235
+ session = await createCaptureSession9(
14183
14236
  fileServer.url,
14184
14237
  join29(workDir, "frame-cost"),
14185
14238
  buildChunkCaptureOptions({
@@ -14191,7 +14244,7 @@ async function measurePlanSecondsPerFrame(input) {
14191
14244
  createChunkVideoFrameInjectorFactory(lookup)(),
14192
14245
  createCaptureCalibrationConfig(input.cfg)
14193
14246
  );
14194
- await initializeSession8(session);
14247
+ await initializeSession9(session);
14195
14248
  const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
14196
14249
  return (estimate.p95Ms ?? 0) / 1e3;
14197
14250
  } finally {
@@ -14785,6 +14838,20 @@ function withDuration(buf, target, duration) {
14785
14838
  function timescaleOf(buf, header) {
14786
14839
  return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
14787
14840
  }
14841
+ var TRACK_ID_AT = { tkhd: [12, 20], mvhd: [96, 108] };
14842
+ function idOf(buf, target) {
14843
+ return buf.readUInt32BE(
14844
+ target.bodyStart + TRACK_ID_AT[target.type][buf[target.bodyStart] === 1 ? 1 : 0]
14845
+ );
14846
+ }
14847
+ function withId(boxBytes, type, id) {
14848
+ const header = boxAt(boxBytes, 0, 0, boxBytes.length);
14849
+ boxBytes.writeUInt32BE(
14850
+ id,
14851
+ header.bodyStart + TRACK_ID_AT[type][boxBytes[header.bodyStart] === 1 ? 1 : 0]
14852
+ );
14853
+ return boxBytes;
14854
+ }
14788
14855
  function buildStbl(stsd, track) {
14789
14856
  const stts = runs(track.durations);
14790
14857
  const stsc = [];
@@ -14826,24 +14893,48 @@ function planTracksOf(tracks, plan2, index) {
14826
14893
  }
14827
14894
  return matched;
14828
14895
  }
14829
- function buildStitchedHeader(layouts) {
14830
- const plan2 = readTracks(layouts[0].moov);
14831
- const chunks = layouts.map((layout, i) => planTracksOf(readTracks(layout.moov), plan2, i));
14896
+ function sourceTracks(layouts, audio) {
14897
+ const chunkPlan = readTracks(layouts[0].moov);
14898
+ const audioTracks = audio ? readTracks(audio.moov) : [];
14899
+ if (audio && (audioTracks.length !== 1 || audioTracks[0].handler !== "soun")) {
14900
+ throw new Error("[mp4Stitch] the separate audio file must hold one audio track");
14901
+ }
14902
+ const plan2 = [...chunkPlan, ...audioTracks];
14903
+ const chunks = layouts.map((layout, i) => {
14904
+ const tracks = readTracks(layout.moov);
14905
+ if (audio && tracks.some((track) => track.handler === "soun")) {
14906
+ throw new Error(`[mp4Stitch] chunk ${i} carries its own audio`);
14907
+ }
14908
+ return planTracksOf(tracks, plan2, i);
14909
+ });
14910
+ return {
14911
+ plan: plan2,
14912
+ chunkTrackCount: chunkPlan.length,
14913
+ sources: audio ? [[...chunkPlan.map(() => null), audioTracks[0]], ...chunks] : chunks
14914
+ };
14915
+ }
14916
+ function buildStitchedHeader(layouts, audio) {
14917
+ const { plan: plan2, chunkTrackCount, sources } = sourceTracks(layouts, audio);
14918
+ const sourceLayouts = audio ? [audio, ...layouts] : layouts;
14832
14919
  const payloadBases = [];
14833
14920
  let payloadBytes = 0;
14834
- for (const layout of layouts) {
14921
+ for (const layout of sourceLayouts) {
14835
14922
  payloadBases.push(payloadBytes);
14836
14923
  payloadBytes += layout.payloadEnd - layout.payloadStart;
14837
14924
  }
14838
14925
  const template = layouts[0].moov;
14839
14926
  const root = boxAt(template, 0, 0, template.length);
14840
- const movieTimescale = timescaleOf(template, child(template, root, "mvhd"));
14927
+ const mvhd = child(template, root, "mvhd");
14928
+ const movieTimescale = timescaleOf(template, mvhd);
14929
+ const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child(template, track.trak, "tkhd")));
14930
+ const audioTrackId = Math.max(...chunkTrackIds) + 1;
14841
14931
  const buildMoov = (dataStart2) => {
14842
14932
  const traks = plan2.map((track, t) => {
14843
- const parts = chunks.flatMap((tracks, chunk) => {
14933
+ const parts = sources.flatMap((tracks, chunk) => {
14844
14934
  const part = tracks[t];
14845
14935
  return part ? [{ ...part, chunk }] : [];
14846
14936
  });
14937
+ const moov = t < chunkTrackCount ? template : audio.moov;
14847
14938
  const merged = {
14848
14939
  durations: parts.flatMap((part) => part.durations),
14849
14940
  sizes: parts.flatMap((part) => part.sizes),
@@ -14852,9 +14943,9 @@ function buildStitchedHeader(layouts) {
14852
14943
  chunkOffsets: parts.flatMap(
14853
14944
  (part) => part.chunkOffsets.map((offset) => {
14854
14945
  const i = part.chunk;
14855
- const { payloadStart, payloadEnd } = layouts[i];
14946
+ const { payloadStart, payloadEnd } = sourceLayouts[i];
14856
14947
  if (offset < payloadStart || offset >= payloadEnd) {
14857
- throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
14948
+ throw new Error(`[mp4Stitch] source ${i} has samples outside its mdat`);
14858
14949
  }
14859
14950
  return offset - payloadStart + payloadBases[i] + dataStart2;
14860
14951
  })
@@ -14870,20 +14961,25 @@ function buildStitchedHeader(layouts) {
14870
14961
  elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
14871
14962
  elst.writeUInt32BE(65536, 20);
14872
14963
  const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
14873
- const trak = rebuild(template, track.trak, {
14874
- tkhd: (tkhd) => Buffer.concat([withDuration(template, tkhd, shown), edts]),
14964
+ const tkhdOf = (tkhd) => {
14965
+ const timed = withDuration(moov, tkhd, shown);
14966
+ return t < chunkTrackCount ? timed : withId(timed, "tkhd", audioTrackId);
14967
+ };
14968
+ const trak = rebuild(moov, track.trak, {
14969
+ tkhd: (tkhd) => Buffer.concat([tkhdOf(tkhd), edts]),
14875
14970
  edts: () => Buffer.alloc(0),
14876
- mdia: (mdia) => rebuild(template, mdia, {
14877
- mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
14878
- minf: (minf) => rebuild(template, minf, { stbl: () => buildStbl(track.stsd, merged) })
14971
+ mdia: (mdia) => rebuild(moov, mdia, {
14972
+ mdhd: (mdhd) => withDuration(moov, mdhd, mediaDuration),
14973
+ minf: (minf) => rebuild(moov, minf, { stbl: () => buildStbl(track.stsd, merged) })
14879
14974
  })
14880
14975
  });
14881
14976
  return { trak, shown };
14882
14977
  });
14883
14978
  let next = 0;
14979
+ const movieHeader = withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown)));
14884
14980
  return rebuild(template, root, {
14885
- mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
14886
- trak: () => traks[next++].trak
14981
+ mvhd: () => audio ? withId(movieHeader, "mvhd", audioTrackId + 1) : movieHeader,
14982
+ trak: () => ++next < chunkTrackCount ? traks[next - 1].trak : Buffer.concat(traks.slice(next - 1).map((t) => t.trak))
14887
14983
  });
14888
14984
  };
14889
14985
  const ftyp = layouts[0].ftyp;
@@ -14916,15 +15012,16 @@ async function copyPayload(path, layout, index, out) {
14916
15012
  );
14917
15013
  }
14918
15014
  }
14919
- async function writeStitchedMp4(chunkPaths, outputPath) {
14920
- const layouts = await Promise.all(chunkPaths.map((path) => readMp4Layout(fileRange(path))));
14921
- const header = buildStitchedHeader(layouts);
15015
+ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
15016
+ const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
15017
+ const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
15018
+ const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
14922
15019
  mkdirSync11(dirname12(outputPath), { recursive: true });
14923
15020
  const workDir = mkdtempSync4(`${outputPath}.stitch-`);
14924
15021
  const out = createWriteStream(join31(workDir, "out.mp4"));
14925
15022
  try {
14926
15023
  out.write(header);
14927
- for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
15024
+ for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
14928
15025
  out.end();
14929
15026
  await finished(out);
14930
15027
  renameSync5(join31(workDir, "out.mp4"), outputPath);
@@ -15495,7 +15592,8 @@ function buildLockedRenderConfig(input) {
15495
15592
  chunkCount: input.chunkCount,
15496
15593
  runtimeEnv: input.runtimeEnv,
15497
15594
  variables: config.variables,
15498
- ...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
15595
+ ...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
15596
+ ...input.separateAudio ? { separateAudio: true } : {}
15499
15597
  };
15500
15598
  }
15501
15599
  function carriesChunkAudio(config, options) {
@@ -15590,8 +15688,10 @@ function createVideoSourceShipper(input) {
15590
15688
  return result;
15591
15689
  };
15592
15690
  }
15593
- async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
15594
- const planDir = executionPlanDir;
15691
+ function plansSeparateAudio(config, options) {
15692
+ return options.deferAudio === true && config.format === "mp4" && config.cfr !== true;
15693
+ }
15694
+ async function compilePlanComposition(projectDir, config, planDir, options) {
15595
15695
  rejectUnsupportedDistributedFormat(config);
15596
15696
  validateNoGpuEncode({
15597
15697
  useGpu: false,
@@ -15727,6 +15827,100 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15727
15827
  });
15728
15828
  }
15729
15829
  }
15830
+ return {
15831
+ projectDir,
15832
+ log,
15833
+ sizeLimitBytes,
15834
+ abortSignal,
15835
+ assertNotAborted,
15836
+ cfg,
15837
+ job,
15838
+ workDir,
15839
+ compiledDir,
15840
+ finalCompiledDir,
15841
+ remoteFiles,
15842
+ urlByAbsentLocalPath,
15843
+ localPathByUrl,
15844
+ compileResult,
15845
+ compiled,
15846
+ composition,
15847
+ deviceScaleFactor,
15848
+ forceScreenshot,
15849
+ width,
15850
+ height,
15851
+ probeResult,
15852
+ totalFrames
15853
+ };
15854
+ }
15855
+ function mixPlanAudio(c, compiledDir, seconds, pcmPath) {
15856
+ return runAudioStage({
15857
+ projectDir: c.projectDir,
15858
+ workDir: c.workDir,
15859
+ compiledDir,
15860
+ urlByAbsentLocalPath: c.urlByAbsentLocalPath,
15861
+ duration: seconds,
15862
+ ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
15863
+ audioGain: c.cfg.audioGain,
15864
+ audios: c.composition.audios,
15865
+ masterFxChain: c.composition.masterFxChain,
15866
+ ...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
15867
+ abortSignal: c.abortSignal,
15868
+ assertNotAborted: c.assertNotAborted
15869
+ });
15870
+ }
15871
+ async function extractPlanVideos(c, deferRangeExtraction) {
15872
+ const extractResult = await runExtractVideosStage({
15873
+ projectDir: c.projectDir,
15874
+ compiledDir: c.compiledDir,
15875
+ job: c.job,
15876
+ cfg: c.cfg,
15877
+ composition: c.composition,
15878
+ abortSignal: c.abortSignal,
15879
+ assertNotAborted: c.assertNotAborted,
15880
+ materializeSymlinks: true,
15881
+ deferRangeExtraction,
15882
+ urlByAbsentLocalPath: c.urlByAbsentLocalPath
15883
+ });
15884
+ if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
15885
+ if (extractResult.extractionResult) {
15886
+ assertVideoExtractionSucceeded(extractResult.extractionResult);
15887
+ }
15888
+ return extractResult;
15889
+ }
15890
+ async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
15891
+ const peak = await constrainAacTruePeak({
15892
+ audioPath,
15893
+ ...existsSync21(pcmPath) ? { sourcePath: pcmPath } : {},
15894
+ targetDurationSeconds: seconds,
15895
+ signal: c.abortSignal,
15896
+ timeoutMs: c.cfg.ffmpegProcessTimeout
15897
+ }).finally(() => rmSync11(pcmPath, { force: true }));
15898
+ if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
15899
+ }
15900
+ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
15901
+ const planDir = executionPlanDir;
15902
+ const c = await compilePlanComposition(projectDir, config, planDir, options);
15903
+ const {
15904
+ log,
15905
+ sizeLimitBytes,
15906
+ cfg,
15907
+ job,
15908
+ workDir,
15909
+ compiledDir,
15910
+ finalCompiledDir,
15911
+ remoteFiles,
15912
+ urlByAbsentLocalPath,
15913
+ localPathByUrl,
15914
+ compileResult,
15915
+ compiled,
15916
+ composition,
15917
+ deviceScaleFactor,
15918
+ forceScreenshot,
15919
+ width,
15920
+ height,
15921
+ probeResult,
15922
+ totalFrames
15923
+ } = c;
15730
15924
  const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
15731
15925
  let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
15732
15926
  const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
@@ -15737,24 +15931,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15737
15931
  observedAt: "pre-extract",
15738
15932
  log
15739
15933
  });
15740
- stageStarted = Date.now();
15741
- const extractResult = await runExtractVideosStage({
15742
- projectDir,
15743
- compiledDir,
15744
- job,
15745
- cfg,
15746
- composition,
15747
- abortSignal,
15748
- assertNotAborted,
15749
- materializeSymlinks: true,
15750
- deferRangeExtraction: options.deferVideoExtraction,
15751
- urlByAbsentLocalPath
15752
- });
15934
+ let stageStarted = Date.now();
15935
+ const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
15753
15936
  logPlanStage(log, "extract", stageStarted);
15754
- if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
15755
- if (extractResult.extractionResult) {
15756
- assertVideoExtractionSucceeded(extractResult.extractionResult);
15757
- }
15758
15937
  const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
15759
15938
  const videoFramesDst = join34(planDir, "video-frames");
15760
15939
  if (existsSync21(videoFramesDst)) rmSync11(videoFramesDst, { recursive: true, force: true });
@@ -15810,23 +15989,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15810
15989
  "utf-8"
15811
15990
  );
15812
15991
  const digests = options.digests ?? /* @__PURE__ */ new Map();
15813
- const chunksCarryAudio2 = carriesChunkAudio(config, options);
15814
- const audioSeconds = chunksCarryAudio2 ? totalFrames / config.fps : probeResult.duration;
15992
+ const separateAudio = plansSeparateAudio(config, options);
15993
+ const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
15994
+ const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
15995
+ const planMixPcmPath = join34(workDir, "plan-mix.wav");
15815
15996
  stageStarted = Date.now();
15816
15997
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
15817
- runAudioStage({
15818
- projectDir,
15819
- workDir,
15820
- compiledDir: finalCompiledDir,
15821
- urlByAbsentLocalPath,
15822
- duration: audioSeconds,
15823
- ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
15824
- audioGain: cfg.audioGain,
15825
- audios: composition.audios,
15826
- masterFxChain: composition.masterFxChain,
15827
- abortSignal,
15828
- assertNotAborted
15829
- }).finally(() => logPlanStage(log, "audio", stageStarted)),
15998
+ separateAudio ? null : mixPlanAudio(
15999
+ c,
16000
+ finalCompiledDir,
16001
+ audioSeconds,
16002
+ chunksCarryAudio ? planMixPcmPath : void 0
16003
+ ).finally(() => logPlanStage(log, "audio", stageStarted)),
15830
16004
  (async () => {
15831
16005
  for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
15832
16006
  await prehashPlanFiles(dir, digests);
@@ -15876,22 +16050,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15876
16050
  );
15877
16051
  const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
15878
16052
  const audioResult = audioOutcome.value;
15879
- if (audioResult.audioError) {
16053
+ if (audioResult?.audioError) {
15880
16054
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
15881
16055
  }
15882
16056
  const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
15883
- if (audioResult.hasAudio && existsSync21(audioResult.audioOutputPath)) {
16057
+ if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
15884
16058
  renameSync6(audioResult.audioOutputPath, planAudioPath);
15885
16059
  }
15886
- if (chunksCarryAudio2 && existsSync21(planAudioPath)) {
16060
+ if (chunksCarryAudio && existsSync21(planAudioPath)) {
15887
16061
  stageStarted = Date.now();
15888
- const peak = await constrainAacTruePeak({
15889
- audioPath: planAudioPath,
15890
- targetDurationSeconds: audioSeconds,
15891
- signal: abortSignal,
15892
- timeoutMs: cfg.ffmpegProcessTimeout
15893
- });
15894
- if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
16062
+ await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
15895
16063
  logPlanStage(log, "audio-peak", stageStarted);
15896
16064
  if (options.sliceChunkAudio) {
15897
16065
  stageStarted = Date.now();
@@ -15917,7 +16085,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15917
16085
  effectiveChunkSize,
15918
16086
  chunkCount,
15919
16087
  runtimeEnv,
15920
- chunksCarryAudio: chunksCarryAudio2
16088
+ chunksCarryAudio,
16089
+ separateAudio
15921
16090
  });
15922
16091
  const compositionJson = {
15923
16092
  durationSeconds: job.duration ?? 0,
@@ -15962,7 +16131,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15962
16131
  fontSnapshotSha,
15963
16132
  durationSeconds: job.duration ?? 0,
15964
16133
  totalFrames,
15965
- hasAudio: audioResult.hasAudio,
16134
+ hasAudio: audioResult?.hasAudio ?? false,
15966
16135
  digests
15967
16136
  });
15968
16137
  logPlanStage(log, "freeze", stageStarted);
@@ -16478,8 +16647,8 @@ function renderRequestFromDistributedConfig(input) {
16478
16647
 
16479
16648
  // src/services/frameCapture.ts
16480
16649
  import {
16481
- createCaptureSession as createCaptureSession9,
16482
- initializeSession as initializeSession9,
16650
+ createCaptureSession as createCaptureSession10,
16651
+ initializeSession as initializeSession10,
16483
16652
  closeCaptureSession as closeCaptureSession10,
16484
16653
  captureFrame as captureFrame2,
16485
16654
  captureFrameToBuffer as captureFrameToBuffer4,
@@ -17980,7 +18149,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
17980
18149
  )
17981
18150
  ),
17982
18151
  chunksCarryAudio: options.chunksCarryAudio,
17983
- sliceChunkAudio: true
18152
+ sliceChunkAudio: true,
18153
+ deferAudio: options.deferAudio
17984
18154
  });
17985
18155
  return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
17986
18156
  } catch (error) {
@@ -18316,7 +18486,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18316
18486
  return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
18317
18487
  }
18318
18488
  if (chunksAreStitchedMp4(planDir, cfr)) {
18319
- await writeStitchedMp4(chunkPaths, outputPath);
18489
+ const separateAudio = stitchesSeparateAudio(planDir) && audioPath !== null;
18490
+ await writeStitchedMp4(chunkPaths, outputPath, separateAudio ? audioPath : void 0);
18320
18491
  return {
18321
18492
  outputPath,
18322
18493
  durationMs: Date.now() - start,
@@ -18514,16 +18685,20 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
18514
18685
  }
18515
18686
  }
18516
18687
  function chunksAreStitchedMp4(planDir, cfr) {
18517
- if (!chunksCarryAudio(planDir)) return false;
18688
+ const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
18689
+ if (!chunksCarryAudio && !separateAudio) return false;
18518
18690
  if (cfr) {
18519
- throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks carry audio");
18691
+ throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
18520
18692
  }
18521
18693
  return true;
18522
18694
  }
18523
- function chunksCarryAudio(planDir) {
18695
+ function stitchesSeparateAudio(planDir) {
18696
+ return lockedAudio(planDir).separateAudio === true;
18697
+ }
18698
+ function lockedAudio(planDir) {
18524
18699
  const encoderJsonPath = join41(planDir, "meta", "encoder.json");
18525
- if (!existsSync27(encoderJsonPath)) return false;
18526
- return JSON.parse(readFileSync14(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
18700
+ if (!existsSync27(encoderJsonPath)) return {};
18701
+ return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
18527
18702
  }
18528
18703
  function readCfrEncoder(planDir, format) {
18529
18704
  if (format !== "mp4") {
@@ -18597,11 +18772,12 @@ import {
18597
18772
  calculateOptimalWorkers as calculateOptimalWorkers2,
18598
18773
  classifyCaptureFailure as classifyCaptureFailure4,
18599
18774
  closeCaptureSession as closeCaptureSession11,
18600
- createCaptureSession as createCaptureSession10,
18775
+ createCaptureSession as createCaptureSession11,
18601
18776
  createFrameLookupTable as createFrameLookupTable5,
18602
18777
  deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
18603
18778
  getEncoderPreset as getEncoderPreset6,
18604
- initializeSession as initializeSession10,
18779
+ initializeSession as initializeSession11,
18780
+ measureVideoShownBoxes,
18605
18781
  probeBeginFrameLiveness as probeBeginFrameLiveness2,
18606
18782
  readWebGlVendorInfoFromCanvas,
18607
18783
  resolveConfig as resolveConfig7
@@ -18630,9 +18806,9 @@ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
18630
18806
  return { startFrame, endFrame };
18631
18807
  }
18632
18808
  var distributedCaptureSessionDependencies = {
18633
- createCaptureSession: createCaptureSession10,
18809
+ createCaptureSession: createCaptureSession11,
18634
18810
  assertSwiftShader,
18635
- initializeSession: initializeSession10,
18811
+ initializeSession: initializeSession11,
18636
18812
  closeCaptureSession: closeCaptureSession11,
18637
18813
  readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
18638
18814
  };
@@ -18823,34 +18999,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18823
18999
  const framesDir = join42(workDir, "captured-frames");
18824
19000
  mkdirSync18(framesDir, { recursive: true });
18825
19001
  const videoFramesDir = `${workDir}.video-frames`;
18826
- const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
18827
- planDir,
18828
- planVideos,
18829
- frames: Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i),
18830
- fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18831
- outputRoot: videoFramesDir,
18832
- cfg,
18833
- remoteSources
18834
- }) : [];
19002
+ const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
19003
+ const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
19004
+ const deferredVideos = planVideos?.extracted.filter((video) => video.deferredRange) ?? [];
19005
+ const shownFrames = planVideos ? shownDeferredFrameIndexes(planVideos, chunkFrameNumbers, chunkFps) : /* @__PURE__ */ new Map();
18835
19006
  const remoteVideoIds = new Set(
18836
19007
  planVideos?.extracted.filter(
18837
19008
  (video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
18838
19009
  ).map((video) => video.videoId)
18839
19010
  );
18840
- const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
18841
- const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
18842
- planVideos.videos,
18843
- [
18844
- ...rebuildExtractedFramesFromPlanDir(
18845
- planDir,
18846
- planVideos.extracted.filter((video) => !video.deferredRange),
18847
- v2Manifest === null ? "dense-v1" : "sparse-v2"
18848
- ),
18849
- ...deferredVideoFrames
18850
- ],
18851
- resolveRenderFpsConfig(job.config.fps).value
18852
- ) : null;
18853
- const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
18854
19011
  const fileServer = await createFileServer({
18855
19012
  projectDir: compiledDir,
18856
19013
  compiledDir,
@@ -18866,8 +19023,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18866
19023
  videoCount: planVideos?.videos.length ?? 0,
18867
19024
  html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
18868
19025
  // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
18869
- skipReadinessVideoIds: deferredVideoFrames.filter((video) => video.framePaths.size === 0 || remoteVideoIds.has(video.videoId)).map((video) => video.videoId),
18870
- videoMetadataHints: collectVideoMetadataHints(remoteVideos)
19026
+ skipReadinessVideoIds: deferredVideos.filter(
19027
+ (video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
19028
+ ).map((video) => video.videoId),
19029
+ videoMetadataHints: collectVideoMetadataHints(
19030
+ deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
19031
+ )
18871
19032
  });
18872
19033
  const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
18873
19034
  let session = null;
@@ -18880,6 +19041,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18880
19041
  const capturePerfs = [];
18881
19042
  const captureAttempts = [];
18882
19043
  let forceScreenshotForChunk = encoder.forceScreenshot;
19044
+ let shownBoxes;
18883
19045
  try {
18884
19046
  if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
18885
19047
  const bootStarted = Date.now();
@@ -18890,6 +19052,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18890
19052
  cfg
18891
19053
  );
18892
19054
  sessionBootMs = Date.now() - bootStarted;
19055
+ shownBoxes = await measureVideoShownBoxes(
19056
+ session.page,
19057
+ [...shownFrames.keys()],
19058
+ chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
19059
+ );
18893
19060
  const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
18894
19061
  const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
18895
19062
  if (browserSelectedScreenshot) {
@@ -18931,6 +19098,29 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18931
19098
  session = null;
18932
19099
  }
18933
19100
  }
19101
+ const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
19102
+ planDir,
19103
+ planVideos,
19104
+ frames: chunkFrameNumbers,
19105
+ fps: chunkFps,
19106
+ outputRoot: videoFramesDir,
19107
+ cfg,
19108
+ remoteSources,
19109
+ shownBoxes
19110
+ }) : [];
19111
+ const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
19112
+ planVideos.videos,
19113
+ [
19114
+ ...rebuildExtractedFramesFromPlanDir(
19115
+ planDir,
19116
+ planVideos.extracted.filter((video) => !video.deferredRange),
19117
+ v2Manifest === null ? "dense-v1" : "sparse-v2"
19118
+ ),
19119
+ ...deferredVideoFrames
19120
+ ],
19121
+ resolveRenderFpsConfig(job.config.fps).value
19122
+ ) : null;
19123
+ const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
18934
19124
  const captureStarted = Date.now();
18935
19125
  captureMode = await runCaptureWithScreenshotFallback({
18936
19126
  forceScreenshot: forceScreenshotForChunk,
@@ -19121,6 +19311,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19121
19311
  encodeStageMs,
19122
19312
  workers: chunkWorkerCount,
19123
19313
  captureMode,
19314
+ // Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
19315
+ videoShownBoxes: Object.fromEntries(shownBoxes ?? []),
19124
19316
  sha256,
19125
19317
  outputKind,
19126
19318
  producerVersion: plan2.producerVersion,
@@ -19188,6 +19380,9 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
19188
19380
  rmSync17(workRoot, { recursive: true, force: true });
19189
19381
  }
19190
19382
  }
19383
+
19384
+ // src/services/distributed/projectHash.ts
19385
+ var READ_CHUNK_BYTES = 256 * 1024;
19191
19386
  export {
19192
19387
  CURRENT_PLAN_PROTOCOL,
19193
19388
  DEFAULT_CONFIG2 as DEFAULT_CONFIG,
@@ -19220,7 +19415,7 @@ export {
19220
19415
  captureFrameToBuffer4 as captureFrameToBuffer,
19221
19416
  closeCaptureSession10 as closeCaptureSession,
19222
19417
  collectRenderMedia,
19223
- createCaptureSession9 as createCaptureSession,
19418
+ createCaptureSession10 as createCaptureSession,
19224
19419
  createConsoleLogger,
19225
19420
  createFileServer,
19226
19421
  createOwnedRenderDir,
@@ -19238,7 +19433,7 @@ export {
19238
19433
  getCompositionDuration2 as getCompositionDuration,
19239
19434
  getDistributedRenderCapabilities,
19240
19435
  getPlanV2ExecutionPlanHash,
19241
- initializeSession9 as initializeSession,
19436
+ initializeSession10 as initializeSession,
19242
19437
  injectDeterministicFontFaces2 as injectDeterministicFontFaces,
19243
19438
  isTransientBrowserError3 as isTransientBrowserError,
19244
19439
  listAbandonedRenderDirs,