@hyperframes/producer 0.8.179 → 0.8.181

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
@@ -10373,14 +10373,84 @@ import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSyn
10373
10373
  import { dirname as dirname9, join as join25 } from "node:path";
10374
10374
  import {
10375
10375
  DEFAULT_CONFIG,
10376
- describeFfmpegFailure,
10376
+ describeFfmpegFailure as describeFfmpegFailure2,
10377
10377
  extractAudioMetadata as extractAudioMetadata2,
10378
10378
  getFfprobeBinary,
10379
10379
  ManagedChildProcess,
10380
- runFfmpeg as runFfmpeg4,
10380
+ runFfmpeg as runFfmpeg5,
10381
10381
  trackChildProcess
10382
10382
  } from "@hyperframes/engine";
10383
10383
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
10384
+
10385
+ // src/services/render/audioTruePeak.ts
10386
+ import { aacSegmentCount, describeFfmpegFailure, runFfmpeg as runFfmpeg4 } from "@hyperframes/engine";
10387
+ var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10388
+ function checkedDbfs(value) {
10389
+ if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10390
+ throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
10391
+ }
10392
+ return value;
10393
+ }
10394
+ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
10395
+ let firstBlock;
10396
+ let laterBlocks = Number.NEGATIVE_INFINITY;
10397
+ let partialLine = "";
10398
+ const readLines = (chunk) => {
10399
+ const lines = (partialLine + chunk).split(/\r|\n/);
10400
+ partialLine = lines.pop() ?? "";
10401
+ for (const line of lines) {
10402
+ const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
10403
+ if (!ftpk) continue;
10404
+ const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
10405
+ if (firstBlock === void 0) firstBlock = block;
10406
+ else laterBlocks = Math.max(laterBlocks, block);
10407
+ }
10408
+ };
10409
+ const result = await runFfmpeg4(
10410
+ ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
10411
+ { signal, timeout: timeoutMs, onStderr: readLines }
10412
+ );
10413
+ if (!result.success) {
10414
+ throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
10415
+ }
10416
+ const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10417
+ return {
10418
+ peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
10419
+ firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
10420
+ laterBlocks: checkedDbfs(laterBlocks)
10421
+ };
10422
+ }
10423
+ async function measureTruePeakDbfs(audioPath, options) {
10424
+ const { timeoutMs, signal, segments } = options;
10425
+ const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
10426
+ 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;
10430
+ const peers = new AbortController();
10431
+ const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
10432
+ const scans = await Promise.all(
10433
+ Array.from({ length: count }, (_, k) => {
10434
+ const start = k === 0 ? 0 : cut(k) - block;
10435
+ const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
10436
+ return scanTruePeak(
10437
+ audioPath,
10438
+ `atrim=start_sample=${start}${end},`,
10439
+ timeoutMs,
10440
+ segmentSignal
10441
+ );
10442
+ })
10443
+ ).catch((error) => {
10444
+ peers.abort();
10445
+ throw error;
10446
+ });
10447
+ const last = scans[count - 1];
10448
+ const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
10449
+ if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
10450
+ return wholeFile();
10451
+ }
10452
+
10453
+ // src/services/render/audioPadTrim.ts
10384
10454
  var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10385
10455
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10386
10456
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
@@ -10480,7 +10550,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10480
10550
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10481
10551
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10482
10552
  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));
10553
+ const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10484
10554
  const [videoResult, audioResult] = await Promise.allSettled([
10485
10555
  probeVideo(input.videoPath),
10486
10556
  probeAudio(input.audioPath, input.signal)
@@ -10589,7 +10659,11 @@ async function constrainAacTruePeak(input) {
10589
10659
  audioPath: input.audioPath,
10590
10660
  targetDurationSeconds: input.targetDurationSeconds,
10591
10661
  signal: input.signal,
10592
- probeTruePeak: (audioPath, signal) => defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, 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
+ },
10593
10667
  runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
10594
10668
  });
10595
10669
  }
@@ -10726,28 +10800,12 @@ async function defaultProbeAudioInfo(audioPath, signal) {
10726
10800
  audioCodec: metadata.audioCodec
10727
10801
  };
10728
10802
  }
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
10803
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
10746
- const result = await runFfmpeg4(args, { signal, timeout: timeoutMs });
10804
+ const result = await runFfmpeg5(args, { signal, timeout: timeoutMs });
10747
10805
  if (result.success) return { success: true };
10748
10806
  return {
10749
10807
  success: false,
10750
- error: `[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`,
10808
+ error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
10751
10809
  failureReason: result.failureReason
10752
10810
  };
10753
10811
  }
@@ -13927,6 +13985,7 @@ var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
13927
13985
  var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
13928
13986
  var MISSING_PLAN_ARTIFACT = "MISSING_PLAN_ARTIFACT";
13929
13987
  var CHUNK_INDEX_OUT_OF_RANGE = "CHUNK_INDEX_OUT_OF_RANGE";
13988
+ var CHUNK_FRAME_RANGE_INVALID = "CHUNK_FRAME_RANGE_INVALID";
13930
13989
  var MISSING_RUNTIME_ENV_SNAPSHOT = "MISSING_RUNTIME_ENV_SNAPSHOT";
13931
13990
  var RenderChunkValidationError = class extends Error {
13932
13991
  code;
@@ -14178,7 +14237,8 @@ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
14178
14237
  accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
14179
14238
  acceptsLegacyV1WithoutDescriptor: true
14180
14239
  })
14181
- })
14240
+ }),
14241
+ chunkFrameRange: true
14182
14242
  });
14183
14243
  function getDistributedRenderCapabilities() {
14184
14244
  return DISTRIBUTED_RENDER_CAPABILITIES;
@@ -14527,7 +14587,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
14527
14587
  import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
14528
14588
  import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
14529
14589
  import { join as join32 } from "node:path";
14530
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure2, runFfmpeg as runFfmpeg5 } from "@hyperframes/engine";
14590
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
14531
14591
 
14532
14592
  // src/services/distributed/mp4Stitch.ts
14533
14593
  import {
@@ -14893,6 +14953,18 @@ function audioCutForChunk(grid, chunks, index, fps) {
14893
14953
  const durationSeconds = end === grid.packetCount ? null : cutSeconds(end) - (seekSeconds ?? 0);
14894
14954
  return { seekSeconds, durationSeconds, firstPacket: first, packetCount: end - first };
14895
14955
  }
14956
+ function chunksWithSubRange(chunks, index, range) {
14957
+ const slice = chunks[index];
14958
+ const parts = [
14959
+ { startFrame: slice.startFrame, endFrame: range.startFrame },
14960
+ range,
14961
+ { startFrame: range.endFrame, endFrame: slice.endFrame }
14962
+ ].filter((part) => part.endFrame > part.startFrame);
14963
+ const split = [...chunks.slice(0, index), ...parts, ...chunks.slice(index + 1)].map(
14964
+ (chunk, i) => ({ index: i, startFrame: chunk.startFrame, endFrame: chunk.endFrame })
14965
+ );
14966
+ return { chunks: split, index: range.startFrame > slice.startFrame ? index + 1 : index };
14967
+ }
14896
14968
  async function probeAacPacketGrid(audioPath) {
14897
14969
  const probe = await runFfprobeJson([
14898
14970
  "-v",
@@ -15005,10 +15077,10 @@ async function muxChunkWithAudio(input) {
15005
15077
  const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
15006
15078
  appendRenderProvenanceArgs(args, input.outputPath);
15007
15079
  args.push("-y", input.outputPath);
15008
- const result = await runFfmpeg5(args, { signal: input.signal, timeout: input.timeoutMs });
15080
+ const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
15009
15081
  if (!result.success) {
15010
15082
  throw new Error(
15011
- `[chunkAudio] chunk mux failed: ${describeFfmpegFailure2(result, input.timeoutMs)}`
15083
+ `[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
15012
15084
  );
15013
15085
  }
15014
15086
  const slice = input.chunks[input.chunkIndex];
@@ -18219,12 +18291,12 @@ import {
18219
18291
  import { dirname as dirname17, join as join41 } from "node:path";
18220
18292
  import {
18221
18293
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
18222
- describeFfmpegFailure as describeFfmpegFailure3,
18294
+ describeFfmpegFailure as describeFfmpegFailure4,
18223
18295
  getEncoderPreset as getEncoderPreset5,
18224
18296
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
18225
18297
  muxVideoWithAudio as muxVideoWithAudio2,
18226
18298
  resolveConfig as resolveConfig6,
18227
- runFfmpeg as runFfmpeg6
18299
+ runFfmpeg as runFfmpeg7
18228
18300
  } from "@hyperframes/engine";
18229
18301
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
18230
18302
  function cfrRateControlArgs(locked) {
@@ -18234,11 +18306,12 @@ function cfrRateControlArgs(locked) {
18234
18306
  }
18235
18307
  return ["-preset", preset.preset, "-crf", String(locked.crf ?? preset.quality)];
18236
18308
  }
18237
- async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
18309
+ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18238
18310
  const start = Date.now();
18239
18311
  const log = options?.logger ?? defaultLogger;
18240
18312
  const cfr = options?.cfr === true;
18241
- const plan2 = readAssemblePlan(planDir, chunkPaths);
18313
+ const plan2 = readAssemblePlan(planDir, chunkOutputs);
18314
+ const chunkPaths = chunkOutputs.flat();
18242
18315
  if (plan2.dimensions.format === "png-sequence") {
18243
18316
  return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
18244
18317
  }
@@ -18321,7 +18394,12 @@ function readAssemblePlan(planDir, chunkPaths) {
18321
18394
  `[assemble] chunkPaths length (${chunkPaths.length}) does not match chunks.json length (${chunks.length}). Adapters must pass one path per chunk, ordered by index.`
18322
18395
  );
18323
18396
  }
18324
- for (const path of chunkPaths) {
18397
+ for (const [index, entry] of chunkPaths.entries()) {
18398
+ if (typeof entry !== "string" && entry.length === 0) {
18399
+ throw new Error(`[assemble] chunk ${index} has an empty list of parts`);
18400
+ }
18401
+ }
18402
+ for (const path of chunkPaths.flat()) {
18325
18403
  if (!existsSync27(path)) {
18326
18404
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
18327
18405
  }
@@ -18427,10 +18505,10 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
18427
18505
  };
18428
18506
  }
18429
18507
  async function runAssembleFfmpeg(args, step, signal, timeout) {
18430
- const result = await runFfmpeg6(args, { signal, timeout });
18508
+ const result = await runFfmpeg7(args, { signal, timeout });
18431
18509
  if (!result.success) {
18432
18510
  throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
18433
- error: describeFfmpegFailure3(result, timeout),
18511
+ error: describeFfmpegFailure4(result, timeout),
18434
18512
  failureReason: result.failureReason
18435
18513
  });
18436
18514
  }
@@ -18540,6 +18618,17 @@ function validatePlanVideosForChunk(value) {
18540
18618
  );
18541
18619
  }
18542
18620
  }
18621
+ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
18622
+ if (frameRange === void 0) return { startFrame: slice.startFrame, endFrame: slice.endFrame };
18623
+ const { startFrame, endFrame } = frameRange;
18624
+ if (!Number.isSafeInteger(startFrame) || !Number.isSafeInteger(endFrame) || startFrame < slice.startFrame || endFrame > slice.endFrame || endFrame <= startFrame) {
18625
+ throw new RenderChunkValidationError(
18626
+ CHUNK_FRAME_RANGE_INVALID,
18627
+ `[renderChunk] frameRange ${JSON.stringify(frameRange)} must be a non-empty integer range inside chunk ${chunkIndex}'s slice [${slice.startFrame}, ${slice.endFrame})`
18628
+ );
18629
+ }
18630
+ return { startFrame, endFrame };
18631
+ }
18543
18632
  var distributedCaptureSessionDependencies = {
18544
18633
  createCaptureSession: createCaptureSession10,
18545
18634
  assertSwiftShader,
@@ -18610,7 +18699,7 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
18610
18699
  if (lockedEncoder.encoder !== "libvpx-vp9-software") return void 0;
18611
18700
  return lockedEncoder.vp9CpuUsed ?? LEGACY_DISTRIBUTED_VP9_CPU_USED;
18612
18701
  }
18613
- async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18702
+ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
18614
18703
  const start = Date.now();
18615
18704
  const log = defaultLogger;
18616
18705
  const planJsonPath = join42(planDir, "plan.json");
@@ -18664,13 +18753,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18664
18753
  if (slice === void 0) {
18665
18754
  throw new RenderChunkValidationError(CHUNK_INDEX_OUT_OF_RANGE, "[renderChunk] missing slice");
18666
18755
  }
18667
- const framesInChunk = slice.endFrame - slice.startFrame;
18668
- if (framesInChunk <= 0) {
18756
+ const sliceFrames = slice.endFrame - slice.startFrame;
18757
+ if (sliceFrames <= 0) {
18669
18758
  throw new RenderChunkValidationError(
18670
18759
  CHUNK_INDEX_OUT_OF_RANGE,
18671
- `[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
18760
+ `[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${sliceFrames}`
18672
18761
  );
18673
18762
  }
18763
+ const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
18764
+ const framesInChunk = range.endFrame - range.startFrame;
18674
18765
  const compiledDir = join42(planDir, "compiled");
18675
18766
  if (!existsSync28(compiledDir)) {
18676
18767
  throw new RenderChunkValidationError(
@@ -18735,10 +18826,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18735
18826
  const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
18736
18827
  planDir,
18737
18828
  planVideos,
18738
- frames: Array.from(
18739
- { length: slice.endFrame - slice.startFrame },
18740
- (_, i) => slice.startFrame + i
18741
- ),
18829
+ frames: Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i),
18742
18830
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18743
18831
  outputRoot: videoFramesDir,
18744
18832
  cfg,
@@ -18894,7 +18982,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18894
18982
  abortSignal: void 0,
18895
18983
  assertNotAborted: () => {
18896
18984
  },
18897
- frameRange: { startFrame: slice.startFrame, endFrame: slice.endFrame }
18985
+ frameRange: range
18898
18986
  });
18899
18987
  const observedModes = new Set(capturePerfs.map((perf) => perf.captureMode));
18900
18988
  if (observedModes.size !== 1 || ![...observedModes].every(isCaptureMode)) {
@@ -18988,12 +19076,13 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18988
19076
  });
18989
19077
  if (encoder.chunksCarryAudio) {
18990
19078
  const audioScratchPath = `${outputChunkPath}.audio.m4a`;
19079
+ const audioChunks = options?.frameRange ? chunksWithSubRange(chunks, chunkIndex, range) : { chunks, index: chunkIndex };
18991
19080
  await muxChunkWithAudio({
18992
19081
  videoPath: videoOnlyPath,
18993
19082
  audioPath: chunkAudioSource(planDir, chunkIndex, audioScratchPath),
18994
19083
  outputPath: outputChunkPath,
18995
- chunks,
18996
- chunkIndex,
19084
+ chunks: audioChunks.chunks,
19085
+ chunkIndex: audioChunks.index,
18997
19086
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18998
19087
  timeoutMs: cfg.ffmpegProcessTimeout
18999
19088
  });
@@ -19022,8 +19111,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
19022
19111
  const perfPayload = {
19023
19112
  planHash: plan2.planHash,
19024
19113
  chunkIndex,
19025
- startFrame: slice.startFrame,
19026
- endFrame: slice.endFrame,
19114
+ startFrame: range.startFrame,
19115
+ endFrame: range.endFrame,
19027
19116
  framesEncoded,
19028
19117
  durationMs,
19029
19118
  planHashMs,
@@ -19052,6 +19141,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
19052
19141
  outputPath: outputChunkPath,
19053
19142
  outputKind,
19054
19143
  framesEncoded,
19144
+ frameRange: range,
19055
19145
  sha256,
19056
19146
  durationMs,
19057
19147
  planHashMs,
@@ -19068,12 +19158,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
19068
19158
  }
19069
19159
 
19070
19160
  // src/services/distributed/planV2Execution.ts
19071
- async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
19161
+ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
19072
19162
  const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-chunk-"));
19073
19163
  const materializedPlanDir = join43(workRoot, "plan");
19074
19164
  try {
19075
19165
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
19076
- return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
19166
+ return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
19077
19167
  } finally {
19078
19168
  rmSync17(workRoot, { recursive: true, force: true });
19079
19169
  }
@@ -10356,14 +10356,84 @@ import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSyn
10356
10356
  import { dirname as dirname9, join as join25 } from "node:path";
10357
10357
  import {
10358
10358
  DEFAULT_CONFIG,
10359
- describeFfmpegFailure,
10359
+ describeFfmpegFailure as describeFfmpegFailure2,
10360
10360
  extractAudioMetadata as extractAudioMetadata2,
10361
10361
  getFfprobeBinary,
10362
10362
  ManagedChildProcess,
10363
- runFfmpeg as runFfmpeg4,
10363
+ runFfmpeg as runFfmpeg5,
10364
10364
  trackChildProcess
10365
10365
  } from "@hyperframes/engine";
10366
10366
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
10367
+
10368
+ // src/services/render/audioTruePeak.ts
10369
+ import { aacSegmentCount, describeFfmpegFailure, runFfmpeg as runFfmpeg4 } from "@hyperframes/engine";
10370
+ var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10371
+ function checkedDbfs(value) {
10372
+ if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10373
+ throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
10374
+ }
10375
+ return value;
10376
+ }
10377
+ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
10378
+ let firstBlock;
10379
+ let laterBlocks = Number.NEGATIVE_INFINITY;
10380
+ let partialLine = "";
10381
+ const readLines = (chunk) => {
10382
+ const lines = (partialLine + chunk).split(/\r|\n/);
10383
+ partialLine = lines.pop() ?? "";
10384
+ for (const line of lines) {
10385
+ const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
10386
+ if (!ftpk) continue;
10387
+ const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
10388
+ if (firstBlock === void 0) firstBlock = block;
10389
+ else laterBlocks = Math.max(laterBlocks, block);
10390
+ }
10391
+ };
10392
+ const result = await runFfmpeg4(
10393
+ ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
10394
+ { signal, timeout: timeoutMs, onStderr: readLines }
10395
+ );
10396
+ if (!result.success) {
10397
+ throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
10398
+ }
10399
+ const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10400
+ return {
10401
+ peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
10402
+ firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
10403
+ laterBlocks: checkedDbfs(laterBlocks)
10404
+ };
10405
+ }
10406
+ async function measureTruePeakDbfs(audioPath, options) {
10407
+ const { timeoutMs, signal, segments } = options;
10408
+ const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
10409
+ const block = 4800;
10410
+ const count = segments?.sampleRate === 48e3 ? aacSegmentCount(segments.durationSeconds) : 1;
10411
+ if (!segments || count === 1) return wholeFile();
10412
+ const cut = (k) => k * Math.floor(segments.durationSeconds * 10 / count) * block;
10413
+ const peers = new AbortController();
10414
+ const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
10415
+ const scans = await Promise.all(
10416
+ Array.from({ length: count }, (_, k) => {
10417
+ const start = k === 0 ? 0 : cut(k) - block;
10418
+ const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
10419
+ return scanTruePeak(
10420
+ audioPath,
10421
+ `atrim=start_sample=${start}${end},`,
10422
+ timeoutMs,
10423
+ segmentSignal
10424
+ );
10425
+ })
10426
+ ).catch((error) => {
10427
+ peers.abort();
10428
+ throw error;
10429
+ });
10430
+ const last = scans[count - 1];
10431
+ const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
10432
+ if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
10433
+ return wholeFile();
10434
+ }
10435
+
10436
+ // src/services/render/audioPadTrim.ts
10367
10437
  var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10368
10438
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10369
10439
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
@@ -10463,7 +10533,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10463
10533
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10464
10534
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10465
10535
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10466
- const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal));
10536
+ const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10467
10537
  const [videoResult, audioResult] = await Promise.allSettled([
10468
10538
  probeVideo(input.videoPath),
10469
10539
  probeAudio(input.audioPath, input.signal)
@@ -10699,28 +10769,12 @@ async function defaultProbeAudioInfo(audioPath, signal) {
10699
10769
  audioCodec: metadata.audioCodec
10700
10770
  };
10701
10771
  }
10702
- async function defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal) {
10703
- const result = await runFfmpeg4(
10704
- ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"],
10705
- { signal, timeout: timeoutMs }
10706
- );
10707
- if (!result.success) {
10708
- throw new Error(`[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`);
10709
- }
10710
- const matches = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
10711
- const token = matches.at(-1)?.[1]?.toLowerCase();
10712
- const value = token === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
10713
- if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
10714
- throw new Error("[audioPadTrim] FFmpeg did not report AAC true peak");
10715
- }
10716
- return value;
10717
- }
10718
10772
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
10719
- const result = await runFfmpeg4(args, { signal, timeout: timeoutMs });
10773
+ const result = await runFfmpeg5(args, { signal, timeout: timeoutMs });
10720
10774
  if (result.success) return { success: true };
10721
10775
  return {
10722
10776
  success: false,
10723
- error: `[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`,
10777
+ error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
10724
10778
  failureReason: result.failureReason
10725
10779
  };
10726
10780
  }
@@ -13845,14 +13899,15 @@ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
13845
13899
  accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
13846
13900
  acceptsLegacyV1WithoutDescriptor: true
13847
13901
  })
13848
- })
13902
+ }),
13903
+ chunkFrameRange: true
13849
13904
  });
13850
13905
 
13851
13906
  // src/services/render/stages/planHash.ts
13852
13907
  var FIELD_DELIMITER = Buffer.from([0]);
13853
13908
 
13854
13909
  // src/services/distributed/chunkAudio.ts
13855
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure2, runFfmpeg as runFfmpeg5 } from "@hyperframes/engine";
13910
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
13856
13911
 
13857
13912
  // src/services/distributed/mp4Stitch.ts
13858
13913
  var SCAN_BYTES = 64 * 1024;
@@ -31,6 +31,7 @@ export interface AssembleResult {
31
31
  framesEncoded: number;
32
32
  fileSize: number;
33
33
  }
34
+ export type AssembleChunkPaths = readonly (string | readonly string[])[];
34
35
  type CfrRateControlSource = Partial<Pick<LockedRenderConfig, "quality" | "crf" | "bitrate">>;
35
36
  export declare function cfrRateControlArgs(locked: CfrRateControlSource): string[];
36
37
  /**
@@ -39,7 +40,8 @@ export declare function cfrRateControlArgs(locked: CfrRateControlSource): string
39
40
  * @param planDir — absolute path to the planDir produced by `plan()`.
40
41
  * @param chunkPaths — ordered chunk outputs, length === `chunks.json` length.
41
42
  * For mp4/mov each entry is a path to an encoded chunk file; for
42
- * png-sequence each entry is a path to a directory of frames.
43
+ * png-sequence each entry is a path to a directory of frames. An entry
44
+ * may instead be the ordered parts of that chunk (see {@link AssembleChunkPaths}).
43
45
  * @param audioPath — `<planDir>/<MIXED_AUDIO_FILENAME>` for mux'd formats. Pass `null`
44
46
  * when the composition has no audio (or `assemble` is being called for a
45
47
  * format whose audio is muxed elsewhere). `assemble` always normalizes
@@ -48,7 +50,7 @@ export declare function cfrRateControlArgs(locked: CfrRateControlSource): string
48
50
  * @param outputPath — final on-disk output (file for mp4/mov; directory
49
51
  * for png-sequence — created if missing).
50
52
  */
51
- export declare function assemble(planDir: string, chunkPaths: readonly string[], audioPath: string | null, outputPath: string, options?: {
53
+ export declare function assemble(planDir: string, chunkOutputs: AssembleChunkPaths, audioPath: string | null, outputPath: string, options?: {
52
54
  logger?: ProducerLogger;
53
55
  abortSignal?: AbortSignal;
54
56
  /**
@@ -15,6 +15,14 @@ interface AudioCut {
15
15
  packetCount: number;
16
16
  }
17
17
  export declare function audioCutForChunk(grid: AacPacketGrid, chunks: readonly ChunkSliceJson[], index: number, fps: Fps): AudioCut | null;
18
+ /** Chunk `index` split around `range`, so `audioCutForChunk` treats `range` as a chunk. */
19
+ export declare function chunksWithSubRange(chunks: readonly ChunkSliceJson[], index: number, range: {
20
+ startFrame: number;
21
+ endFrame: number;
22
+ }): {
23
+ chunks: ChunkSliceJson[];
24
+ index: number;
25
+ };
18
26
  export declare function chunkAudioSlicePath(index: number): string;
19
27
  /** Which chunks need a sliced-audio file: `"all"`, one index, or `null` if it is not one. */
20
28
  export declare function chunkAudioSliceTarget(path: string): "all" | number | null;
@@ -7,9 +7,10 @@ export declare const FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
7
7
  export declare const PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
8
8
  export declare const MISSING_PLAN_ARTIFACT = "MISSING_PLAN_ARTIFACT";
9
9
  export declare const CHUNK_INDEX_OUT_OF_RANGE = "CHUNK_INDEX_OUT_OF_RANGE";
10
+ export declare const CHUNK_FRAME_RANGE_INVALID = "CHUNK_FRAME_RANGE_INVALID";
10
11
  export declare const MISSING_RUNTIME_ENV_SNAPSHOT = "MISSING_RUNTIME_ENV_SNAPSHOT";
11
12
  export { INVALID_VIDEO_METADATA };
12
- export type RenderChunkValidationCode = typeof FFMPEG_VERSION_MISMATCH | typeof PLAN_HASH_MISMATCH | typeof MISSING_PLAN_ARTIFACT | typeof CHUNK_INDEX_OUT_OF_RANGE | typeof MISSING_RUNTIME_ENV_SNAPSHOT | typeof INVALID_VIDEO_METADATA | typeof BROWSER_GPU_NOT_SOFTWARE;
13
+ export type RenderChunkValidationCode = typeof FFMPEG_VERSION_MISMATCH | typeof PLAN_HASH_MISMATCH | typeof MISSING_PLAN_ARTIFACT | typeof CHUNK_INDEX_OUT_OF_RANGE | typeof CHUNK_FRAME_RANGE_INVALID | typeof MISSING_RUNTIME_ENV_SNAPSHOT | typeof INVALID_VIDEO_METADATA | typeof BROWSER_GPU_NOT_SOFTWARE;
13
14
  /** A malformed planDir or a worker that does not match the plan's fingerprint; adapters key retries off `code`. */
14
15
  export declare class RenderChunkValidationError extends Error {
15
16
  readonly code: RenderChunkValidationCode;
@@ -53,6 +53,7 @@ export interface DistributedRenderCapabilities {
53
53
  chunk: Readonly<PlanProtocolConsumerCapabilities>;
54
54
  assembler: Readonly<PlanProtocolConsumerCapabilities>;
55
55
  }>;
56
+ readonly chunkFrameRange?: boolean;
56
57
  }
57
58
  /** Serializable capability payload for fleet rollout and worker handshakes. */
58
59
  export declare const DISTRIBUTED_RENDER_CAPABILITIES: Readonly<DistributedRenderCapabilities>;
@@ -1,12 +1,12 @@
1
- import { assemble, type AssembleResult } from "./assemble.js";
2
- import { type EffectiveChunkResult } from "./renderChunk.js";
1
+ import { assemble, type AssembleChunkPaths, type AssembleResult } from "./assemble.js";
2
+ import { type EffectiveChunkResult, type RenderChunkOptions } from "./renderChunk.js";
3
3
  /**
4
4
  * Direct v2 chunk-role entry point. Storage adapters may instead download the
5
5
  * selected blobs themselves and call `materializePlanV2Target` + `renderChunk`.
6
6
  */
7
- export declare function renderChunkV2(planV2Dir: string, chunkIndex: number, outputChunkPath: string): Promise<EffectiveChunkResult>;
7
+ export declare function renderChunkV2(planV2Dir: string, chunkIndex: number, outputChunkPath: string, options?: RenderChunkOptions): Promise<EffectiveChunkResult>;
8
8
  /**
9
9
  * Direct v2 assembler-role entry point. Audio is resolved exclusively from
10
10
  * the assembler dependency set and never materialized by chunk workers.
11
11
  */
12
- export declare function assembleV2(planV2Dir: string, chunkPaths: readonly string[], outputPath: string, options?: Parameters<typeof assemble>[4]): Promise<AssembleResult>;
12
+ export declare function assembleV2(planV2Dir: string, chunkPaths: AssembleChunkPaths, outputPath: string, options?: Parameters<typeof assemble>[4]): Promise<AssembleResult>;
@@ -34,9 +34,9 @@
34
34
  * In-process renders (`executeRenderJob`) leave them off.
35
35
  */
36
36
  import { assertSwiftShader, type CaptureOptions, type CaptureMode, type CaptureSession, closeCaptureSession, createCaptureSession, type EngineConfig, initializeSession, probeBeginFrameLiveness, readWebGlVendorInfoFromCanvas } from "@hyperframes/engine";
37
- import { type LockedRenderConfig } from "../render/stages/freezePlan.js";
37
+ import { type ChunkSliceJson, type LockedRenderConfig } from "../render/stages/freezePlan.js";
38
38
  import { type PlanVideosJson } from "./shared.js";
39
- export { CHUNK_INDEX_OUT_OF_RANGE, createChunkVideoFrameInjectorFactory, extractDeferredVideoFramesForChunk, FFMPEG_VERSION_MISMATCH, INVALID_VIDEO_METADATA, MISSING_PLAN_ARTIFACT, MISSING_RUNTIME_ENV_SNAPSHOT, PLAN_HASH_MISMATCH, rebuildExtractedFramesFromPlanDir, RenderChunkValidationError, } from "./chunkFrames.js";
39
+ export { CHUNK_FRAME_RANGE_INVALID, CHUNK_INDEX_OUT_OF_RANGE, createChunkVideoFrameInjectorFactory, extractDeferredVideoFramesForChunk, FFMPEG_VERSION_MISMATCH, INVALID_VIDEO_METADATA, MISSING_PLAN_ARTIFACT, MISSING_RUNTIME_ENV_SNAPSHOT, PLAN_HASH_MISMATCH, rebuildExtractedFramesFromPlanDir, RenderChunkValidationError, } from "./chunkFrames.js";
40
40
  /** Validate the shared video contract before any v1 chunk can inject frames. */
41
41
  export declare function validatePlanVideosForChunk(value: unknown): PlanVideosJson;
42
42
  /**
@@ -51,6 +51,7 @@ export interface ChunkResult {
51
51
  /** `"file"` for mp4/mov; `"frame-dir"` for png-sequence. */
52
52
  outputKind: "file" | "frame-dir";
53
53
  framesEncoded: number;
54
+ frameRange?: ChunkFrameRange;
54
55
  sha256: string;
55
56
  durationMs: number;
56
57
  /**
@@ -92,7 +93,26 @@ export interface ChunkResult {
92
93
  /** Result returned by the built-in renderer, which always observes its mode. */
93
94
  export interface EffectiveChunkResult extends ChunkResult {
94
95
  captureMode: CaptureMode;
96
+ frameRange: ChunkFrameRange;
95
97
  }
98
+ /** Absolute composition frame indices, end exclusive, like a chunk's slice. */
99
+ export interface ChunkFrameRange {
100
+ startFrame: number;
101
+ endFrame: number;
102
+ }
103
+ export interface RenderChunkOptions {
104
+ /**
105
+ * Render only these frames of the chunk, e.g. to retry a slow chunk as
106
+ * parts rendered in parallel. Must be a non-empty range inside the chunk's
107
+ * slice. Each part is a self-contained output (its own IDR keyframe, frame
108
+ * names from zero, the audio packets it owns); pass the parts of a chunk,
109
+ * in order, as one `chunkPaths` entry to `assemble`. Omitted renders the
110
+ * whole slice.
111
+ */
112
+ frameRange?: ChunkFrameRange;
113
+ }
114
+ /** The frames a chunk renders: its slice, or a validated sub-range of it. */
115
+ export declare function resolveChunkFrameRange(slice: ChunkSliceJson, chunkIndex: number, frameRange: ChunkFrameRange | undefined): ChunkFrameRange;
96
116
  /** Compatibility-safe adapter seam for built-in or injected chunk renderers. */
97
117
  export type ChunkRenderer = (planDir: string, chunkIndex: number, outputChunkPath: string) => Promise<ChunkResult>;
98
118
  interface DistributedCaptureSessionDependencies {
@@ -159,4 +179,4 @@ export declare function resolveLockedVp9CpuUsed(lockedEncoder: Pick<LockedRender
159
179
  * outputs — the caller picks the right shape based on `meta/encoder.json`.
160
180
  * `renderChunk` enforces the same choice via `outputKind` on the result.
161
181
  */
162
- export declare function renderChunk(planDir: string, chunkIndex: number, outputChunkPath: string): Promise<EffectiveChunkResult>;
182
+ export declare function renderChunk(planDir: string, chunkIndex: number, outputChunkPath: string, options?: RenderChunkOptions): Promise<EffectiveChunkResult>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The true peak one `ebur128=peak=true` pass over the whole file reports, measured in parallel
3
+ * segments when the length is known and the rate is 48 kHz: the same digit, not an estimate.
4
+ */
5
+ export declare function measureTruePeakDbfs(audioPath: string, options: {
6
+ timeoutMs: number;
7
+ signal?: AbortSignal;
8
+ segments?: {
9
+ durationSeconds: number;
10
+ sampleRate: number;
11
+ };
12
+ }): Promise<number>;