@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.
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,
@@ -10373,36 +10425,97 @@ import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSyn
10373
10425
  import { dirname as dirname9, join as join25 } from "node:path";
10374
10426
  import {
10375
10427
  DEFAULT_CONFIG,
10376
- describeFfmpegFailure,
10377
- extractAudioMetadata as extractAudioMetadata2,
10428
+ describeFfmpegFailure as describeFfmpegFailure2,
10429
+ encodeAac,
10430
+ extractAudioMetadata as extractAudioMetadata3,
10378
10431
  getFfprobeBinary,
10379
10432
  ManagedChildProcess,
10380
- runFfmpeg as runFfmpeg4,
10433
+ runFfmpeg as runFfmpeg5,
10381
10434
  trackChildProcess
10382
10435
  } from "@hyperframes/engine";
10383
10436
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
10437
+
10438
+ // src/services/render/audioTruePeak.ts
10439
+ import {
10440
+ aacSegmentCount,
10441
+ describeFfmpegFailure,
10442
+ extractAudioMetadata as extractAudioMetadata2,
10443
+ runFfmpeg as runFfmpeg4
10444
+ } from "@hyperframes/engine";
10445
+ var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10446
+ function checkedDbfs(value) {
10447
+ if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10448
+ throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
10449
+ }
10450
+ return value;
10451
+ }
10452
+ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
10453
+ let firstBlock;
10454
+ let laterBlocks = Number.NEGATIVE_INFINITY;
10455
+ let partialLine = "";
10456
+ const readLines = (chunk) => {
10457
+ const lines = (partialLine + chunk).split(/\r|\n/);
10458
+ partialLine = lines.pop() ?? "";
10459
+ for (const line of lines) {
10460
+ const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
10461
+ if (!ftpk) continue;
10462
+ const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
10463
+ if (firstBlock === void 0) firstBlock = block;
10464
+ else laterBlocks = Math.max(laterBlocks, block);
10465
+ }
10466
+ };
10467
+ const result = await runFfmpeg4(
10468
+ ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
10469
+ { signal, timeout: timeoutMs, onStderr: readLines }
10470
+ );
10471
+ if (!result.success) {
10472
+ throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
10473
+ }
10474
+ const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10475
+ return {
10476
+ peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
10477
+ firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
10478
+ laterBlocks: checkedDbfs(laterBlocks)
10479
+ };
10480
+ }
10481
+ async function measureTruePeakDbfs(audioPath, options) {
10482
+ const { timeoutMs, signal } = options;
10483
+ const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
10484
+ const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
10485
+ const block = 4800;
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;
10489
+ const peers = new AbortController();
10490
+ const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
10491
+ const scans = await Promise.all(
10492
+ Array.from({ length: count }, (_, k) => {
10493
+ const start = k === 0 ? 0 : cut(k) - block;
10494
+ const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
10495
+ return scanTruePeak(
10496
+ audioPath,
10497
+ `atrim=start_sample=${start}${end},`,
10498
+ timeoutMs,
10499
+ segmentSignal
10500
+ );
10501
+ })
10502
+ ).catch((error) => {
10503
+ peers.abort();
10504
+ throw error;
10505
+ });
10506
+ const last = scans[count - 1];
10507
+ const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
10508
+ if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
10509
+ return wholeFile();
10510
+ }
10511
+
10512
+ // src/services/render/audioPadTrim.ts
10384
10513
  var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10385
10514
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10386
10515
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
10387
10516
  var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
10388
- function buildAacTruePeakCorrectionArgs(inputPath, outputPath, targetDurationSeconds, attenuationDb) {
10389
- return [
10390
- "-i",
10391
- inputPath,
10392
- "-map",
10393
- "0:a:0",
10394
- "-vn",
10395
- "-af",
10396
- `volume=${attenuationDb.toFixed(3)}dB`,
10397
- "-t",
10398
- formatSeconds(targetDurationSeconds),
10399
- "-c:a",
10400
- "aac",
10401
- "-b:a",
10402
- "192k",
10403
- "-y",
10404
- outputPath
10405
- ];
10517
+ function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
10518
+ return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10406
10519
  }
10407
10520
  function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
10408
10521
  const delta = targetDurationSeconds - sourceDurationSeconds;
@@ -10417,53 +10530,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
10417
10530
  if (delta > 0) {
10418
10531
  return {
10419
10532
  operation: "pad",
10420
- steps: [
10421
- {
10422
- kind: "normalize",
10423
- args: [
10424
- "-i",
10425
- audioPath,
10426
- "-af",
10427
- `apad,atrim=0:${targetSec}`,
10428
- "-t",
10429
- targetSec,
10430
- "-c:a",
10431
- "aac",
10432
- "-b:a",
10433
- "192k",
10434
- "-y",
10435
- outputPath
10436
- ]
10437
- }
10438
- ],
10533
+ steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
10439
10534
  cleanupPaths: []
10440
10535
  };
10441
10536
  }
10442
10537
  return {
10443
10538
  operation: "trim",
10444
- steps: [
10445
- {
10446
- kind: "trim",
10447
- args: [
10448
- "-i",
10449
- audioPath,
10450
- "-af",
10451
- `atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
10452
- // `atrim` limits decoded samples but does not cap muxer timestamps
10453
- // introduced by encoder delay/flush. The output duration contract
10454
- // is enforced at the container boundary as well; without `-t`, a
10455
- // long primed source can retain its original tail after re-encode.
10456
- "-t",
10457
- targetSec,
10458
- "-c:a",
10459
- "aac",
10460
- "-b:a",
10461
- "192k",
10462
- "-y",
10463
- outputPath
10464
- ]
10465
- }
10466
- ],
10539
+ steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
10467
10540
  cleanupPaths: []
10468
10541
  };
10469
10542
  }
@@ -10480,7 +10553,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10480
10553
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10481
10554
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10482
10555
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10483
- const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => defaultProbeAudioTruePeakDbfs(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 }));
10484
10558
  const [videoResult, audioResult] = await Promise.allSettled([
10485
10559
  probeVideo(input.videoPath),
10486
10560
  probeAudio(input.audioPath, input.signal)
@@ -10527,7 +10601,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10527
10601
  );
10528
10602
  try {
10529
10603
  for (const step of plan2.steps) {
10530
- 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);
10531
10605
  if (!ffmpegResult.success) {
10532
10606
  return {
10533
10607
  success: false,
@@ -10559,7 +10633,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10559
10633
  targetDurationSeconds,
10560
10634
  signal: input.signal,
10561
10635
  probeTruePeak,
10562
- runner
10636
+ encode
10563
10637
  });
10564
10638
  if (!correction.success) {
10565
10639
  return {
@@ -10589,8 +10663,8 @@ async function constrainAacTruePeak(input) {
10589
10663
  audioPath: input.audioPath,
10590
10664
  targetDurationSeconds: input.targetDurationSeconds,
10591
10665
  signal: input.signal,
10592
- probeTruePeak: (audioPath, signal) => defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal),
10593
- runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
10666
+ probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
10667
+ encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
10594
10668
  });
10595
10669
  }
10596
10670
  async function enforceAacTruePeak(input) {
@@ -10617,13 +10691,11 @@ async function enforceAacTruePeak(input) {
10617
10691
  }
10618
10692
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
10619
10693
  attenuationDb += correctionTargetDbfs - truePeakDbfs;
10620
- const result = await input.runner(
10621
- buildAacTruePeakCorrectionArgs(
10622
- input.audioPath,
10623
- correctedPath,
10624
- input.targetDurationSeconds,
10625
- attenuationDb
10626
- )
10694
+ const result = await input.encode(
10695
+ input.audioPath,
10696
+ correctedPath,
10697
+ input.targetDurationSeconds,
10698
+ aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
10627
10699
  );
10628
10700
  if (!result.success) {
10629
10701
  return {
@@ -10718,7 +10790,7 @@ function parseFrameRate(rate) {
10718
10790
  return { fpsNum, fpsDen };
10719
10791
  }
10720
10792
  async function defaultProbeAudioInfo(audioPath, signal) {
10721
- const metadata = await extractAudioMetadata2(audioPath, { signal });
10793
+ const metadata = await extractAudioMetadata3(audioPath, { signal });
10722
10794
  return {
10723
10795
  durationSeconds: metadata.durationSeconds,
10724
10796
  sampleRate: metadata.sampleRate,
@@ -10726,28 +10798,23 @@ async function defaultProbeAudioInfo(audioPath, signal) {
10726
10798
  audioCodec: metadata.audioCodec
10727
10799
  };
10728
10800
  }
10729
- async function defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal) {
10730
- const result = await runFfmpeg4(
10731
- ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"],
10732
- { signal, timeout: timeoutMs }
10733
- );
10734
- if (!result.success) {
10735
- throw new Error(`[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`);
10736
- }
10737
- const matches = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10738
- const token = matches.at(-1)?.[1]?.toLowerCase();
10739
- const value = token === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10740
- if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10741
- throw new Error("[audioPadTrim] FFmpeg did not report AAC true peak");
10742
- }
10743
- return value;
10744
- }
10745
10801
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
10746
- const result = await runFfmpeg4(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) {
10747
10814
  if (result.success) return { success: true };
10748
10815
  return {
10749
10816
  success: false,
10750
- error: `[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`,
10817
+ error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
10751
10818
  failureReason: result.failureReason
10752
10819
  };
10753
10820
  }
@@ -10931,7 +10998,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
10931
10998
  const reason = UNSUPPORTED_REASON2[route];
10932
10999
  if (reason === null) return;
10933
11000
  throw new Error(
10934
- `[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.`
10935
11002
  );
10936
11003
  }
10937
11004
 
@@ -11148,7 +11215,7 @@ function getNextRetryWorkerCount(currentWorkers) {
11148
11215
  return Math.max(1, Math.floor(currentWorkers / 2));
11149
11216
  }
11150
11217
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11151
- if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
11218
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11152
11219
  return join26(dirname10(outputPath), `work-${jobId}-`);
11153
11220
  }
11154
11221
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
@@ -11642,6 +11709,9 @@ function resolveDeShortBand(args) {
11642
11709
  function shouldPreferSingleWorkerDrawElement(args) {
11643
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;
11644
11711
  }
11712
+ function isProbeDeGated(session) {
11713
+ return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
11714
+ }
11645
11715
  function resolveInversionRetryPlan(args) {
11646
11716
  if (args.deWorkerInversion !== "inverted") return null;
11647
11717
  const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
@@ -11723,7 +11793,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
11723
11793
  }
11724
11794
  function resolveParallelCaptureMode(args) {
11725
11795
  const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
11726
- 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";
11727
11798
  }
11728
11799
  function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
11729
11800
  const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
@@ -12331,6 +12402,7 @@ async function executeRenderPipeline(input) {
12331
12402
  );
12332
12403
  const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
12333
12404
  const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
12405
+ if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
12334
12406
  const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
12335
12407
  const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
12336
12408
  const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
@@ -12371,7 +12443,7 @@ async function executeRenderPipeline(input) {
12371
12443
  singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
12372
12444
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12373
12445
  supersampling: deviceScaleFactor !== 1,
12374
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12446
+ probeDeGated: isProbeDeGated(probeSession),
12375
12447
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
12376
12448
  process.env.HF_DE_PARALLEL_STREAM === "true"
12377
12449
  };
@@ -12380,14 +12452,14 @@ async function executeRenderPipeline(input) {
12380
12452
  ...deInversionArgs,
12381
12453
  minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
12382
12454
  });
12383
- const deShortBand = resolveDeShortBand({
12455
+ let deShortBand = resolveDeShortBand({
12384
12456
  invertAtBaseFloor,
12385
12457
  invertAtBandFloor,
12386
12458
  bandEnabled: deShortBandEnabled,
12387
12459
  bandOpen: deShortBandOpen,
12388
12460
  elementCountSource: compositionElementCountSource
12389
12461
  });
12390
- const deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12462
+ let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
12391
12463
  const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
12392
12464
  const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
12393
12465
  const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
@@ -12396,7 +12468,7 @@ async function executeRenderPipeline(input) {
12396
12468
  const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
12397
12469
  const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
12398
12470
  const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
12399
- const deParallelRouterEligible = shouldPreferParallelDrawElement({
12471
+ let deParallelRouterEligible = shouldPreferParallelDrawElement({
12400
12472
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12401
12473
  requestedWorkers: job.config.workers,
12402
12474
  useDrawElement: cfg.useDrawElement,
@@ -12407,7 +12479,7 @@ async function executeRenderPipeline(input) {
12407
12479
  minFrames: deParallelMinFrames,
12408
12480
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12409
12481
  supersampling: deviceScaleFactor !== 1,
12410
- probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
12482
+ probeDeGated: isProbeDeGated(probeSession),
12411
12483
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12412
12484
  routerEnabled: deParallelRouterEnabled,
12413
12485
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12423,6 +12495,35 @@ async function executeRenderPipeline(input) {
12423
12495
  totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
12424
12496
  minMemoryMb: deParallelMinMemoryMb
12425
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;
12426
12527
  let workerCount;
12427
12528
  const captureStageObservationData = (extra = {}) => ({
12428
12529
  stagePhase: "capturing",
@@ -12610,7 +12711,7 @@ async function executeRenderPipeline(input) {
12610
12711
  }
12611
12712
  const parallelCaptureMode = resolveParallelCaptureMode({
12612
12713
  platform: process.platform,
12613
- headlessShell: resolveHeadlessShellPath(cfg) !== null,
12714
+ headlessShellPath: resolveHeadlessShellPath(cfg),
12614
12715
  forceScreenshot: captureForceScreenshot,
12615
12716
  deviceScaleFactor: captureOptions.deviceScaleFactor
12616
12717
  });
@@ -13382,7 +13483,9 @@ async function executeRenderPipeline(input) {
13382
13483
  outputFormat,
13383
13484
  durationSeconds: job.duration,
13384
13485
  fps: fpsToNumber9(job.config.fps),
13385
- expectedFrames: captureTotalFrames
13486
+ expectedFrames: captureTotalFrames,
13487
+ width: outputWidth,
13488
+ height: outputHeight
13386
13489
  })
13387
13490
  );
13388
13491
  const totalElapsed = Date.now() - pipelineStart;
@@ -13418,6 +13521,7 @@ async function executeRenderPipeline(input) {
13418
13521
  drawElement: {
13419
13522
  compileGate: deCompileGate,
13420
13523
  clampReason: deClampReason,
13524
+ probeGateReason: deProbeGateReason,
13421
13525
  workerInversion: deWorkerInversion,
13422
13526
  preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
13423
13527
  compositionElementCount,
@@ -13615,9 +13719,9 @@ import {
13615
13719
  import { join as join29 } from "node:path";
13616
13720
  import {
13617
13721
  closeCaptureSession as closeCaptureSession9,
13618
- createCaptureSession as createCaptureSession8,
13722
+ createCaptureSession as createCaptureSession9,
13619
13723
  createFrameLookupTable as createFrameLookupTable3,
13620
- initializeSession as initializeSession8
13724
+ initializeSession as initializeSession9
13621
13725
  } from "@hyperframes/engine";
13622
13726
 
13623
13727
  // src/services/distributed/chunkFrames.ts
@@ -14015,6 +14119,29 @@ function buildChunkCaptureOptions(input) {
14015
14119
  requiresWebGpu: compositionRequiresWebGpu3(input.html)
14016
14120
  };
14017
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
+ }
14018
14145
  async function extractDeferredVideoFramesForChunk(input) {
14019
14146
  const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
14020
14147
  const remoteSources = input.remoteSources ?? {};
@@ -14031,24 +14158,7 @@ async function extractDeferredVideoFramesForChunk(input) {
14031
14158
  });
14032
14159
  }
14033
14160
  if (deferred.length === 0) return [];
14034
- const table = createFrameLookupTable2(
14035
- input.planVideos.videos,
14036
- deferred.map((video) => ({
14037
- ...video,
14038
- outputDir: input.outputRoot,
14039
- framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
14040
- })),
14041
- resolveRenderFpsConfig(input.fps).value
14042
- );
14043
- const shown = /* @__PURE__ */ new Map();
14044
- for (const frame of input.frames) {
14045
- const globalTime = frame * input.fps.den / input.fps.num;
14046
- for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
14047
- const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
14048
- indexes.add(payload.frameIndex);
14049
- shown.set(videoId, indexes);
14050
- }
14051
- }
14161
+ const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
14052
14162
  return Promise.all(
14053
14163
  deferred.map(async (video) => {
14054
14164
  const outputDir = join28(input.outputRoot, video.videoId);
@@ -14074,7 +14184,8 @@ async function extractDeferredVideoFramesForChunk(input) {
14074
14184
  fps: input.fps,
14075
14185
  outputDir: input.outputRoot,
14076
14186
  format: range.format,
14077
- frameRanges: contiguousRuns(indexes)
14187
+ frameRanges: contiguousRuns(indexes),
14188
+ shownBox: input.shownBoxes?.get(video.videoId)
14078
14189
  },
14079
14190
  void 0,
14080
14191
  input.cfg
@@ -14121,7 +14232,7 @@ async function measurePlanSecondsPerFrame(input) {
14121
14232
  });
14122
14233
  let session = null;
14123
14234
  try {
14124
- session = await createCaptureSession8(
14235
+ session = await createCaptureSession9(
14125
14236
  fileServer.url,
14126
14237
  join29(workDir, "frame-cost"),
14127
14238
  buildChunkCaptureOptions({
@@ -14133,7 +14244,7 @@ async function measurePlanSecondsPerFrame(input) {
14133
14244
  createChunkVideoFrameInjectorFactory(lookup)(),
14134
14245
  createCaptureCalibrationConfig(input.cfg)
14135
14246
  );
14136
- await initializeSession8(session);
14247
+ await initializeSession9(session);
14137
14248
  const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
14138
14249
  return (estimate.p95Ms ?? 0) / 1e3;
14139
14250
  } finally {
@@ -14529,7 +14640,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
14529
14640
  import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
14530
14641
  import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
14531
14642
  import { join as join32 } from "node:path";
14532
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure2, runFfmpeg as runFfmpeg5 } from "@hyperframes/engine";
14643
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
14533
14644
 
14534
14645
  // src/services/distributed/mp4Stitch.ts
14535
14646
  import {
@@ -14727,6 +14838,20 @@ function withDuration(buf, target, duration) {
14727
14838
  function timescaleOf(buf, header) {
14728
14839
  return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
14729
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
+ }
14730
14855
  function buildStbl(stsd, track) {
14731
14856
  const stts = runs(track.durations);
14732
14857
  const stsc = [];
@@ -14768,24 +14893,48 @@ function planTracksOf(tracks, plan2, index) {
14768
14893
  }
14769
14894
  return matched;
14770
14895
  }
14771
- function buildStitchedHeader(layouts) {
14772
- const plan2 = readTracks(layouts[0].moov);
14773
- 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;
14774
14919
  const payloadBases = [];
14775
14920
  let payloadBytes = 0;
14776
- for (const layout of layouts) {
14921
+ for (const layout of sourceLayouts) {
14777
14922
  payloadBases.push(payloadBytes);
14778
14923
  payloadBytes += layout.payloadEnd - layout.payloadStart;
14779
14924
  }
14780
14925
  const template = layouts[0].moov;
14781
14926
  const root = boxAt(template, 0, 0, template.length);
14782
- 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;
14783
14931
  const buildMoov = (dataStart2) => {
14784
14932
  const traks = plan2.map((track, t) => {
14785
- const parts = chunks.flatMap((tracks, chunk) => {
14933
+ const parts = sources.flatMap((tracks, chunk) => {
14786
14934
  const part = tracks[t];
14787
14935
  return part ? [{ ...part, chunk }] : [];
14788
14936
  });
14937
+ const moov = t < chunkTrackCount ? template : audio.moov;
14789
14938
  const merged = {
14790
14939
  durations: parts.flatMap((part) => part.durations),
14791
14940
  sizes: parts.flatMap((part) => part.sizes),
@@ -14794,9 +14943,9 @@ function buildStitchedHeader(layouts) {
14794
14943
  chunkOffsets: parts.flatMap(
14795
14944
  (part) => part.chunkOffsets.map((offset) => {
14796
14945
  const i = part.chunk;
14797
- const { payloadStart, payloadEnd } = layouts[i];
14946
+ const { payloadStart, payloadEnd } = sourceLayouts[i];
14798
14947
  if (offset < payloadStart || offset >= payloadEnd) {
14799
- throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
14948
+ throw new Error(`[mp4Stitch] source ${i} has samples outside its mdat`);
14800
14949
  }
14801
14950
  return offset - payloadStart + payloadBases[i] + dataStart2;
14802
14951
  })
@@ -14812,20 +14961,25 @@ function buildStitchedHeader(layouts) {
14812
14961
  elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
14813
14962
  elst.writeUInt32BE(65536, 20);
14814
14963
  const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
14815
- const trak = rebuild(template, track.trak, {
14816
- 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]),
14817
14970
  edts: () => Buffer.alloc(0),
14818
- mdia: (mdia) => rebuild(template, mdia, {
14819
- mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
14820
- 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) })
14821
14974
  })
14822
14975
  });
14823
14976
  return { trak, shown };
14824
14977
  });
14825
14978
  let next = 0;
14979
+ const movieHeader = withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown)));
14826
14980
  return rebuild(template, root, {
14827
- mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
14828
- 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))
14829
14983
  });
14830
14984
  };
14831
14985
  const ftyp = layouts[0].ftyp;
@@ -14858,15 +15012,16 @@ async function copyPayload(path, layout, index, out) {
14858
15012
  );
14859
15013
  }
14860
15014
  }
14861
- async function writeStitchedMp4(chunkPaths, outputPath) {
14862
- const layouts = await Promise.all(chunkPaths.map((path) => readMp4Layout(fileRange(path))));
14863
- 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);
14864
15019
  mkdirSync11(dirname12(outputPath), { recursive: true });
14865
15020
  const workDir = mkdtempSync4(`${outputPath}.stitch-`);
14866
15021
  const out = createWriteStream(join31(workDir, "out.mp4"));
14867
15022
  try {
14868
15023
  out.write(header);
14869
- 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);
14870
15025
  out.end();
14871
15026
  await finished(out);
14872
15027
  renameSync5(join31(workDir, "out.mp4"), outputPath);
@@ -15019,10 +15174,10 @@ async function muxChunkWithAudio(input) {
15019
15174
  const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
15020
15175
  appendRenderProvenanceArgs(args, input.outputPath);
15021
15176
  args.push("-y", input.outputPath);
15022
- const result = await runFfmpeg5(args, { signal: input.signal, timeout: input.timeoutMs });
15177
+ const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
15023
15178
  if (!result.success) {
15024
15179
  throw new Error(
15025
- `[chunkAudio] chunk mux failed: ${describeFfmpegFailure2(result, input.timeoutMs)}`
15180
+ `[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
15026
15181
  );
15027
15182
  }
15028
15183
  const slice = input.chunks[input.chunkIndex];
@@ -15437,7 +15592,8 @@ function buildLockedRenderConfig(input) {
15437
15592
  chunkCount: input.chunkCount,
15438
15593
  runtimeEnv: input.runtimeEnv,
15439
15594
  variables: config.variables,
15440
- ...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
15595
+ ...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
15596
+ ...input.separateAudio ? { separateAudio: true } : {}
15441
15597
  };
15442
15598
  }
15443
15599
  function carriesChunkAudio(config, options) {
@@ -15532,8 +15688,10 @@ function createVideoSourceShipper(input) {
15532
15688
  return result;
15533
15689
  };
15534
15690
  }
15535
- async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
15536
- 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) {
15537
15695
  rejectUnsupportedDistributedFormat(config);
15538
15696
  validateNoGpuEncode({
15539
15697
  useGpu: false,
@@ -15669,6 +15827,100 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15669
15827
  });
15670
15828
  }
15671
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;
15672
15924
  const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
15673
15925
  let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
15674
15926
  const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
@@ -15679,24 +15931,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15679
15931
  observedAt: "pre-extract",
15680
15932
  log
15681
15933
  });
15682
- stageStarted = Date.now();
15683
- const extractResult = await runExtractVideosStage({
15684
- projectDir,
15685
- compiledDir,
15686
- job,
15687
- cfg,
15688
- composition,
15689
- abortSignal,
15690
- assertNotAborted,
15691
- materializeSymlinks: true,
15692
- deferRangeExtraction: options.deferVideoExtraction,
15693
- urlByAbsentLocalPath
15694
- });
15934
+ let stageStarted = Date.now();
15935
+ const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
15695
15936
  logPlanStage(log, "extract", stageStarted);
15696
- if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
15697
- if (extractResult.extractionResult) {
15698
- assertVideoExtractionSucceeded(extractResult.extractionResult);
15699
- }
15700
15937
  const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
15701
15938
  const videoFramesDst = join34(planDir, "video-frames");
15702
15939
  if (existsSync21(videoFramesDst)) rmSync11(videoFramesDst, { recursive: true, force: true });
@@ -15752,23 +15989,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15752
15989
  "utf-8"
15753
15990
  );
15754
15991
  const digests = options.digests ?? /* @__PURE__ */ new Map();
15755
- const chunksCarryAudio2 = carriesChunkAudio(config, options);
15756
- 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");
15757
15996
  stageStarted = Date.now();
15758
15997
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
15759
- runAudioStage({
15760
- projectDir,
15761
- workDir,
15762
- compiledDir: finalCompiledDir,
15763
- urlByAbsentLocalPath,
15764
- duration: audioSeconds,
15765
- ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
15766
- audioGain: cfg.audioGain,
15767
- audios: composition.audios,
15768
- masterFxChain: composition.masterFxChain,
15769
- abortSignal,
15770
- assertNotAborted
15771
- }).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)),
15772
16004
  (async () => {
15773
16005
  for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
15774
16006
  await prehashPlanFiles(dir, digests);
@@ -15818,22 +16050,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15818
16050
  );
15819
16051
  const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
15820
16052
  const audioResult = audioOutcome.value;
15821
- if (audioResult.audioError) {
16053
+ if (audioResult?.audioError) {
15822
16054
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
15823
16055
  }
15824
16056
  const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
15825
- if (audioResult.hasAudio && existsSync21(audioResult.audioOutputPath)) {
16057
+ if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
15826
16058
  renameSync6(audioResult.audioOutputPath, planAudioPath);
15827
16059
  }
15828
- if (chunksCarryAudio2 && existsSync21(planAudioPath)) {
16060
+ if (chunksCarryAudio && existsSync21(planAudioPath)) {
15829
16061
  stageStarted = Date.now();
15830
- const peak = await constrainAacTruePeak({
15831
- audioPath: planAudioPath,
15832
- targetDurationSeconds: audioSeconds,
15833
- signal: abortSignal,
15834
- timeoutMs: cfg.ffmpegProcessTimeout
15835
- });
15836
- if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
16062
+ await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
15837
16063
  logPlanStage(log, "audio-peak", stageStarted);
15838
16064
  if (options.sliceChunkAudio) {
15839
16065
  stageStarted = Date.now();
@@ -15859,7 +16085,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15859
16085
  effectiveChunkSize,
15860
16086
  chunkCount,
15861
16087
  runtimeEnv,
15862
- chunksCarryAudio: chunksCarryAudio2
16088
+ chunksCarryAudio,
16089
+ separateAudio
15863
16090
  });
15864
16091
  const compositionJson = {
15865
16092
  durationSeconds: job.duration ?? 0,
@@ -15904,7 +16131,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
15904
16131
  fontSnapshotSha,
15905
16132
  durationSeconds: job.duration ?? 0,
15906
16133
  totalFrames,
15907
- hasAudio: audioResult.hasAudio,
16134
+ hasAudio: audioResult?.hasAudio ?? false,
15908
16135
  digests
15909
16136
  });
15910
16137
  logPlanStage(log, "freeze", stageStarted);
@@ -16420,8 +16647,8 @@ function renderRequestFromDistributedConfig(input) {
16420
16647
 
16421
16648
  // src/services/frameCapture.ts
16422
16649
  import {
16423
- createCaptureSession as createCaptureSession9,
16424
- initializeSession as initializeSession9,
16650
+ createCaptureSession as createCaptureSession10,
16651
+ initializeSession as initializeSession10,
16425
16652
  closeCaptureSession as closeCaptureSession10,
16426
16653
  captureFrame as captureFrame2,
16427
16654
  captureFrameToBuffer as captureFrameToBuffer4,
@@ -17922,7 +18149,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
17922
18149
  )
17923
18150
  ),
17924
18151
  chunksCarryAudio: options.chunksCarryAudio,
17925
- sliceChunkAudio: true
18152
+ sliceChunkAudio: true,
18153
+ deferAudio: options.deferAudio
17926
18154
  });
17927
18155
  return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
17928
18156
  } catch (error) {
@@ -18233,12 +18461,12 @@ import {
18233
18461
  import { dirname as dirname17, join as join41 } from "node:path";
18234
18462
  import {
18235
18463
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
18236
- describeFfmpegFailure as describeFfmpegFailure3,
18464
+ describeFfmpegFailure as describeFfmpegFailure4,
18237
18465
  getEncoderPreset as getEncoderPreset5,
18238
18466
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
18239
18467
  muxVideoWithAudio as muxVideoWithAudio2,
18240
18468
  resolveConfig as resolveConfig6,
18241
- runFfmpeg as runFfmpeg6
18469
+ runFfmpeg as runFfmpeg7
18242
18470
  } from "@hyperframes/engine";
18243
18471
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
18244
18472
  function cfrRateControlArgs(locked) {
@@ -18258,7 +18486,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18258
18486
  return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
18259
18487
  }
18260
18488
  if (chunksAreStitchedMp4(planDir, cfr)) {
18261
- await writeStitchedMp4(chunkPaths, outputPath);
18489
+ const separateAudio = stitchesSeparateAudio(planDir) && audioPath !== null;
18490
+ await writeStitchedMp4(chunkPaths, outputPath, separateAudio ? audioPath : void 0);
18262
18491
  return {
18263
18492
  outputPath,
18264
18493
  durationMs: Date.now() - start,
@@ -18447,25 +18676,29 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
18447
18676
  };
18448
18677
  }
18449
18678
  async function runAssembleFfmpeg(args, step, signal, timeout) {
18450
- const result = await runFfmpeg6(args, { signal, timeout });
18679
+ const result = await runFfmpeg7(args, { signal, timeout });
18451
18680
  if (!result.success) {
18452
18681
  throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
18453
- error: describeFfmpegFailure3(result, timeout),
18682
+ error: describeFfmpegFailure4(result, timeout),
18454
18683
  failureReason: result.failureReason
18455
18684
  });
18456
18685
  }
18457
18686
  }
18458
18687
  function chunksAreStitchedMp4(planDir, cfr) {
18459
- if (!chunksCarryAudio(planDir)) return false;
18688
+ const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
18689
+ if (!chunksCarryAudio && !separateAudio) return false;
18460
18690
  if (cfr) {
18461
- 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");
18462
18692
  }
18463
18693
  return true;
18464
18694
  }
18465
- function chunksCarryAudio(planDir) {
18695
+ function stitchesSeparateAudio(planDir) {
18696
+ return lockedAudio(planDir).separateAudio === true;
18697
+ }
18698
+ function lockedAudio(planDir) {
18466
18699
  const encoderJsonPath = join41(planDir, "meta", "encoder.json");
18467
- if (!existsSync27(encoderJsonPath)) return false;
18468
- return JSON.parse(readFileSync14(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
18700
+ if (!existsSync27(encoderJsonPath)) return {};
18701
+ return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
18469
18702
  }
18470
18703
  function readCfrEncoder(planDir, format) {
18471
18704
  if (format !== "mp4") {
@@ -18539,11 +18772,12 @@ import {
18539
18772
  calculateOptimalWorkers as calculateOptimalWorkers2,
18540
18773
  classifyCaptureFailure as classifyCaptureFailure4,
18541
18774
  closeCaptureSession as closeCaptureSession11,
18542
- createCaptureSession as createCaptureSession10,
18775
+ createCaptureSession as createCaptureSession11,
18543
18776
  createFrameLookupTable as createFrameLookupTable5,
18544
18777
  deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
18545
18778
  getEncoderPreset as getEncoderPreset6,
18546
- initializeSession as initializeSession10,
18779
+ initializeSession as initializeSession11,
18780
+ measureVideoShownBoxes,
18547
18781
  probeBeginFrameLiveness as probeBeginFrameLiveness2,
18548
18782
  readWebGlVendorInfoFromCanvas,
18549
18783
  resolveConfig as resolveConfig7
@@ -18572,9 +18806,9 @@ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
18572
18806
  return { startFrame, endFrame };
18573
18807
  }
18574
18808
  var distributedCaptureSessionDependencies = {
18575
- createCaptureSession: createCaptureSession10,
18809
+ createCaptureSession: createCaptureSession11,
18576
18810
  assertSwiftShader,
18577
- initializeSession: initializeSession10,
18811
+ initializeSession: initializeSession11,
18578
18812
  closeCaptureSession: closeCaptureSession11,
18579
18813
  readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
18580
18814
  };
@@ -18765,34 +18999,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18765
18999
  const framesDir = join42(workDir, "captured-frames");
18766
19000
  mkdirSync18(framesDir, { recursive: true });
18767
19001
  const videoFramesDir = `${workDir}.video-frames`;
18768
- const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
18769
- planDir,
18770
- planVideos,
18771
- frames: Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i),
18772
- fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18773
- outputRoot: videoFramesDir,
18774
- cfg,
18775
- remoteSources
18776
- }) : [];
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();
18777
19006
  const remoteVideoIds = new Set(
18778
19007
  planVideos?.extracted.filter(
18779
19008
  (video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
18780
19009
  ).map((video) => video.videoId)
18781
19010
  );
18782
- const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
18783
- const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
18784
- planVideos.videos,
18785
- [
18786
- ...rebuildExtractedFramesFromPlanDir(
18787
- planDir,
18788
- planVideos.extracted.filter((video) => !video.deferredRange),
18789
- v2Manifest === null ? "dense-v1" : "sparse-v2"
18790
- ),
18791
- ...deferredVideoFrames
18792
- ],
18793
- resolveRenderFpsConfig(job.config.fps).value
18794
- ) : null;
18795
- const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
18796
19011
  const fileServer = await createFileServer({
18797
19012
  projectDir: compiledDir,
18798
19013
  compiledDir,
@@ -18808,8 +19023,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18808
19023
  videoCount: planVideos?.videos.length ?? 0,
18809
19024
  html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
18810
19025
  // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
18811
- skipReadinessVideoIds: deferredVideoFrames.filter((video) => video.framePaths.size === 0 || remoteVideoIds.has(video.videoId)).map((video) => video.videoId),
18812
- 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
+ )
18813
19032
  });
18814
19033
  const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
18815
19034
  let session = null;
@@ -18822,6 +19041,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18822
19041
  const capturePerfs = [];
18823
19042
  const captureAttempts = [];
18824
19043
  let forceScreenshotForChunk = encoder.forceScreenshot;
19044
+ let shownBoxes;
18825
19045
  try {
18826
19046
  if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
18827
19047
  const bootStarted = Date.now();
@@ -18832,6 +19052,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18832
19052
  cfg
18833
19053
  );
18834
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
+ );
18835
19060
  const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
18836
19061
  const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
18837
19062
  if (browserSelectedScreenshot) {
@@ -18873,6 +19098,29 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18873
19098
  session = null;
18874
19099
  }
18875
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);
18876
19124
  const captureStarted = Date.now();
18877
19125
  captureMode = await runCaptureWithScreenshotFallback({
18878
19126
  forceScreenshot: forceScreenshotForChunk,
@@ -19063,6 +19311,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19063
19311
  encodeStageMs,
19064
19312
  workers: chunkWorkerCount,
19065
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 ?? []),
19066
19316
  sha256,
19067
19317
  outputKind,
19068
19318
  producerVersion: plan2.producerVersion,
@@ -19130,6 +19380,9 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
19130
19380
  rmSync17(workRoot, { recursive: true, force: true });
19131
19381
  }
19132
19382
  }
19383
+
19384
+ // src/services/distributed/projectHash.ts
19385
+ var READ_CHUNK_BYTES = 256 * 1024;
19133
19386
  export {
19134
19387
  CURRENT_PLAN_PROTOCOL,
19135
19388
  DEFAULT_CONFIG2 as DEFAULT_CONFIG,
@@ -19162,7 +19415,7 @@ export {
19162
19415
  captureFrameToBuffer4 as captureFrameToBuffer,
19163
19416
  closeCaptureSession10 as closeCaptureSession,
19164
19417
  collectRenderMedia,
19165
- createCaptureSession9 as createCaptureSession,
19418
+ createCaptureSession10 as createCaptureSession,
19166
19419
  createConsoleLogger,
19167
19420
  createFileServer,
19168
19421
  createOwnedRenderDir,
@@ -19180,7 +19433,7 @@ export {
19180
19433
  getCompositionDuration2 as getCompositionDuration,
19181
19434
  getDistributedRenderCapabilities,
19182
19435
  getPlanV2ExecutionPlanHash,
19183
- initializeSession9 as initializeSession,
19436
+ initializeSession10 as initializeSession,
19184
19437
  injectDeterministicFontFaces2 as injectDeterministicFontFaces,
19185
19438
  isTransientBrowserError3 as isTransientBrowserError,
19186
19439
  listAbandonedRenderDirs,