@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/distributed.d.ts +2 -2
- package/dist/distributed.js +139 -48
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +54 -54
- package/dist/index.js +138 -48
- package/dist/public-server.js +78 -23
- package/dist/services/distributed/assemble.d.ts +4 -2
- package/dist/services/distributed/chunkAudio.d.ts +8 -0
- package/dist/services/distributed/chunkFrames.d.ts +2 -1
- package/dist/services/distributed/planProtocol.d.ts +1 -0
- package/dist/services/distributed/planV2Execution.d.ts +4 -4
- package/dist/services/distributed/renderChunk.d.ts +23 -3
- package/dist/services/render/audioTruePeak.d.ts +12 -0
- package/package.json +7 -7
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
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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] ${
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
18508
|
+
const result = await runFfmpeg7(args, { signal, timeout });
|
|
18431
18509
|
if (!result.success) {
|
|
18432
18510
|
throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
|
|
18433
|
-
error:
|
|
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
|
|
18668
|
-
if (
|
|
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: ${
|
|
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:
|
|
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:
|
|
19026
|
-
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
|
}
|
package/dist/public-server.js
CHANGED
|
@@ -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
|
|
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) =>
|
|
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
|
|
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] ${
|
|
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
|
|
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,
|
|
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:
|
|
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>;
|