@hyperframes/producer 0.8.173 → 0.8.175
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 +3 -1
- package/dist/distributed.js +469 -107
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +59 -59
- package/dist/index.js +482 -124
- package/dist/public-server.js +1 -1
- package/dist/services/distributed/assemble.d.ts +2 -0
- package/dist/services/distributed/mp4Stitch.d.ts +38 -0
- package/dist/services/distributed/plan.d.ts +3 -3
- package/dist/services/render/audioPadTrim.d.ts +15 -0
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -210,11 +210,11 @@ function getHfEarlyStub() {
|
|
|
210
210
|
|
|
211
211
|
// src/services/fileServer.ts
|
|
212
212
|
var readFileAsync = promisify(readFile);
|
|
213
|
-
function isPathInside(
|
|
213
|
+
function isPathInside(child2, parent, options = {}) {
|
|
214
214
|
const { resolveSymlinks = false, pathModule } = options;
|
|
215
215
|
const resolveFn = pathModule?.resolve ?? resolve2;
|
|
216
216
|
const separator = pathModule?.sep ?? sep;
|
|
217
|
-
const resolvedChild = resolveFn(
|
|
217
|
+
const resolvedChild = resolveFn(child2);
|
|
218
218
|
const resolvedParent = resolveFn(parent);
|
|
219
219
|
const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
|
|
220
220
|
const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
|
|
@@ -3115,7 +3115,7 @@ async function embedLocalFontFaces(html, projectDir) {
|
|
|
3115
3115
|
embeddedPaths.add(localPath);
|
|
3116
3116
|
continue;
|
|
3117
3117
|
}
|
|
3118
|
-
dataUri = await toDataUri(font.buffer, ext);
|
|
3118
|
+
dataUri = await toDataUri(font.buffer, ext, { compressToWoff2: false });
|
|
3119
3119
|
dataUriByAbsolutePath.set(absPath, dataUri);
|
|
3120
3120
|
defaultLogger.info(
|
|
3121
3121
|
`[Compiler] Embedded local font file: ${localPath} (${(font.buffer.length / 1024).toFixed(0)} KB \u2192 data URI)`
|
|
@@ -4558,7 +4558,7 @@ var VideoExtractionStageError = class extends Error {
|
|
|
4558
4558
|
affectedElementCount: failure.count
|
|
4559
4559
|
}))
|
|
4560
4560
|
)) {
|
|
4561
|
-
const total = failures.reduce((
|
|
4561
|
+
const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
|
|
4562
4562
|
const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
|
|
4563
4563
|
super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
|
|
4564
4564
|
this.code = code;
|
|
@@ -5822,6 +5822,16 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5822
5822
|
...loweredDb > 0 ? { audioLoweredDb: loweredDb } : {}
|
|
5823
5823
|
};
|
|
5824
5824
|
}
|
|
5825
|
+
async function constrainAacTruePeak(input) {
|
|
5826
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
5827
|
+
return enforceAacTruePeak({
|
|
5828
|
+
audioPath: input.audioPath,
|
|
5829
|
+
targetDurationSeconds: input.targetDurationSeconds,
|
|
5830
|
+
signal: input.signal,
|
|
5831
|
+
probeTruePeak: (audioPath, signal) => defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal),
|
|
5832
|
+
runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5825
5835
|
async function enforceAacTruePeak(input) {
|
|
5826
5836
|
let scratchDir;
|
|
5827
5837
|
try {
|
|
@@ -6170,7 +6180,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
|
6170
6180
|
return captured;
|
|
6171
6181
|
}
|
|
6172
6182
|
function countFrameRanges(ranges) {
|
|
6173
|
-
return ranges.reduce((
|
|
6183
|
+
return ranges.reduce((sum2, range) => sum2 + (range.endFrame - range.startFrame), 0);
|
|
6174
6184
|
}
|
|
6175
6185
|
async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
6176
6186
|
const attempts = [];
|
|
@@ -7207,15 +7217,15 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
|
7207
7217
|
function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger) {
|
|
7208
7218
|
if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
|
|
7209
7219
|
const { workerTimeoutSeconds, workerConcurrency } = config;
|
|
7210
|
-
const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
|
|
7211
|
-
if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
|
|
7212
|
-
return DEFAULT_MAX_PARALLEL_CHUNKS;
|
|
7213
|
-
}
|
|
7214
|
-
const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
|
|
7215
7220
|
const wave = Math.min(
|
|
7216
7221
|
workerConcurrency ?? DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
7217
7222
|
MAX_PARALLEL_CHUNKS_CEILING
|
|
7218
7223
|
);
|
|
7224
|
+
const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
|
|
7225
|
+
if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
|
|
7226
|
+
return wave;
|
|
7227
|
+
}
|
|
7228
|
+
const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
|
|
7219
7229
|
const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
|
|
7220
7230
|
const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
|
|
7221
7231
|
if (workerTimeoutSeconds === void 0) return wide;
|
|
@@ -7232,7 +7242,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
|
|
|
7232
7242
|
}
|
|
7233
7243
|
if (needed > wave) {
|
|
7234
7244
|
log.warn(
|
|
7235
|
-
`[plan] ${needed} chunks are needed to keep each under the ${workerTimeoutSeconds} s worker time limit, but the plan assumes ${wave} workers at once, so the render runs in ${Math.ceil(needed / wave)} waves. Raise --concurrency to ${needed} to render in one wave (Lambda: hyperframes lambda deploy --concurrency ${needed}).`,
|
|
7245
|
+
`[plan] ${needed} chunks are needed to keep each under the ${workerTimeoutSeconds} s worker time limit, but the plan assumes ${wave} workers at once, so the render runs in ${Math.ceil(needed / wave)} waves. Raise --concurrency to ${needed} to render in one wave (Lambda: hyperframes lambda deploy --concurrency ${needed}, or a higher account concurrency quota).`,
|
|
7236
7246
|
{ needed, workerConcurrency: wave, workerTimeoutSeconds }
|
|
7237
7247
|
);
|
|
7238
7248
|
}
|
|
@@ -7407,21 +7417,6 @@ function buildLockedRenderConfig(input) {
|
|
|
7407
7417
|
function carriesChunkAudio(config, options) {
|
|
7408
7418
|
return options.chunksCarryAudio === true && config.format === "mp4" && config.cfr !== true;
|
|
7409
7419
|
}
|
|
7410
|
-
async function fitPlanAudioToFrames(audioPath, workDir, totalFrames, fps, run) {
|
|
7411
|
-
const fittedPath = join18(workDir, "audio-fitted.m4a");
|
|
7412
|
-
const result = await padOrTrimAudioToVideoFrameCount({
|
|
7413
|
-
videoPath: audioPath,
|
|
7414
|
-
probeVideoFrameInfo: async () => ({ frameCount: totalFrames, fpsNum: fps, fpsDen: 1 }),
|
|
7415
|
-
audioPath,
|
|
7416
|
-
outputPath: fittedPath,
|
|
7417
|
-
signal: run.signal,
|
|
7418
|
-
timeoutMs: run.timeoutMs
|
|
7419
|
-
});
|
|
7420
|
-
if (!result.success) {
|
|
7421
|
-
throw encoderFailureError("[plan] audio pad/trim failed", result);
|
|
7422
|
-
}
|
|
7423
|
-
renameSync3(fittedPath, audioPath);
|
|
7424
|
-
}
|
|
7425
7420
|
function resolveEncoderTriple(config) {
|
|
7426
7421
|
if (config.format === "mp4") {
|
|
7427
7422
|
return resolveMp4EncoderTriple(config.codec);
|
|
@@ -7719,13 +7714,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7719
7714
|
"utf-8"
|
|
7720
7715
|
);
|
|
7721
7716
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
7717
|
+
const chunksCarryAudio2 = carriesChunkAudio(config, options);
|
|
7718
|
+
const audioSeconds = chunksCarryAudio2 ? totalFrames / config.fps : probeResult.duration;
|
|
7722
7719
|
stageStarted = Date.now();
|
|
7723
7720
|
const [audioOutcome, overlapOutcome] = await Promise.allSettled([
|
|
7724
7721
|
runAudioStage({
|
|
7725
7722
|
projectDir,
|
|
7726
7723
|
workDir,
|
|
7727
7724
|
compiledDir: finalCompiledDir,
|
|
7728
|
-
duration:
|
|
7725
|
+
duration: audioSeconds,
|
|
7729
7726
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
7730
7727
|
audioGain: cfg.audioGain,
|
|
7731
7728
|
audios: composition.audios,
|
|
@@ -7755,12 +7752,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7755
7752
|
if (audioResult.hasAudio && existsSync13(audioResult.audioOutputPath)) {
|
|
7756
7753
|
renameSync3(audioResult.audioOutputPath, planAudioPath);
|
|
7757
7754
|
}
|
|
7758
|
-
const chunksCarryAudio2 = carriesChunkAudio(config, options);
|
|
7759
7755
|
if (chunksCarryAudio2 && existsSync13(planAudioPath)) {
|
|
7760
|
-
|
|
7756
|
+
stageStarted = Date.now();
|
|
7757
|
+
const peak = await constrainAacTruePeak({
|
|
7758
|
+
audioPath: planAudioPath,
|
|
7759
|
+
targetDurationSeconds: audioSeconds,
|
|
7761
7760
|
signal: abortSignal,
|
|
7762
7761
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
7763
7762
|
});
|
|
7763
|
+
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
7764
|
+
logPlanStage(log, "audio-peak", stageStarted);
|
|
7764
7765
|
}
|
|
7765
7766
|
const ffmpegVersion = await readFfmpegVersion();
|
|
7766
7767
|
const producerVersion = readProducerVersion();
|
|
@@ -7845,8 +7846,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7845
7846
|
producerVersion
|
|
7846
7847
|
};
|
|
7847
7848
|
}
|
|
7848
|
-
async function plan(projectDir, config, planDir) {
|
|
7849
|
-
const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir);
|
|
7849
|
+
async function plan(projectDir, config, planDir, options = {}) {
|
|
7850
|
+
const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir, options);
|
|
7850
7851
|
return {
|
|
7851
7852
|
planDir: executionPlan.executionPlanDir,
|
|
7852
7853
|
planProtocol: PLAN_PROTOCOL_V1,
|
|
@@ -8091,7 +8092,7 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
|
|
|
8091
8092
|
}
|
|
8092
8093
|
function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
|
|
8093
8094
|
if (isPlanAudioArtifactPath(path)) {
|
|
8094
|
-
return { chunks:
|
|
8095
|
+
return chunksCarryAudio2 ? { chunks: "all", assembler: false } : { chunks: [], assembler: true };
|
|
8095
8096
|
}
|
|
8096
8097
|
if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
|
|
8097
8098
|
return { chunks: "all", assembler: true };
|
|
@@ -8652,7 +8653,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
|
|
|
8652
8653
|
planHash: manifest.planHash,
|
|
8653
8654
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
8654
8655
|
artifactCount: artifacts.length,
|
|
8655
|
-
sizeBytes: artifacts.reduce((
|
|
8656
|
+
sizeBytes: artifacts.reduce((sum2, artifact) => sum2 + artifact.sizeBytes, 0),
|
|
8656
8657
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
8657
8658
|
// before the container change carries the legacy name, and materializing it
|
|
8658
8659
|
// under the new one would point at a file that was never written.
|
|
@@ -8689,24 +8690,24 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
8689
8690
|
}
|
|
8690
8691
|
|
|
8691
8692
|
// src/services/distributed/planV2Execution.ts
|
|
8692
|
-
import { mkdtempSync as
|
|
8693
|
+
import { mkdtempSync as mkdtempSync7, rmSync as rmSync13 } from "node:fs";
|
|
8693
8694
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8694
|
-
import { join as
|
|
8695
|
+
import { join as join25 } from "node:path";
|
|
8695
8696
|
|
|
8696
8697
|
// src/services/distributed/assemble.ts
|
|
8697
8698
|
import {
|
|
8698
8699
|
cpSync as cpSync3,
|
|
8699
8700
|
existsSync as existsSync16,
|
|
8700
|
-
mkdirSync as
|
|
8701
|
-
mkdtempSync as
|
|
8701
|
+
mkdirSync as mkdirSync12,
|
|
8702
|
+
mkdtempSync as mkdtempSync6,
|
|
8702
8703
|
readFileSync as readFileSync10,
|
|
8703
8704
|
readdirSync as readdirSync7,
|
|
8704
|
-
renameSync as
|
|
8705
|
-
rmSync as
|
|
8705
|
+
renameSync as renameSync7,
|
|
8706
|
+
rmSync as rmSync11,
|
|
8706
8707
|
statSync as statSync7,
|
|
8707
8708
|
writeFileSync as writeFileSync8
|
|
8708
8709
|
} from "node:fs";
|
|
8709
|
-
import { dirname as
|
|
8710
|
+
import { dirname as dirname12, join as join23 } from "node:path";
|
|
8710
8711
|
import {
|
|
8711
8712
|
appendRenderProvenanceArgs,
|
|
8712
8713
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -8717,6 +8718,353 @@ import {
|
|
|
8717
8718
|
runFfmpeg as runFfmpeg5
|
|
8718
8719
|
} from "@hyperframes/engine";
|
|
8719
8720
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
8721
|
+
|
|
8722
|
+
// src/services/distributed/mp4Stitch.ts
|
|
8723
|
+
import {
|
|
8724
|
+
createReadStream as createReadStream4,
|
|
8725
|
+
createWriteStream,
|
|
8726
|
+
mkdirSync as mkdirSync11,
|
|
8727
|
+
mkdtempSync as mkdtempSync5,
|
|
8728
|
+
renameSync as renameSync6,
|
|
8729
|
+
rmSync as rmSync10
|
|
8730
|
+
} from "node:fs";
|
|
8731
|
+
import { open } from "node:fs/promises";
|
|
8732
|
+
import { dirname as dirname11, join as join22 } from "node:path";
|
|
8733
|
+
import { finished, pipeline } from "node:stream/promises";
|
|
8734
|
+
function boxAt(src, off, at, limit) {
|
|
8735
|
+
let size = src.readUInt32BE(off);
|
|
8736
|
+
let bodyStart = at + 8;
|
|
8737
|
+
if (size === 1) {
|
|
8738
|
+
size = Number(src.readBigUInt64BE(off + 8));
|
|
8739
|
+
bodyStart = at + 16;
|
|
8740
|
+
}
|
|
8741
|
+
if (size < 8 || at + size > limit) throw new Error(`[mp4Stitch] malformed box at ${at}`);
|
|
8742
|
+
return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
|
|
8743
|
+
}
|
|
8744
|
+
function childBoxes(buf, parent) {
|
|
8745
|
+
const boxes = [];
|
|
8746
|
+
for (let at = parent.bodyStart; at < parent.end; at = boxes.at(-1).end) {
|
|
8747
|
+
boxes.push(boxAt(buf, at, at, parent.end));
|
|
8748
|
+
}
|
|
8749
|
+
return boxes;
|
|
8750
|
+
}
|
|
8751
|
+
function findChild(buf, parent, type) {
|
|
8752
|
+
return childBoxes(buf, parent).find((box2) => box2.type === type);
|
|
8753
|
+
}
|
|
8754
|
+
function child(buf, parent, type) {
|
|
8755
|
+
const found = findChild(buf, parent, type);
|
|
8756
|
+
if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
|
|
8757
|
+
return found;
|
|
8758
|
+
}
|
|
8759
|
+
var SCAN_BYTES = 64 * 1024;
|
|
8760
|
+
async function readMp4Layout(read) {
|
|
8761
|
+
const found = /* @__PURE__ */ new Map();
|
|
8762
|
+
let payload;
|
|
8763
|
+
let buf = Buffer.alloc(0);
|
|
8764
|
+
let bufAt = 0;
|
|
8765
|
+
const load = async (start, end) => {
|
|
8766
|
+
if (start >= bufAt && end <= bufAt + buf.length) return;
|
|
8767
|
+
buf = await read(start, Math.max(end, start + SCAN_BYTES));
|
|
8768
|
+
bufAt = start;
|
|
8769
|
+
};
|
|
8770
|
+
for (let at = 0; !(found.has("ftyp") && found.has("moov") && payload); ) {
|
|
8771
|
+
await load(at, at + 16);
|
|
8772
|
+
if (at + 8 > bufAt + buf.length) {
|
|
8773
|
+
throw new Error(`[mp4Stitch] chunk needs ftyp, moov and mdat; found ${[...found.keys()]}`);
|
|
8774
|
+
}
|
|
8775
|
+
const box2 = boxAt(buf, at - bufAt, at, Number.MAX_SAFE_INTEGER);
|
|
8776
|
+
if (box2.type === "mdat") payload ??= box2;
|
|
8777
|
+
if (box2.type === "ftyp" || box2.type === "moov") {
|
|
8778
|
+
await load(at, box2.end);
|
|
8779
|
+
found.set(box2.type, buf.subarray(at - bufAt, box2.end - bufAt));
|
|
8780
|
+
}
|
|
8781
|
+
at = box2.end;
|
|
8782
|
+
}
|
|
8783
|
+
const [ftyp, moov] = [found.get("ftyp"), found.get("moov")];
|
|
8784
|
+
if (moov.length !== moovSize(moov)) throw new Error("[mp4Stitch] moov is cut off");
|
|
8785
|
+
return { ftyp, moov, payloadStart: payload.bodyStart, payloadEnd: payload.end };
|
|
8786
|
+
}
|
|
8787
|
+
var moovSize = (moov) => boxAt(moov, 0, 0, Number.MAX_SAFE_INTEGER).end;
|
|
8788
|
+
function readTracks(moov) {
|
|
8789
|
+
const root = boxAt(moov, 0, 0, moov.length);
|
|
8790
|
+
return childBoxes(moov, root).filter((box2) => box2.type === "trak").map((trak) => readTrack(moov, trak));
|
|
8791
|
+
}
|
|
8792
|
+
function rows(buf, table, columns, countAt = 4) {
|
|
8793
|
+
const count = buf.readUInt32BE(table.bodyStart + countAt);
|
|
8794
|
+
const first = table.bodyStart + countAt + 4;
|
|
8795
|
+
return Array.from(
|
|
8796
|
+
{ length: count },
|
|
8797
|
+
(_, row) => Array.from(
|
|
8798
|
+
{ length: columns },
|
|
8799
|
+
(_2, col) => buf.readUInt32BE(first + (row * columns + col) * 4)
|
|
8800
|
+
)
|
|
8801
|
+
);
|
|
8802
|
+
}
|
|
8803
|
+
function readMediaTime(moov, trak) {
|
|
8804
|
+
const edts = findChild(moov, trak, "edts");
|
|
8805
|
+
if (!edts) return 0;
|
|
8806
|
+
const elst = child(moov, edts, "elst");
|
|
8807
|
+
const wide = moov[elst.bodyStart] === 1;
|
|
8808
|
+
const entries = moov.readUInt32BE(elst.bodyStart + 4);
|
|
8809
|
+
for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
|
|
8810
|
+
const offset = at + (wide ? 8 : 4);
|
|
8811
|
+
const mediaTime = wide ? Number(moov.readBigInt64BE(offset)) : moov.readInt32BE(offset);
|
|
8812
|
+
if (mediaTime !== -1) return mediaTime;
|
|
8813
|
+
}
|
|
8814
|
+
return 0;
|
|
8815
|
+
}
|
|
8816
|
+
function readSizes(moov, stsz) {
|
|
8817
|
+
const fixedSize = moov.readUInt32BE(stsz.bodyStart + 4);
|
|
8818
|
+
if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
|
|
8819
|
+
return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
|
|
8820
|
+
}
|
|
8821
|
+
function readSync2(moov, stbl, sampleCount) {
|
|
8822
|
+
const stss = findChild(moov, stbl, "stss");
|
|
8823
|
+
if (!stss) return null;
|
|
8824
|
+
const sync = Array(sampleCount).fill(false);
|
|
8825
|
+
for (const [n] of rows(moov, stss, 1)) sync[n - 1] = true;
|
|
8826
|
+
return sync;
|
|
8827
|
+
}
|
|
8828
|
+
function readChunkOffsets(moov, stbl) {
|
|
8829
|
+
const stco = findChild(moov, stbl, "stco");
|
|
8830
|
+
if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
|
|
8831
|
+
return rows(moov, child(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
|
|
8832
|
+
}
|
|
8833
|
+
function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
8834
|
+
const stsc = rows(moov, child(moov, stbl, "stsc"), 3);
|
|
8835
|
+
return stsc.flatMap(([firstChunk, perChunk, description], i) => {
|
|
8836
|
+
if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
|
|
8837
|
+
const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
|
|
8838
|
+
return Array(nextFirst - firstChunk).fill(perChunk);
|
|
8839
|
+
});
|
|
8840
|
+
}
|
|
8841
|
+
function readTrack(moov, trak) {
|
|
8842
|
+
const mdia = child(moov, trak, "mdia");
|
|
8843
|
+
const hdlr = child(moov, mdia, "hdlr");
|
|
8844
|
+
const stbl = child(moov, child(moov, mdia, "minf"), "stbl");
|
|
8845
|
+
if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
|
|
8846
|
+
const stts = rows(moov, child(moov, stbl, "stts"), 2);
|
|
8847
|
+
const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
|
|
8848
|
+
const sizes = readSizes(moov, child(moov, stbl, "stsz"));
|
|
8849
|
+
const chunkOffsets = readChunkOffsets(moov, stbl);
|
|
8850
|
+
const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
|
|
8851
|
+
if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
|
|
8852
|
+
throw new Error("[mp4Stitch] sample tables disagree on the sample count");
|
|
8853
|
+
}
|
|
8854
|
+
const stsd = child(moov, stbl, "stsd");
|
|
8855
|
+
return {
|
|
8856
|
+
trak,
|
|
8857
|
+
handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
|
|
8858
|
+
timescale: timescaleOf(moov, child(moov, mdia, "mdhd")),
|
|
8859
|
+
mediaTime: readMediaTime(moov, trak),
|
|
8860
|
+
stsd: moov.subarray(stsd.start, stsd.end),
|
|
8861
|
+
durations,
|
|
8862
|
+
sizes,
|
|
8863
|
+
sync: readSync2(moov, stbl, sizes.length),
|
|
8864
|
+
samplesPerChunk,
|
|
8865
|
+
chunkOffsets
|
|
8866
|
+
};
|
|
8867
|
+
}
|
|
8868
|
+
function sum(values) {
|
|
8869
|
+
return values.reduce((total, value) => total + value, 0);
|
|
8870
|
+
}
|
|
8871
|
+
function box(type, ...body) {
|
|
8872
|
+
const header = Buffer.alloc(8);
|
|
8873
|
+
header.writeUInt32BE(8 + sum(body.map((part) => part.length)));
|
|
8874
|
+
header.write(type, 4, "latin1");
|
|
8875
|
+
return Buffer.concat([header, ...body]);
|
|
8876
|
+
}
|
|
8877
|
+
function fullBox(type, version, body) {
|
|
8878
|
+
return box(type, Buffer.from([version, 0, 0, 0]), body);
|
|
8879
|
+
}
|
|
8880
|
+
function u32s(values) {
|
|
8881
|
+
const buf = Buffer.alloc(values.length * 4);
|
|
8882
|
+
values.forEach((value, i) => buf.writeUInt32BE(value, i * 4));
|
|
8883
|
+
return buf;
|
|
8884
|
+
}
|
|
8885
|
+
function runs(values) {
|
|
8886
|
+
const out = [];
|
|
8887
|
+
for (const value of values) {
|
|
8888
|
+
const last = out.at(-1);
|
|
8889
|
+
if (last && last[1] === value) last[0]++;
|
|
8890
|
+
else out.push([1, value]);
|
|
8891
|
+
}
|
|
8892
|
+
return out;
|
|
8893
|
+
}
|
|
8894
|
+
function rebuild(buf, parent, replace) {
|
|
8895
|
+
const parts = childBoxes(buf, parent).map(
|
|
8896
|
+
(box2) => replace[box2.type]?.(box2) ?? buf.subarray(box2.start, box2.end)
|
|
8897
|
+
);
|
|
8898
|
+
return box(parent.type, ...parts);
|
|
8899
|
+
}
|
|
8900
|
+
var DURATION_AT = {
|
|
8901
|
+
mvhd: [16, 24],
|
|
8902
|
+
mdhd: [16, 24],
|
|
8903
|
+
tkhd: [20, 28]
|
|
8904
|
+
};
|
|
8905
|
+
function withDuration(buf, target, duration) {
|
|
8906
|
+
const copy = Buffer.from(buf.subarray(target.start, target.end));
|
|
8907
|
+
const body = target.bodyStart - target.start;
|
|
8908
|
+
const wide = copy[body] === 1;
|
|
8909
|
+
const at = body + DURATION_AT[target.type][wide ? 1 : 0];
|
|
8910
|
+
if (wide) copy.writeBigUInt64BE(BigInt(duration), at);
|
|
8911
|
+
else if (duration <= 4294967295) copy.writeUInt32BE(duration, at);
|
|
8912
|
+
else throw new Error(`[mp4Stitch] ${target.type} duration ${duration} needs a version 1 box`);
|
|
8913
|
+
return copy;
|
|
8914
|
+
}
|
|
8915
|
+
function timescaleOf(buf, header) {
|
|
8916
|
+
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
8917
|
+
}
|
|
8918
|
+
function buildStbl(stsd, track) {
|
|
8919
|
+
const stts = runs(track.durations);
|
|
8920
|
+
const stsc = [];
|
|
8921
|
+
let firstChunk = 1;
|
|
8922
|
+
for (const [count, perChunk] of runs(track.samplesPerChunk)) {
|
|
8923
|
+
stsc.push([firstChunk, perChunk, 1]);
|
|
8924
|
+
firstChunk += count;
|
|
8925
|
+
}
|
|
8926
|
+
const co64 = Buffer.alloc(4 + track.chunkOffsets.length * 8);
|
|
8927
|
+
co64.writeUInt32BE(track.chunkOffsets.length);
|
|
8928
|
+
track.chunkOffsets.forEach((offset, i) => co64.writeBigUInt64BE(BigInt(offset), 4 + i * 8));
|
|
8929
|
+
const syncSamples = track.sync?.flatMap((isSync, i) => isSync ? [i + 1] : []) ?? null;
|
|
8930
|
+
return box(
|
|
8931
|
+
"stbl",
|
|
8932
|
+
stsd,
|
|
8933
|
+
fullBox("stts", 0, u32s([stts.length, ...stts.flat()])),
|
|
8934
|
+
...syncSamples ? [fullBox("stss", 0, u32s([syncSamples.length, ...syncSamples]))] : [],
|
|
8935
|
+
fullBox("stsc", 0, u32s([stsc.length, ...stsc.flat()])),
|
|
8936
|
+
fullBox("stsz", 0, u32s([0, track.sizes.length, ...track.sizes])),
|
|
8937
|
+
fullBox("co64", 0, co64)
|
|
8938
|
+
);
|
|
8939
|
+
}
|
|
8940
|
+
var codecOf = (track) => track.stsd.toString("latin1", 20, 24);
|
|
8941
|
+
var sameTrack = (track, plan2) => track.handler === plan2.handler && track.timescale === plan2.timescale && codecOf(track) === codecOf(plan2);
|
|
8942
|
+
function planTracksOf(tracks, plan2, index) {
|
|
8943
|
+
let next = 0;
|
|
8944
|
+
const matched = plan2.map((planTrack) => {
|
|
8945
|
+
const track = tracks[next];
|
|
8946
|
+
if (track && sameTrack(track, planTrack)) return tracks[next++];
|
|
8947
|
+
if (planTrack.handler === "soun") return null;
|
|
8948
|
+
throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
|
|
8949
|
+
});
|
|
8950
|
+
if (next !== tracks.length)
|
|
8951
|
+
throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
|
|
8952
|
+
for (const track of tracks) {
|
|
8953
|
+
if (track.handler === "vide" && track.mediaTime !== 0) {
|
|
8954
|
+
throw new Error(`[mp4Stitch] chunk ${index} video starts at media time ${track.mediaTime}`);
|
|
8955
|
+
}
|
|
8956
|
+
}
|
|
8957
|
+
return matched;
|
|
8958
|
+
}
|
|
8959
|
+
function buildStitchedHeader(layouts) {
|
|
8960
|
+
const plan2 = readTracks(layouts[0].moov);
|
|
8961
|
+
const chunks = layouts.map((layout, i) => planTracksOf(readTracks(layout.moov), plan2, i));
|
|
8962
|
+
const payloadBases = [];
|
|
8963
|
+
let payloadBytes = 0;
|
|
8964
|
+
for (const layout of layouts) {
|
|
8965
|
+
payloadBases.push(payloadBytes);
|
|
8966
|
+
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
8967
|
+
}
|
|
8968
|
+
const template = layouts[0].moov;
|
|
8969
|
+
const root = boxAt(template, 0, 0, template.length);
|
|
8970
|
+
const movieTimescale = timescaleOf(template, child(template, root, "mvhd"));
|
|
8971
|
+
const buildMoov = (dataStart2) => {
|
|
8972
|
+
const traks = plan2.map((track, t) => {
|
|
8973
|
+
const parts = chunks.flatMap((tracks, chunk) => {
|
|
8974
|
+
const part = tracks[t];
|
|
8975
|
+
return part ? [{ ...part, chunk }] : [];
|
|
8976
|
+
});
|
|
8977
|
+
const merged = {
|
|
8978
|
+
durations: parts.flatMap((part) => part.durations),
|
|
8979
|
+
sizes: parts.flatMap((part) => part.sizes),
|
|
8980
|
+
sync: parts.every((part) => !part.sync) ? null : parts.flatMap((part) => part.sync ?? part.sizes.map(() => true)),
|
|
8981
|
+
samplesPerChunk: parts.flatMap((part) => part.samplesPerChunk),
|
|
8982
|
+
chunkOffsets: parts.flatMap(
|
|
8983
|
+
(part) => part.chunkOffsets.map((offset) => {
|
|
8984
|
+
const i = part.chunk;
|
|
8985
|
+
const { payloadStart, payloadEnd } = layouts[i];
|
|
8986
|
+
if (offset < payloadStart || offset >= payloadEnd) {
|
|
8987
|
+
throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
|
|
8988
|
+
}
|
|
8989
|
+
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
8990
|
+
})
|
|
8991
|
+
)
|
|
8992
|
+
};
|
|
8993
|
+
const mediaDuration = sum(merged.durations);
|
|
8994
|
+
const shown = Math.round(
|
|
8995
|
+
(mediaDuration - track.mediaTime) * movieTimescale / track.timescale
|
|
8996
|
+
);
|
|
8997
|
+
const elst = Buffer.alloc(24);
|
|
8998
|
+
elst.writeUInt32BE(1);
|
|
8999
|
+
elst.writeBigUInt64BE(BigInt(shown), 4);
|
|
9000
|
+
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
9001
|
+
elst.writeUInt32BE(65536, 20);
|
|
9002
|
+
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
9003
|
+
const trak = rebuild(template, track.trak, {
|
|
9004
|
+
tkhd: (tkhd) => Buffer.concat([withDuration(template, tkhd, shown), edts]),
|
|
9005
|
+
edts: () => Buffer.alloc(0),
|
|
9006
|
+
mdia: (mdia) => rebuild(template, mdia, {
|
|
9007
|
+
mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
|
|
9008
|
+
minf: (minf) => rebuild(template, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
9009
|
+
})
|
|
9010
|
+
});
|
|
9011
|
+
return { trak, shown };
|
|
9012
|
+
});
|
|
9013
|
+
let next = 0;
|
|
9014
|
+
return rebuild(template, root, {
|
|
9015
|
+
mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
|
|
9016
|
+
trak: () => traks[next++].trak
|
|
9017
|
+
});
|
|
9018
|
+
};
|
|
9019
|
+
const ftyp = layouts[0].ftyp;
|
|
9020
|
+
const dataStart = ftyp.length + buildMoov(0).length + 16;
|
|
9021
|
+
const mdat = Buffer.alloc(16);
|
|
9022
|
+
mdat.writeUInt32BE(1);
|
|
9023
|
+
mdat.write("mdat", 4, "latin1");
|
|
9024
|
+
mdat.writeBigUInt64BE(BigInt(16 + payloadBytes), 8);
|
|
9025
|
+
return Buffer.concat([ftyp, buildMoov(dataStart), mdat]);
|
|
9026
|
+
}
|
|
9027
|
+
function fileRange(path) {
|
|
9028
|
+
return async (start, end) => {
|
|
9029
|
+
const file = await open(path, "r", 384);
|
|
9030
|
+
try {
|
|
9031
|
+
const buf = Buffer.alloc(end - start);
|
|
9032
|
+
const { bytesRead } = await file.read(buf, 0, buf.length, start);
|
|
9033
|
+
return buf.subarray(0, bytesRead);
|
|
9034
|
+
} finally {
|
|
9035
|
+
await file.close();
|
|
9036
|
+
}
|
|
9037
|
+
};
|
|
9038
|
+
}
|
|
9039
|
+
async function copyPayload(path, layout, index, out) {
|
|
9040
|
+
const { payloadStart, payloadEnd } = layout;
|
|
9041
|
+
const payload = createReadStream4(path, { start: payloadStart, end: payloadEnd - 1 });
|
|
9042
|
+
await pipeline(payload, out, { end: false });
|
|
9043
|
+
if (payload.bytesRead !== payloadEnd - payloadStart) {
|
|
9044
|
+
throw new Error(
|
|
9045
|
+
`[mp4Stitch] chunk ${index} mdat holds ${payload.bytesRead} of ${payloadEnd - payloadStart} payload bytes`
|
|
9046
|
+
);
|
|
9047
|
+
}
|
|
9048
|
+
}
|
|
9049
|
+
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
9050
|
+
const layouts = await Promise.all(chunkPaths.map((path) => readMp4Layout(fileRange(path))));
|
|
9051
|
+
const header = buildStitchedHeader(layouts);
|
|
9052
|
+
mkdirSync11(dirname11(outputPath), { recursive: true });
|
|
9053
|
+
const workDir = mkdtempSync5(`${outputPath}.stitch-`);
|
|
9054
|
+
const out = createWriteStream(join22(workDir, "out.mp4"));
|
|
9055
|
+
try {
|
|
9056
|
+
out.write(header);
|
|
9057
|
+
for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
9058
|
+
out.end();
|
|
9059
|
+
await finished(out);
|
|
9060
|
+
renameSync6(join22(workDir, "out.mp4"), outputPath);
|
|
9061
|
+
} finally {
|
|
9062
|
+
out.destroy();
|
|
9063
|
+
rmSync10(workDir, { recursive: true, force: true });
|
|
9064
|
+
}
|
|
9065
|
+
}
|
|
9066
|
+
|
|
9067
|
+
// src/services/distributed/assemble.ts
|
|
8720
9068
|
function cfrRateControlArgs(locked) {
|
|
8721
9069
|
const preset = getEncoderPreset5(locked.quality ?? "standard", "mp4");
|
|
8722
9070
|
if (locked.crf == null && locked.bitrate) {
|
|
@@ -8732,12 +9080,21 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
8732
9080
|
if (plan2.dimensions.format === "png-sequence") {
|
|
8733
9081
|
return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
|
|
8734
9082
|
}
|
|
9083
|
+
if (chunksAreStitchedMp4(planDir, cfr)) {
|
|
9084
|
+
await writeStitchedMp4(chunkPaths, outputPath);
|
|
9085
|
+
return {
|
|
9086
|
+
outputPath,
|
|
9087
|
+
durationMs: Date.now() - start,
|
|
9088
|
+
framesEncoded: plan2.totalFrames,
|
|
9089
|
+
fileSize: statSync7(outputPath).size
|
|
9090
|
+
};
|
|
9091
|
+
}
|
|
8735
9092
|
const format = plan2.dimensions.format;
|
|
8736
9093
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
8737
|
-
if (!existsSync16(
|
|
8738
|
-
|
|
9094
|
+
if (!existsSync16(dirname12(outputPath))) {
|
|
9095
|
+
mkdirSync12(dirname12(outputPath), { recursive: true });
|
|
8739
9096
|
}
|
|
8740
|
-
const workDir =
|
|
9097
|
+
const workDir = mkdtempSync6(`${outputPath}.assemble-work-`);
|
|
8741
9098
|
const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
8742
9099
|
const stageMs = {};
|
|
8743
9100
|
const ctx = {
|
|
@@ -8745,7 +9102,6 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
8745
9102
|
format,
|
|
8746
9103
|
fps,
|
|
8747
9104
|
fpsArg: fpsToFfmpegArg3(fps),
|
|
8748
|
-
chunkSeconds: chunksCarryAudio(planDir) ? plan2.chunks.map((chunk) => (chunk.endFrame - chunk.startFrame) * fps.den / fps.num) : null,
|
|
8749
9105
|
signal: options?.abortSignal,
|
|
8750
9106
|
timeout: resolveConfig4().ffmpegProcessTimeout,
|
|
8751
9107
|
log,
|
|
@@ -8758,10 +9114,10 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
8758
9114
|
};
|
|
8759
9115
|
try {
|
|
8760
9116
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
8761
|
-
const finalPath =
|
|
9117
|
+
const finalPath = join23(workDir, `output.${format}`);
|
|
8762
9118
|
const audio = audioPath !== null && existsSync16(audioPath) ? audioPath : null;
|
|
8763
9119
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
8764
|
-
|
|
9120
|
+
renameSync7(finalPath, outputPath);
|
|
8765
9121
|
log.info("[assemble] stage timings", {
|
|
8766
9122
|
...stageMs,
|
|
8767
9123
|
chunks: chunkPaths.length,
|
|
@@ -8769,7 +9125,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
8769
9125
|
});
|
|
8770
9126
|
} finally {
|
|
8771
9127
|
try {
|
|
8772
|
-
|
|
9128
|
+
rmSync11(workDir, { recursive: true, force: true });
|
|
8773
9129
|
} catch (err) {
|
|
8774
9130
|
log.warn("[assemble] failed to remove work dir", {
|
|
8775
9131
|
workDir,
|
|
@@ -8786,8 +9142,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
8786
9142
|
};
|
|
8787
9143
|
}
|
|
8788
9144
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
8789
|
-
const planJsonPath =
|
|
8790
|
-
const chunksJsonPath =
|
|
9145
|
+
const planJsonPath = join23(planDir, "plan.json");
|
|
9146
|
+
const chunksJsonPath = join23(planDir, "meta", "chunks.json");
|
|
8791
9147
|
if (!existsSync16(planJsonPath)) {
|
|
8792
9148
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
8793
9149
|
}
|
|
@@ -8808,13 +9164,13 @@ function readAssemblePlan(planDir, chunkPaths) {
|
|
|
8808
9164
|
throw new Error(`[assemble] chunk path does not exist: ${path}`);
|
|
8809
9165
|
}
|
|
8810
9166
|
}
|
|
8811
|
-
return
|
|
9167
|
+
return plan2;
|
|
8812
9168
|
}
|
|
8813
9169
|
function videoInputArgs(ctx, videoSources) {
|
|
8814
|
-
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg
|
|
9170
|
+
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
8815
9171
|
}
|
|
8816
9172
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
8817
|
-
const cfrOutputPath =
|
|
9173
|
+
const cfrOutputPath = join23(ctx.workDir, `cfr.${ctx.format}`);
|
|
8818
9174
|
const cfrArgs = [
|
|
8819
9175
|
...videoInputArgs(ctx, chunkPaths),
|
|
8820
9176
|
"-c:v",
|
|
@@ -8852,7 +9208,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
8852
9208
|
);
|
|
8853
9209
|
return;
|
|
8854
9210
|
}
|
|
8855
|
-
const paddedAudioPath =
|
|
9211
|
+
const paddedAudioPath = join23(ctx.workDir, "audio-padded.m4a");
|
|
8856
9212
|
const padTrimResult = await ctx.timed(
|
|
8857
9213
|
"audioMs",
|
|
8858
9214
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -8891,15 +9247,10 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
8891
9247
|
throw encoderFailureError("[assemble] audio mux failed", muxResult);
|
|
8892
9248
|
}
|
|
8893
9249
|
}
|
|
8894
|
-
function concatDemuxerInput(chunkPaths, workDir, fpsArg
|
|
8895
|
-
const listPath =
|
|
8896
|
-
const
|
|
8897
|
-
|
|
8898
|
-
const line = `file '${path.replace(/'/g, "'\\''")}'`;
|
|
8899
|
-
return chunkSeconds ? `${line}
|
|
8900
|
-
duration ${chunkSeconds[i].toFixed(6)}` : line;
|
|
8901
|
-
});
|
|
8902
|
-
writeFileSync8(listPath, `${[...head, ...body].join("\n")}
|
|
9250
|
+
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
9251
|
+
const listPath = join23(workDir, "concat-list.txt");
|
|
9252
|
+
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
9253
|
+
writeFileSync8(listPath, `${body}
|
|
8903
9254
|
`, "utf-8");
|
|
8904
9255
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
8905
9256
|
}
|
|
@@ -8908,7 +9259,7 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
8908
9259
|
videoPaths.map((path) => defaultProbeVideoFrameInfo(path, signal))
|
|
8909
9260
|
);
|
|
8910
9261
|
return {
|
|
8911
|
-
frameCount: infos.reduce((
|
|
9262
|
+
frameCount: infos.reduce((sum2, info) => sum2 + info.frameCount, 0),
|
|
8912
9263
|
fpsNum: fps.num,
|
|
8913
9264
|
fpsDen: fps.den
|
|
8914
9265
|
};
|
|
@@ -8922,8 +9273,15 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
8922
9273
|
});
|
|
8923
9274
|
}
|
|
8924
9275
|
}
|
|
9276
|
+
function chunksAreStitchedMp4(planDir, cfr) {
|
|
9277
|
+
if (!chunksCarryAudio(planDir)) return false;
|
|
9278
|
+
if (cfr) {
|
|
9279
|
+
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks carry audio");
|
|
9280
|
+
}
|
|
9281
|
+
return true;
|
|
9282
|
+
}
|
|
8925
9283
|
function chunksCarryAudio(planDir) {
|
|
8926
|
-
const encoderJsonPath =
|
|
9284
|
+
const encoderJsonPath = join23(planDir, "meta", "encoder.json");
|
|
8927
9285
|
if (!existsSync16(encoderJsonPath)) return false;
|
|
8928
9286
|
return JSON.parse(readFileSync10(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
|
|
8929
9287
|
}
|
|
@@ -8933,7 +9291,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
8933
9291
|
`[assemble] cfr=true is only supported for format="mp4" (got "${format}"). Stream-copy paths for webm and mov already produce exact avg_frame_rate; cfr re-encode is not needed.`
|
|
8934
9292
|
);
|
|
8935
9293
|
}
|
|
8936
|
-
const encoderJsonPath =
|
|
9294
|
+
const encoderJsonPath = join23(planDir, "meta", "encoder.json");
|
|
8937
9295
|
if (!existsSync16(encoderJsonPath)) {
|
|
8938
9296
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
8939
9297
|
}
|
|
@@ -8946,8 +9304,8 @@ function readCfrEncoder(planDir, format) {
|
|
|
8946
9304
|
return encoderJson;
|
|
8947
9305
|
}
|
|
8948
9306
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
8949
|
-
if (existsSync16(outputPath))
|
|
8950
|
-
|
|
9307
|
+
if (existsSync16(outputPath)) rmSync11(outputPath, { recursive: true, force: true });
|
|
9308
|
+
mkdirSync12(outputPath, { recursive: true });
|
|
8951
9309
|
let globalIdx = 0;
|
|
8952
9310
|
for (const chunkDir of chunkPaths) {
|
|
8953
9311
|
if (!statSync7(chunkDir).isDirectory()) {
|
|
@@ -8960,8 +9318,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
8960
9318
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
8961
9319
|
}
|
|
8962
9320
|
for (const frame of frames) {
|
|
8963
|
-
const dst =
|
|
8964
|
-
cpSync3(
|
|
9321
|
+
const dst = join23(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
9322
|
+
cpSync3(join23(chunkDir, frame), dst);
|
|
8965
9323
|
globalIdx += 1;
|
|
8966
9324
|
}
|
|
8967
9325
|
}
|
|
@@ -8971,13 +9329,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
8971
9329
|
);
|
|
8972
9330
|
}
|
|
8973
9331
|
if (audioPath !== null && existsSync16(audioPath)) {
|
|
8974
|
-
const sidecar =
|
|
9332
|
+
const sidecar = join23(outputPath, MIXED_AUDIO_FILENAME4);
|
|
8975
9333
|
cpSync3(audioPath, sidecar);
|
|
8976
9334
|
}
|
|
8977
9335
|
let fileSize = 0;
|
|
8978
9336
|
for (const name of readdirSync7(outputPath)) {
|
|
8979
9337
|
try {
|
|
8980
|
-
fileSize += statSync7(
|
|
9338
|
+
fileSize += statSync7(join23(outputPath, name)).size;
|
|
8981
9339
|
} catch {
|
|
8982
9340
|
}
|
|
8983
9341
|
}
|
|
@@ -8991,8 +9349,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
8991
9349
|
|
|
8992
9350
|
// src/services/distributed/renderChunk.ts
|
|
8993
9351
|
import { randomBytes } from "node:crypto";
|
|
8994
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
8995
|
-
import { extname as extname4, join as
|
|
9352
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync13, readFileSync as readFileSync11, readdirSync as readdirSync8, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
9353
|
+
import { extname as extname4, join as join24 } from "node:path";
|
|
8996
9354
|
import {
|
|
8997
9355
|
assertSwiftShader,
|
|
8998
9356
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE3,
|
|
@@ -9198,7 +9556,7 @@ function frameNumberFromFileName(name) {
|
|
|
9198
9556
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
9199
9557
|
const result = [];
|
|
9200
9558
|
for (const v of videos) {
|
|
9201
|
-
const outputDir =
|
|
9559
|
+
const outputDir = join24(planDir, "video-frames", v.videoId);
|
|
9202
9560
|
if (!existsSync17(outputDir)) {
|
|
9203
9561
|
throw new Error(
|
|
9204
9562
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
@@ -9217,7 +9575,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
9217
9575
|
if (!frameName) continue;
|
|
9218
9576
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
9219
9577
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
9220
|
-
framePaths.set(frameIndex,
|
|
9578
|
+
framePaths.set(frameIndex, join24(outputDir, frameName));
|
|
9221
9579
|
}
|
|
9222
9580
|
result.push({
|
|
9223
9581
|
videoId: v.videoId,
|
|
@@ -9239,13 +9597,13 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
9239
9597
|
return result;
|
|
9240
9598
|
}
|
|
9241
9599
|
function contiguousRuns(sortedIndexes) {
|
|
9242
|
-
const
|
|
9600
|
+
const runs2 = [];
|
|
9243
9601
|
for (const index of sortedIndexes) {
|
|
9244
|
-
const last =
|
|
9602
|
+
const last = runs2[runs2.length - 1];
|
|
9245
9603
|
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
9246
|
-
else
|
|
9604
|
+
else runs2.push({ firstFrame: index, frames: 1 });
|
|
9247
9605
|
}
|
|
9248
|
-
return
|
|
9606
|
+
return runs2;
|
|
9249
9607
|
}
|
|
9250
9608
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
9251
9609
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
@@ -9270,13 +9628,13 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
9270
9628
|
}
|
|
9271
9629
|
return Promise.all(
|
|
9272
9630
|
deferred.map(async (video) => {
|
|
9273
|
-
const outputDir =
|
|
9631
|
+
const outputDir = join24(input.outputRoot, video.videoId);
|
|
9274
9632
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
9275
9633
|
if (indexes.length === 0) {
|
|
9276
9634
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
9277
9635
|
}
|
|
9278
9636
|
const range = video.deferredRange;
|
|
9279
|
-
const sourcePath =
|
|
9637
|
+
const sourcePath = join24(input.planDir, ...range.sourcePath.split("/"));
|
|
9280
9638
|
if (!existsSync17(sourcePath)) {
|
|
9281
9639
|
throw new RenderChunkValidationError(
|
|
9282
9640
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -9312,7 +9670,7 @@ function hashChunkOutput(outputPath, kind) {
|
|
|
9312
9670
|
if (kind === "file") return sha256Hex(readFileSync11(outputPath));
|
|
9313
9671
|
const entries = readdirSync8(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
9314
9672
|
const lines = entries.map(
|
|
9315
|
-
(name) => `${name}\0${sha256Hex(readFileSync11(
|
|
9673
|
+
(name) => `${name}\0${sha256Hex(readFileSync11(join24(outputPath, name)))}`
|
|
9316
9674
|
);
|
|
9317
9675
|
return sha256Hex(lines.join("\0"));
|
|
9318
9676
|
}
|
|
@@ -9329,9 +9687,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
9329
9687
|
async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
9330
9688
|
const start = Date.now();
|
|
9331
9689
|
const log = defaultLogger;
|
|
9332
|
-
const planJsonPath =
|
|
9333
|
-
const encoderJsonPath =
|
|
9334
|
-
const chunksJsonPath =
|
|
9690
|
+
const planJsonPath = join24(planDir, "plan.json");
|
|
9691
|
+
const encoderJsonPath = join24(planDir, "meta", "encoder.json");
|
|
9692
|
+
const chunksJsonPath = join24(planDir, "meta", "chunks.json");
|
|
9335
9693
|
if (!existsSync17(planJsonPath)) {
|
|
9336
9694
|
throw new RenderChunkValidationError(
|
|
9337
9695
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -9355,7 +9713,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9355
9713
|
}
|
|
9356
9714
|
const encoder = JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
9357
9715
|
const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
|
|
9358
|
-
const videosJsonPath =
|
|
9716
|
+
const videosJsonPath = join24(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9359
9717
|
let planVideos = null;
|
|
9360
9718
|
if (existsSync17(videosJsonPath)) {
|
|
9361
9719
|
try {
|
|
@@ -9385,7 +9743,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9385
9743
|
`[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
|
|
9386
9744
|
);
|
|
9387
9745
|
}
|
|
9388
|
-
const compiledDir =
|
|
9746
|
+
const compiledDir = join24(planDir, "compiled");
|
|
9389
9747
|
if (!existsSync17(compiledDir)) {
|
|
9390
9748
|
throw new RenderChunkValidationError(
|
|
9391
9749
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -9442,9 +9800,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9442
9800
|
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
|
|
9443
9801
|
};
|
|
9444
9802
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
9445
|
-
|
|
9446
|
-
const framesDir =
|
|
9447
|
-
|
|
9803
|
+
mkdirSync13(workDir, { recursive: true });
|
|
9804
|
+
const framesDir = join24(workDir, "captured-frames");
|
|
9805
|
+
mkdirSync13(framesDir, { recursive: true });
|
|
9448
9806
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
9449
9807
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable3(
|
|
9450
9808
|
planVideos.videos,
|
|
@@ -9497,7 +9855,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9497
9855
|
// `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
|
|
9498
9856
|
lockWarmupTicks: true,
|
|
9499
9857
|
requiresWebGpu: compositionRequiresWebGpu3(
|
|
9500
|
-
readFileSync11(
|
|
9858
|
+
readFileSync11(join24(compiledDir, "index.html"), "utf-8")
|
|
9501
9859
|
)
|
|
9502
9860
|
};
|
|
9503
9861
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
@@ -9628,8 +9986,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9628
9986
|
return observedMode;
|
|
9629
9987
|
},
|
|
9630
9988
|
resetForScreenshotRetry: () => {
|
|
9631
|
-
|
|
9632
|
-
|
|
9989
|
+
rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
9990
|
+
mkdirSync13(framesDir, { recursive: true });
|
|
9633
9991
|
capturePerfs.length = 0;
|
|
9634
9992
|
captureAttempts.length = 0;
|
|
9635
9993
|
job.framesRendered = 0;
|
|
@@ -9660,10 +10018,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9660
10018
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
9661
10019
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
9662
10020
|
if (isPngSequence) {
|
|
9663
|
-
if (!existsSync17(outputChunkPath))
|
|
10021
|
+
if (!existsSync17(outputChunkPath)) mkdirSync13(outputChunkPath, { recursive: true });
|
|
9664
10022
|
} else {
|
|
9665
|
-
const outDir =
|
|
9666
|
-
if (!existsSync17(outDir))
|
|
10023
|
+
const outDir = join24(outputChunkPath, "..");
|
|
10024
|
+
if (!existsSync17(outDir)) mkdirSync13(outDir, { recursive: true });
|
|
9667
10025
|
}
|
|
9668
10026
|
const encodeStarted = Date.now();
|
|
9669
10027
|
await runEncodeStage({
|
|
@@ -9715,7 +10073,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9715
10073
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
9716
10074
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
9717
10075
|
});
|
|
9718
|
-
|
|
10076
|
+
rmSync12(videoOnlyPath, { force: true });
|
|
9719
10077
|
}
|
|
9720
10078
|
encodeStageMs = Date.now() - encodeStarted;
|
|
9721
10079
|
} finally {
|
|
@@ -9757,8 +10115,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9757
10115
|
writeFileSync9(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
9758
10116
|
`, "utf-8");
|
|
9759
10117
|
try {
|
|
9760
|
-
|
|
9761
|
-
|
|
10118
|
+
rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10119
|
+
rmSync12(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
9762
10120
|
} catch (err) {
|
|
9763
10121
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
9764
10122
|
workDir,
|
|
@@ -9786,18 +10144,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9786
10144
|
|
|
9787
10145
|
// src/services/distributed/planV2Execution.ts
|
|
9788
10146
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
|
|
9789
|
-
const workRoot =
|
|
9790
|
-
const materializedPlanDir =
|
|
10147
|
+
const workRoot = mkdtempSync7(join25(tmpdir2(), "hf-plan-v2-chunk-"));
|
|
10148
|
+
const materializedPlanDir = join25(workRoot, "plan");
|
|
9791
10149
|
try {
|
|
9792
10150
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
9793
10151
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
|
|
9794
10152
|
} finally {
|
|
9795
|
-
|
|
10153
|
+
rmSync13(workRoot, { recursive: true, force: true });
|
|
9796
10154
|
}
|
|
9797
10155
|
}
|
|
9798
10156
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
9799
|
-
const workRoot =
|
|
9800
|
-
const materializedPlanDir =
|
|
10157
|
+
const workRoot = mkdtempSync7(join25(tmpdir2(), "hf-plan-v2-assembler-"));
|
|
10158
|
+
const materializedPlanDir = join25(workRoot, "plan");
|
|
9801
10159
|
try {
|
|
9802
10160
|
const materialized = materializePlanV2Target(
|
|
9803
10161
|
planV2Dir,
|
|
@@ -9812,7 +10170,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
9812
10170
|
options
|
|
9813
10171
|
);
|
|
9814
10172
|
} finally {
|
|
9815
|
-
|
|
10173
|
+
rmSync13(workRoot, { recursive: true, force: true });
|
|
9816
10174
|
}
|
|
9817
10175
|
}
|
|
9818
10176
|
|
|
@@ -10061,7 +10419,7 @@ function validateIntDimension(field, value) {
|
|
|
10061
10419
|
// src/services/distributed/projectHash.ts
|
|
10062
10420
|
import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "node:fs";
|
|
10063
10421
|
import { createHash as createHash6 } from "node:crypto";
|
|
10064
|
-
import { join as
|
|
10422
|
+
import { join as join26, relative as relative7 } from "node:path";
|
|
10065
10423
|
function hashProjectDir(projectDir) {
|
|
10066
10424
|
const hash = createHash6("sha256");
|
|
10067
10425
|
const files = [];
|
|
@@ -10070,7 +10428,7 @@ function hashProjectDir(projectDir) {
|
|
|
10070
10428
|
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
10071
10429
|
)) {
|
|
10072
10430
|
if (isRoot && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(entry.name)) continue;
|
|
10073
|
-
const full =
|
|
10431
|
+
const full = join26(dir, entry.name);
|
|
10074
10432
|
if (entry.isDirectory()) walk(full, false);
|
|
10075
10433
|
else if (entry.isFile()) files.push(full);
|
|
10076
10434
|
}
|
|
@@ -10127,6 +10485,8 @@ export {
|
|
|
10127
10485
|
assemble,
|
|
10128
10486
|
assembleV2,
|
|
10129
10487
|
buildChunkSlices,
|
|
10488
|
+
buildStitchedHeader,
|
|
10489
|
+
chunksAreStitchedMp4,
|
|
10130
10490
|
createPlanV2FromExecutionPlan,
|
|
10131
10491
|
createPlanV2FromV1,
|
|
10132
10492
|
getDistributedRenderCapabilities,
|
|
@@ -10136,11 +10496,13 @@ export {
|
|
|
10136
10496
|
listPlanV2ArtifactsForTarget,
|
|
10137
10497
|
materializePlanV2Target,
|
|
10138
10498
|
measurePlanDirBytes,
|
|
10499
|
+
muxChunkWithAudio,
|
|
10139
10500
|
plan,
|
|
10140
10501
|
planV2,
|
|
10141
10502
|
planV2WithPublisher,
|
|
10142
10503
|
publishPlanV2FromExecutionPlan,
|
|
10143
10504
|
publishPlanV2FromV1,
|
|
10505
|
+
readMp4Layout,
|
|
10144
10506
|
readPlanProtocol,
|
|
10145
10507
|
readPlanProtocolV1,
|
|
10146
10508
|
readPlanV2Manifest,
|