@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/index.js
CHANGED
|
@@ -193,11 +193,11 @@ var defaultLogger = createConsoleLogger("info");
|
|
|
193
193
|
|
|
194
194
|
// src/services/fileServer.ts
|
|
195
195
|
var readFileAsync = promisify(readFile);
|
|
196
|
-
function isPathInside(
|
|
196
|
+
function isPathInside(child2, parent, options = {}) {
|
|
197
197
|
const { resolveSymlinks = false, pathModule } = options;
|
|
198
198
|
const resolveFn = pathModule?.resolve ?? resolve2;
|
|
199
199
|
const separator = pathModule?.sep ?? sep;
|
|
200
|
-
const resolvedChild = resolveFn(
|
|
200
|
+
const resolvedChild = resolveFn(child2);
|
|
201
201
|
const resolvedParent = resolveFn(parent);
|
|
202
202
|
const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
|
|
203
203
|
const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
|
|
@@ -3217,10 +3217,10 @@ function aggregateDrawElement(perfs, de) {
|
|
|
3217
3217
|
gateReason: gateReasons.length > 0 ? gateReasons.join("|") : void 0,
|
|
3218
3218
|
gpuRenderer: gpuRenderers.length > 0 ? gpuRenderers.join("|") : void 0,
|
|
3219
3219
|
workerEncode: perfs.some((p) => p.deWorkerEncode),
|
|
3220
|
-
verifyArmed: perfs.reduce((
|
|
3220
|
+
verifyArmed: perfs.reduce((sum2, p) => sum2 + (p.deVerifyArmed ?? 0), 0),
|
|
3221
3221
|
verifyChecked: drain?.verifyChecked ?? 0,
|
|
3222
3222
|
verifyMinDb: roundDb(drain?.verifyMinDb),
|
|
3223
|
-
verifyInitMs: perfs.reduce((
|
|
3223
|
+
verifyInitMs: perfs.reduce((sum2, p) => sum2 + (p.deVerifyInitMs ?? 0), 0),
|
|
3224
3224
|
selfVerifyFallback: de.selfVerifyFallback,
|
|
3225
3225
|
fallbackReason: de.fallbackReason,
|
|
3226
3226
|
fallbackFailedDb: roundDb(de.fallbackFailedDb),
|
|
@@ -3229,9 +3229,9 @@ function aggregateDrawElement(perfs, de) {
|
|
|
3229
3229
|
blankSuspects: drain?.blankSuspects ?? 0,
|
|
3230
3230
|
blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
|
|
3231
3231
|
blankRecaptures: drain?.blankRecaptures ?? 0,
|
|
3232
|
-
boundaryFrames: perfs.reduce((
|
|
3233
|
-
ncprFallbacks: perfs.reduce((
|
|
3234
|
-
frameTimeouts: perfs.reduce((
|
|
3232
|
+
boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
|
|
3233
|
+
ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
|
|
3234
|
+
frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
|
|
3235
3235
|
};
|
|
3236
3236
|
}
|
|
3237
3237
|
function maxDefined(values) {
|
|
@@ -3246,9 +3246,9 @@ function aggregateChromeMemory(perfs) {
|
|
|
3246
3246
|
rendererRssPeakMb: maxDefined(sampled.map((p) => p.chromeRendererRssPeakMb)),
|
|
3247
3247
|
// Last samples are per session; summing approximates the whole fleet's
|
|
3248
3248
|
// footprint at the end of capture (workers run concurrently).
|
|
3249
|
-
rssLastMb: sampled.reduce((
|
|
3249
|
+
rssLastMb: sampled.reduce((sum2, p) => sum2 + (p.chromeRssLastMb ?? 0), 0),
|
|
3250
3250
|
gpuProcessSeenLastSample: sampled.some((p) => p.chromeGpuProcessSeenLastSample === true),
|
|
3251
|
-
samples: sampled.reduce((
|
|
3251
|
+
samples: sampled.reduce((sum2, p) => sum2 + (p.chromeMemorySamples ?? 0), 0)
|
|
3252
3252
|
};
|
|
3253
3253
|
}
|
|
3254
3254
|
function aggregateDedup(perfs) {
|
|
@@ -3266,45 +3266,45 @@ function aggregateDedup(perfs) {
|
|
|
3266
3266
|
return {
|
|
3267
3267
|
enabled: perfs.some((p) => p.staticDedupEnabled),
|
|
3268
3268
|
armed,
|
|
3269
|
-
predictedFrames: perfs.reduce((
|
|
3270
|
-
reusedFrames: perfs.reduce((
|
|
3269
|
+
predictedFrames: perfs.reduce((sum2, p) => sum2 + (p.staticDedupPredicted ?? 0), 0),
|
|
3270
|
+
reusedFrames: perfs.reduce((sum2, p) => sum2 + (p.staticDedupReused ?? 0), 0),
|
|
3271
3271
|
skipReason: skipReasons.length > 0 ? skipReasons.join("|") : void 0,
|
|
3272
3272
|
...verificationPerfs.length === 0 ? {} : {
|
|
3273
3273
|
verifiedFrames: verificationPerfs.reduce(
|
|
3274
|
-
(
|
|
3274
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerified ?? 0),
|
|
3275
3275
|
0
|
|
3276
3276
|
),
|
|
3277
3277
|
verificationOutcomes,
|
|
3278
3278
|
plannedRuns: verificationPerfs.reduce(
|
|
3279
|
-
(
|
|
3279
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerificationPlannedRuns ?? 0),
|
|
3280
3280
|
0
|
|
3281
3281
|
),
|
|
3282
3282
|
completedRuns: verificationPerfs.reduce(
|
|
3283
|
-
(
|
|
3283
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerificationCompletedRuns ?? 0),
|
|
3284
3284
|
0
|
|
3285
3285
|
),
|
|
3286
3286
|
screenshots: verificationPerfs.reduce(
|
|
3287
|
-
(
|
|
3287
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerificationScreenshots ?? 0),
|
|
3288
3288
|
0
|
|
3289
3289
|
),
|
|
3290
3290
|
seeks: verificationPerfs.reduce(
|
|
3291
|
-
(
|
|
3291
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerificationSeeks ?? 0),
|
|
3292
3292
|
0
|
|
3293
3293
|
),
|
|
3294
3294
|
comparisons: verificationPerfs.reduce(
|
|
3295
|
-
(
|
|
3295
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerificationComparisons ?? 0),
|
|
3296
3296
|
0
|
|
3297
3297
|
),
|
|
3298
3298
|
verificationElapsedMs: verificationPerfs.reduce(
|
|
3299
|
-
(
|
|
3299
|
+
(sum2, perf) => sum2 + (perf.staticDedupVerificationElapsedMs ?? 0),
|
|
3300
3300
|
0
|
|
3301
3301
|
)
|
|
3302
3302
|
}
|
|
3303
3303
|
};
|
|
3304
3304
|
}
|
|
3305
3305
|
function aggregateBeginFrameReuse(perfs) {
|
|
3306
|
-
const noDamageFrames = perfs.reduce((
|
|
3307
|
-
const hasDamageFrames = perfs.reduce((
|
|
3306
|
+
const noDamageFrames = perfs.reduce((sum2, p) => sum2 + (p.beginFrameNoDamage ?? 0), 0);
|
|
3307
|
+
const hasDamageFrames = perfs.reduce((sum2, p) => sum2 + (p.beginFrameHasDamage ?? 0), 0);
|
|
3308
3308
|
if (noDamageFrames + hasDamageFrames === 0) return void 0;
|
|
3309
3309
|
return { noDamageFrames, hasDamageFrames };
|
|
3310
3310
|
}
|
|
@@ -6923,7 +6923,7 @@ async function embedLocalFontFaces(html, projectDir) {
|
|
|
6923
6923
|
embeddedPaths.add(localPath);
|
|
6924
6924
|
continue;
|
|
6925
6925
|
}
|
|
6926
|
-
dataUri = await toDataUri(font.buffer, ext);
|
|
6926
|
+
dataUri = await toDataUri(font.buffer, ext, { compressToWoff2: false });
|
|
6927
6927
|
dataUriByAbsolutePath.set(absPath, dataUri);
|
|
6928
6928
|
defaultLogger.info(
|
|
6929
6929
|
`[Compiler] Embedded local font file: ${localPath} (${(font.buffer.length / 1024).toFixed(0)} KB \u2192 data URI)`
|
|
@@ -8366,7 +8366,7 @@ var VideoExtractionStageError = class extends Error {
|
|
|
8366
8366
|
affectedElementCount: failure.count
|
|
8367
8367
|
}))
|
|
8368
8368
|
)) {
|
|
8369
|
-
const total = failures.reduce((
|
|
8369
|
+
const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
|
|
8370
8370
|
const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
|
|
8371
8371
|
super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
|
|
8372
8372
|
this.code = code;
|
|
@@ -10567,6 +10567,16 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10567
10567
|
...loweredDb > 0 ? { audioLoweredDb: loweredDb } : {}
|
|
10568
10568
|
};
|
|
10569
10569
|
}
|
|
10570
|
+
async function constrainAacTruePeak(input) {
|
|
10571
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
10572
|
+
return enforceAacTruePeak({
|
|
10573
|
+
audioPath: input.audioPath,
|
|
10574
|
+
targetDurationSeconds: input.targetDurationSeconds,
|
|
10575
|
+
signal: input.signal,
|
|
10576
|
+
probeTruePeak: (audioPath, signal) => defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal),
|
|
10577
|
+
runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
|
|
10578
|
+
});
|
|
10579
|
+
}
|
|
10570
10580
|
async function enforceAacTruePeak(input) {
|
|
10571
10581
|
let scratchDir;
|
|
10572
10582
|
try {
|
|
@@ -11161,7 +11171,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
|
11161
11171
|
return captured;
|
|
11162
11172
|
}
|
|
11163
11173
|
function countFrameRanges(ranges) {
|
|
11164
|
-
return ranges.reduce((
|
|
11174
|
+
return ranges.reduce((sum2, range) => sum2 + (range.endFrame - range.startFrame), 0);
|
|
11165
11175
|
}
|
|
11166
11176
|
async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
11167
11177
|
const attempts = [];
|
|
@@ -11412,7 +11422,7 @@ function scanElementTags(html) {
|
|
|
11412
11422
|
const byTag = Object.fromEntries(
|
|
11413
11423
|
ranked.slice(0, MAX_REPORTED_ELEMENT_TAGS)
|
|
11414
11424
|
);
|
|
11415
|
-
const otherCount = ranked.slice(MAX_REPORTED_ELEMENT_TAGS).reduce((
|
|
11425
|
+
const otherCount = ranked.slice(MAX_REPORTED_ELEMENT_TAGS).reduce((sum2, [, count]) => sum2 + count, 0);
|
|
11416
11426
|
if (otherCount > 0) byTag.other = (byTag.other ?? 0) + otherCount;
|
|
11417
11427
|
const arollVideoCount = markup.match(/<video\b[^>]*\bdata-aroll=["']true["'][^>]*>/gi)?.length ?? 0;
|
|
11418
11428
|
const heygenVideoCount = markup.match(/<video\b[^>]*\bdata-media-source=["']heygen["'][^>]*>/gi)?.length ?? 0;
|
|
@@ -14438,15 +14448,15 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
|
14438
14448
|
function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger) {
|
|
14439
14449
|
if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
|
|
14440
14450
|
const { workerTimeoutSeconds, workerConcurrency } = config;
|
|
14441
|
-
const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
|
|
14442
|
-
if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
|
|
14443
|
-
return DEFAULT_MAX_PARALLEL_CHUNKS;
|
|
14444
|
-
}
|
|
14445
|
-
const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
|
|
14446
14451
|
const wave = Math.min(
|
|
14447
14452
|
workerConcurrency ?? DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
14448
14453
|
MAX_PARALLEL_CHUNKS_CEILING
|
|
14449
14454
|
);
|
|
14455
|
+
const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
|
|
14456
|
+
if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
|
|
14457
|
+
return wave;
|
|
14458
|
+
}
|
|
14459
|
+
const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
|
|
14450
14460
|
const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
|
|
14451
14461
|
const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
|
|
14452
14462
|
if (workerTimeoutSeconds === void 0) return wide;
|
|
@@ -14463,7 +14473,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
|
|
|
14463
14473
|
}
|
|
14464
14474
|
if (needed > wave) {
|
|
14465
14475
|
log.warn(
|
|
14466
|
-
`[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}).`,
|
|
14476
|
+
`[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).`,
|
|
14467
14477
|
{ needed, workerConcurrency: wave, workerTimeoutSeconds }
|
|
14468
14478
|
);
|
|
14469
14479
|
}
|
|
@@ -14635,21 +14645,6 @@ function buildLockedRenderConfig(input) {
|
|
|
14635
14645
|
function carriesChunkAudio(config, options) {
|
|
14636
14646
|
return options.chunksCarryAudio === true && config.format === "mp4" && config.cfr !== true;
|
|
14637
14647
|
}
|
|
14638
|
-
async function fitPlanAudioToFrames(audioPath, workDir, totalFrames, fps, run) {
|
|
14639
|
-
const fittedPath = join30(workDir, "audio-fitted.m4a");
|
|
14640
|
-
const result = await padOrTrimAudioToVideoFrameCount({
|
|
14641
|
-
videoPath: audioPath,
|
|
14642
|
-
probeVideoFrameInfo: async () => ({ frameCount: totalFrames, fpsNum: fps, fpsDen: 1 }),
|
|
14643
|
-
audioPath,
|
|
14644
|
-
outputPath: fittedPath,
|
|
14645
|
-
signal: run.signal,
|
|
14646
|
-
timeoutMs: run.timeoutMs
|
|
14647
|
-
});
|
|
14648
|
-
if (!result.success) {
|
|
14649
|
-
throw encoderFailureError("[plan] audio pad/trim failed", result);
|
|
14650
|
-
}
|
|
14651
|
-
renameSync5(fittedPath, audioPath);
|
|
14652
|
-
}
|
|
14653
14648
|
function resolveEncoderTriple(config) {
|
|
14654
14649
|
if (config.format === "mp4") {
|
|
14655
14650
|
return resolveMp4EncoderTriple(config.codec);
|
|
@@ -14947,13 +14942,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
14947
14942
|
"utf-8"
|
|
14948
14943
|
);
|
|
14949
14944
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
14945
|
+
const chunksCarryAudio2 = carriesChunkAudio(config, options);
|
|
14946
|
+
const audioSeconds = chunksCarryAudio2 ? totalFrames / config.fps : probeResult.duration;
|
|
14950
14947
|
stageStarted = Date.now();
|
|
14951
14948
|
const [audioOutcome, overlapOutcome] = await Promise.allSettled([
|
|
14952
14949
|
runAudioStage({
|
|
14953
14950
|
projectDir,
|
|
14954
14951
|
workDir,
|
|
14955
14952
|
compiledDir: finalCompiledDir,
|
|
14956
|
-
duration:
|
|
14953
|
+
duration: audioSeconds,
|
|
14957
14954
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
14958
14955
|
audioGain: cfg.audioGain,
|
|
14959
14956
|
audios: composition.audios,
|
|
@@ -14983,12 +14980,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
14983
14980
|
if (audioResult.hasAudio && existsSync19(audioResult.audioOutputPath)) {
|
|
14984
14981
|
renameSync5(audioResult.audioOutputPath, planAudioPath);
|
|
14985
14982
|
}
|
|
14986
|
-
const chunksCarryAudio2 = carriesChunkAudio(config, options);
|
|
14987
14983
|
if (chunksCarryAudio2 && existsSync19(planAudioPath)) {
|
|
14988
|
-
|
|
14984
|
+
stageStarted = Date.now();
|
|
14985
|
+
const peak = await constrainAacTruePeak({
|
|
14986
|
+
audioPath: planAudioPath,
|
|
14987
|
+
targetDurationSeconds: audioSeconds,
|
|
14989
14988
|
signal: abortSignal,
|
|
14990
14989
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
14991
14990
|
});
|
|
14991
|
+
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
14992
|
+
logPlanStage(log, "audio-peak", stageStarted);
|
|
14992
14993
|
}
|
|
14993
14994
|
const ffmpegVersion = await readFfmpegVersion();
|
|
14994
14995
|
const producerVersion = readProducerVersion();
|
|
@@ -15073,8 +15074,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15073
15074
|
producerVersion
|
|
15074
15075
|
};
|
|
15075
15076
|
}
|
|
15076
|
-
async function plan(projectDir, config, planDir) {
|
|
15077
|
-
const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir);
|
|
15077
|
+
async function plan(projectDir, config, planDir, options = {}) {
|
|
15078
|
+
const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir, options);
|
|
15078
15079
|
return {
|
|
15079
15080
|
planDir: executionPlan.executionPlanDir,
|
|
15080
15081
|
planProtocol: PLAN_PROTOCOL_V1,
|
|
@@ -16704,7 +16705,7 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
|
|
|
16704
16705
|
}
|
|
16705
16706
|
function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
|
|
16706
16707
|
if (isPlanAudioArtifactPath(path)) {
|
|
16707
|
-
return { chunks:
|
|
16708
|
+
return chunksCarryAudio2 ? { chunks: "all", assembler: false } : { chunks: [], assembler: true };
|
|
16708
16709
|
}
|
|
16709
16710
|
if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
|
|
16710
16711
|
return { chunks: "all", assembler: true };
|
|
@@ -17265,7 +17266,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
|
|
|
17265
17266
|
planHash: manifest.planHash,
|
|
17266
17267
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
17267
17268
|
artifactCount: artifacts.length,
|
|
17268
|
-
sizeBytes: artifacts.reduce((
|
|
17269
|
+
sizeBytes: artifacts.reduce((sum2, artifact) => sum2 + artifact.sizeBytes, 0),
|
|
17269
17270
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
17270
17271
|
// before the container change carries the legacy name, and materializing it
|
|
17271
17272
|
// under the new one would point at a file that was never written.
|
|
@@ -17302,24 +17303,24 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
17302
17303
|
}
|
|
17303
17304
|
|
|
17304
17305
|
// src/services/distributed/planV2Execution.ts
|
|
17305
|
-
import { mkdtempSync as
|
|
17306
|
+
import { mkdtempSync as mkdtempSync9, rmSync as rmSync17 } from "node:fs";
|
|
17306
17307
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
17307
|
-
import { join as
|
|
17308
|
+
import { join as join40 } from "node:path";
|
|
17308
17309
|
|
|
17309
17310
|
// src/services/distributed/assemble.ts
|
|
17310
17311
|
import {
|
|
17311
17312
|
cpSync as cpSync3,
|
|
17312
17313
|
existsSync as existsSync25,
|
|
17313
|
-
mkdirSync as
|
|
17314
|
-
mkdtempSync as
|
|
17314
|
+
mkdirSync as mkdirSync16,
|
|
17315
|
+
mkdtempSync as mkdtempSync8,
|
|
17315
17316
|
readFileSync as readFileSync13,
|
|
17316
17317
|
readdirSync as readdirSync9,
|
|
17317
|
-
renameSync as
|
|
17318
|
-
rmSync as
|
|
17318
|
+
renameSync as renameSync9,
|
|
17319
|
+
rmSync as rmSync15,
|
|
17319
17320
|
statSync as statSync11,
|
|
17320
17321
|
writeFileSync as writeFileSync11
|
|
17321
17322
|
} from "node:fs";
|
|
17322
|
-
import { dirname as
|
|
17323
|
+
import { dirname as dirname17, join as join38 } from "node:path";
|
|
17323
17324
|
import {
|
|
17324
17325
|
appendRenderProvenanceArgs,
|
|
17325
17326
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -17330,6 +17331,353 @@ import {
|
|
|
17330
17331
|
runFfmpeg as runFfmpeg5
|
|
17331
17332
|
} from "@hyperframes/engine";
|
|
17332
17333
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
17334
|
+
|
|
17335
|
+
// src/services/distributed/mp4Stitch.ts
|
|
17336
|
+
import {
|
|
17337
|
+
createReadStream as createReadStream5,
|
|
17338
|
+
createWriteStream,
|
|
17339
|
+
mkdirSync as mkdirSync15,
|
|
17340
|
+
mkdtempSync as mkdtempSync7,
|
|
17341
|
+
renameSync as renameSync8,
|
|
17342
|
+
rmSync as rmSync14
|
|
17343
|
+
} from "node:fs";
|
|
17344
|
+
import { open } from "node:fs/promises";
|
|
17345
|
+
import { dirname as dirname16, join as join37 } from "node:path";
|
|
17346
|
+
import { finished, pipeline } from "node:stream/promises";
|
|
17347
|
+
function boxAt(src, off, at, limit) {
|
|
17348
|
+
let size = src.readUInt32BE(off);
|
|
17349
|
+
let bodyStart = at + 8;
|
|
17350
|
+
if (size === 1) {
|
|
17351
|
+
size = Number(src.readBigUInt64BE(off + 8));
|
|
17352
|
+
bodyStart = at + 16;
|
|
17353
|
+
}
|
|
17354
|
+
if (size < 8 || at + size > limit) throw new Error(`[mp4Stitch] malformed box at ${at}`);
|
|
17355
|
+
return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
|
|
17356
|
+
}
|
|
17357
|
+
function childBoxes(buf, parent) {
|
|
17358
|
+
const boxes = [];
|
|
17359
|
+
for (let at = parent.bodyStart; at < parent.end; at = boxes.at(-1).end) {
|
|
17360
|
+
boxes.push(boxAt(buf, at, at, parent.end));
|
|
17361
|
+
}
|
|
17362
|
+
return boxes;
|
|
17363
|
+
}
|
|
17364
|
+
function findChild(buf, parent, type) {
|
|
17365
|
+
return childBoxes(buf, parent).find((box2) => box2.type === type);
|
|
17366
|
+
}
|
|
17367
|
+
function child(buf, parent, type) {
|
|
17368
|
+
const found = findChild(buf, parent, type);
|
|
17369
|
+
if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
|
|
17370
|
+
return found;
|
|
17371
|
+
}
|
|
17372
|
+
var SCAN_BYTES = 64 * 1024;
|
|
17373
|
+
async function readMp4Layout(read) {
|
|
17374
|
+
const found = /* @__PURE__ */ new Map();
|
|
17375
|
+
let payload;
|
|
17376
|
+
let buf = Buffer.alloc(0);
|
|
17377
|
+
let bufAt = 0;
|
|
17378
|
+
const load = async (start, end) => {
|
|
17379
|
+
if (start >= bufAt && end <= bufAt + buf.length) return;
|
|
17380
|
+
buf = await read(start, Math.max(end, start + SCAN_BYTES));
|
|
17381
|
+
bufAt = start;
|
|
17382
|
+
};
|
|
17383
|
+
for (let at = 0; !(found.has("ftyp") && found.has("moov") && payload); ) {
|
|
17384
|
+
await load(at, at + 16);
|
|
17385
|
+
if (at + 8 > bufAt + buf.length) {
|
|
17386
|
+
throw new Error(`[mp4Stitch] chunk needs ftyp, moov and mdat; found ${[...found.keys()]}`);
|
|
17387
|
+
}
|
|
17388
|
+
const box2 = boxAt(buf, at - bufAt, at, Number.MAX_SAFE_INTEGER);
|
|
17389
|
+
if (box2.type === "mdat") payload ??= box2;
|
|
17390
|
+
if (box2.type === "ftyp" || box2.type === "moov") {
|
|
17391
|
+
await load(at, box2.end);
|
|
17392
|
+
found.set(box2.type, buf.subarray(at - bufAt, box2.end - bufAt));
|
|
17393
|
+
}
|
|
17394
|
+
at = box2.end;
|
|
17395
|
+
}
|
|
17396
|
+
const [ftyp, moov] = [found.get("ftyp"), found.get("moov")];
|
|
17397
|
+
if (moov.length !== moovSize(moov)) throw new Error("[mp4Stitch] moov is cut off");
|
|
17398
|
+
return { ftyp, moov, payloadStart: payload.bodyStart, payloadEnd: payload.end };
|
|
17399
|
+
}
|
|
17400
|
+
var moovSize = (moov) => boxAt(moov, 0, 0, Number.MAX_SAFE_INTEGER).end;
|
|
17401
|
+
function readTracks(moov) {
|
|
17402
|
+
const root = boxAt(moov, 0, 0, moov.length);
|
|
17403
|
+
return childBoxes(moov, root).filter((box2) => box2.type === "trak").map((trak) => readTrack(moov, trak));
|
|
17404
|
+
}
|
|
17405
|
+
function rows(buf, table, columns, countAt = 4) {
|
|
17406
|
+
const count = buf.readUInt32BE(table.bodyStart + countAt);
|
|
17407
|
+
const first = table.bodyStart + countAt + 4;
|
|
17408
|
+
return Array.from(
|
|
17409
|
+
{ length: count },
|
|
17410
|
+
(_, row) => Array.from(
|
|
17411
|
+
{ length: columns },
|
|
17412
|
+
(_2, col) => buf.readUInt32BE(first + (row * columns + col) * 4)
|
|
17413
|
+
)
|
|
17414
|
+
);
|
|
17415
|
+
}
|
|
17416
|
+
function readMediaTime(moov, trak) {
|
|
17417
|
+
const edts = findChild(moov, trak, "edts");
|
|
17418
|
+
if (!edts) return 0;
|
|
17419
|
+
const elst = child(moov, edts, "elst");
|
|
17420
|
+
const wide = moov[elst.bodyStart] === 1;
|
|
17421
|
+
const entries = moov.readUInt32BE(elst.bodyStart + 4);
|
|
17422
|
+
for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
|
|
17423
|
+
const offset = at + (wide ? 8 : 4);
|
|
17424
|
+
const mediaTime = wide ? Number(moov.readBigInt64BE(offset)) : moov.readInt32BE(offset);
|
|
17425
|
+
if (mediaTime !== -1) return mediaTime;
|
|
17426
|
+
}
|
|
17427
|
+
return 0;
|
|
17428
|
+
}
|
|
17429
|
+
function readSizes(moov, stsz) {
|
|
17430
|
+
const fixedSize = moov.readUInt32BE(stsz.bodyStart + 4);
|
|
17431
|
+
if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
|
|
17432
|
+
return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
|
|
17433
|
+
}
|
|
17434
|
+
function readSync4(moov, stbl, sampleCount) {
|
|
17435
|
+
const stss = findChild(moov, stbl, "stss");
|
|
17436
|
+
if (!stss) return null;
|
|
17437
|
+
const sync = Array(sampleCount).fill(false);
|
|
17438
|
+
for (const [n] of rows(moov, stss, 1)) sync[n - 1] = true;
|
|
17439
|
+
return sync;
|
|
17440
|
+
}
|
|
17441
|
+
function readChunkOffsets(moov, stbl) {
|
|
17442
|
+
const stco = findChild(moov, stbl, "stco");
|
|
17443
|
+
if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
|
|
17444
|
+
return rows(moov, child(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
|
|
17445
|
+
}
|
|
17446
|
+
function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
17447
|
+
const stsc = rows(moov, child(moov, stbl, "stsc"), 3);
|
|
17448
|
+
return stsc.flatMap(([firstChunk, perChunk, description], i) => {
|
|
17449
|
+
if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
|
|
17450
|
+
const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
|
|
17451
|
+
return Array(nextFirst - firstChunk).fill(perChunk);
|
|
17452
|
+
});
|
|
17453
|
+
}
|
|
17454
|
+
function readTrack(moov, trak) {
|
|
17455
|
+
const mdia = child(moov, trak, "mdia");
|
|
17456
|
+
const hdlr = child(moov, mdia, "hdlr");
|
|
17457
|
+
const stbl = child(moov, child(moov, mdia, "minf"), "stbl");
|
|
17458
|
+
if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
|
|
17459
|
+
const stts = rows(moov, child(moov, stbl, "stts"), 2);
|
|
17460
|
+
const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
|
|
17461
|
+
const sizes = readSizes(moov, child(moov, stbl, "stsz"));
|
|
17462
|
+
const chunkOffsets = readChunkOffsets(moov, stbl);
|
|
17463
|
+
const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
|
|
17464
|
+
if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
|
|
17465
|
+
throw new Error("[mp4Stitch] sample tables disagree on the sample count");
|
|
17466
|
+
}
|
|
17467
|
+
const stsd = child(moov, stbl, "stsd");
|
|
17468
|
+
return {
|
|
17469
|
+
trak,
|
|
17470
|
+
handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
|
|
17471
|
+
timescale: timescaleOf(moov, child(moov, mdia, "mdhd")),
|
|
17472
|
+
mediaTime: readMediaTime(moov, trak),
|
|
17473
|
+
stsd: moov.subarray(stsd.start, stsd.end),
|
|
17474
|
+
durations,
|
|
17475
|
+
sizes,
|
|
17476
|
+
sync: readSync4(moov, stbl, sizes.length),
|
|
17477
|
+
samplesPerChunk,
|
|
17478
|
+
chunkOffsets
|
|
17479
|
+
};
|
|
17480
|
+
}
|
|
17481
|
+
function sum(values) {
|
|
17482
|
+
return values.reduce((total, value) => total + value, 0);
|
|
17483
|
+
}
|
|
17484
|
+
function box(type, ...body) {
|
|
17485
|
+
const header = Buffer.alloc(8);
|
|
17486
|
+
header.writeUInt32BE(8 + sum(body.map((part) => part.length)));
|
|
17487
|
+
header.write(type, 4, "latin1");
|
|
17488
|
+
return Buffer.concat([header, ...body]);
|
|
17489
|
+
}
|
|
17490
|
+
function fullBox(type, version, body) {
|
|
17491
|
+
return box(type, Buffer.from([version, 0, 0, 0]), body);
|
|
17492
|
+
}
|
|
17493
|
+
function u32s(values) {
|
|
17494
|
+
const buf = Buffer.alloc(values.length * 4);
|
|
17495
|
+
values.forEach((value, i) => buf.writeUInt32BE(value, i * 4));
|
|
17496
|
+
return buf;
|
|
17497
|
+
}
|
|
17498
|
+
function runs(values) {
|
|
17499
|
+
const out = [];
|
|
17500
|
+
for (const value of values) {
|
|
17501
|
+
const last = out.at(-1);
|
|
17502
|
+
if (last && last[1] === value) last[0]++;
|
|
17503
|
+
else out.push([1, value]);
|
|
17504
|
+
}
|
|
17505
|
+
return out;
|
|
17506
|
+
}
|
|
17507
|
+
function rebuild(buf, parent, replace) {
|
|
17508
|
+
const parts = childBoxes(buf, parent).map(
|
|
17509
|
+
(box2) => replace[box2.type]?.(box2) ?? buf.subarray(box2.start, box2.end)
|
|
17510
|
+
);
|
|
17511
|
+
return box(parent.type, ...parts);
|
|
17512
|
+
}
|
|
17513
|
+
var DURATION_AT = {
|
|
17514
|
+
mvhd: [16, 24],
|
|
17515
|
+
mdhd: [16, 24],
|
|
17516
|
+
tkhd: [20, 28]
|
|
17517
|
+
};
|
|
17518
|
+
function withDuration(buf, target, duration) {
|
|
17519
|
+
const copy = Buffer.from(buf.subarray(target.start, target.end));
|
|
17520
|
+
const body = target.bodyStart - target.start;
|
|
17521
|
+
const wide = copy[body] === 1;
|
|
17522
|
+
const at = body + DURATION_AT[target.type][wide ? 1 : 0];
|
|
17523
|
+
if (wide) copy.writeBigUInt64BE(BigInt(duration), at);
|
|
17524
|
+
else if (duration <= 4294967295) copy.writeUInt32BE(duration, at);
|
|
17525
|
+
else throw new Error(`[mp4Stitch] ${target.type} duration ${duration} needs a version 1 box`);
|
|
17526
|
+
return copy;
|
|
17527
|
+
}
|
|
17528
|
+
function timescaleOf(buf, header) {
|
|
17529
|
+
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
17530
|
+
}
|
|
17531
|
+
function buildStbl(stsd, track) {
|
|
17532
|
+
const stts = runs(track.durations);
|
|
17533
|
+
const stsc = [];
|
|
17534
|
+
let firstChunk = 1;
|
|
17535
|
+
for (const [count, perChunk] of runs(track.samplesPerChunk)) {
|
|
17536
|
+
stsc.push([firstChunk, perChunk, 1]);
|
|
17537
|
+
firstChunk += count;
|
|
17538
|
+
}
|
|
17539
|
+
const co64 = Buffer.alloc(4 + track.chunkOffsets.length * 8);
|
|
17540
|
+
co64.writeUInt32BE(track.chunkOffsets.length);
|
|
17541
|
+
track.chunkOffsets.forEach((offset, i) => co64.writeBigUInt64BE(BigInt(offset), 4 + i * 8));
|
|
17542
|
+
const syncSamples = track.sync?.flatMap((isSync, i) => isSync ? [i + 1] : []) ?? null;
|
|
17543
|
+
return box(
|
|
17544
|
+
"stbl",
|
|
17545
|
+
stsd,
|
|
17546
|
+
fullBox("stts", 0, u32s([stts.length, ...stts.flat()])),
|
|
17547
|
+
...syncSamples ? [fullBox("stss", 0, u32s([syncSamples.length, ...syncSamples]))] : [],
|
|
17548
|
+
fullBox("stsc", 0, u32s([stsc.length, ...stsc.flat()])),
|
|
17549
|
+
fullBox("stsz", 0, u32s([0, track.sizes.length, ...track.sizes])),
|
|
17550
|
+
fullBox("co64", 0, co64)
|
|
17551
|
+
);
|
|
17552
|
+
}
|
|
17553
|
+
var codecOf = (track) => track.stsd.toString("latin1", 20, 24);
|
|
17554
|
+
var sameTrack = (track, plan2) => track.handler === plan2.handler && track.timescale === plan2.timescale && codecOf(track) === codecOf(plan2);
|
|
17555
|
+
function planTracksOf(tracks, plan2, index) {
|
|
17556
|
+
let next = 0;
|
|
17557
|
+
const matched = plan2.map((planTrack) => {
|
|
17558
|
+
const track = tracks[next];
|
|
17559
|
+
if (track && sameTrack(track, planTrack)) return tracks[next++];
|
|
17560
|
+
if (planTrack.handler === "soun") return null;
|
|
17561
|
+
throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
|
|
17562
|
+
});
|
|
17563
|
+
if (next !== tracks.length)
|
|
17564
|
+
throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
|
|
17565
|
+
for (const track of tracks) {
|
|
17566
|
+
if (track.handler === "vide" && track.mediaTime !== 0) {
|
|
17567
|
+
throw new Error(`[mp4Stitch] chunk ${index} video starts at media time ${track.mediaTime}`);
|
|
17568
|
+
}
|
|
17569
|
+
}
|
|
17570
|
+
return matched;
|
|
17571
|
+
}
|
|
17572
|
+
function buildStitchedHeader(layouts) {
|
|
17573
|
+
const plan2 = readTracks(layouts[0].moov);
|
|
17574
|
+
const chunks = layouts.map((layout, i) => planTracksOf(readTracks(layout.moov), plan2, i));
|
|
17575
|
+
const payloadBases = [];
|
|
17576
|
+
let payloadBytes = 0;
|
|
17577
|
+
for (const layout of layouts) {
|
|
17578
|
+
payloadBases.push(payloadBytes);
|
|
17579
|
+
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
17580
|
+
}
|
|
17581
|
+
const template = layouts[0].moov;
|
|
17582
|
+
const root = boxAt(template, 0, 0, template.length);
|
|
17583
|
+
const movieTimescale = timescaleOf(template, child(template, root, "mvhd"));
|
|
17584
|
+
const buildMoov = (dataStart2) => {
|
|
17585
|
+
const traks = plan2.map((track, t) => {
|
|
17586
|
+
const parts = chunks.flatMap((tracks, chunk) => {
|
|
17587
|
+
const part = tracks[t];
|
|
17588
|
+
return part ? [{ ...part, chunk }] : [];
|
|
17589
|
+
});
|
|
17590
|
+
const merged = {
|
|
17591
|
+
durations: parts.flatMap((part) => part.durations),
|
|
17592
|
+
sizes: parts.flatMap((part) => part.sizes),
|
|
17593
|
+
sync: parts.every((part) => !part.sync) ? null : parts.flatMap((part) => part.sync ?? part.sizes.map(() => true)),
|
|
17594
|
+
samplesPerChunk: parts.flatMap((part) => part.samplesPerChunk),
|
|
17595
|
+
chunkOffsets: parts.flatMap(
|
|
17596
|
+
(part) => part.chunkOffsets.map((offset) => {
|
|
17597
|
+
const i = part.chunk;
|
|
17598
|
+
const { payloadStart, payloadEnd } = layouts[i];
|
|
17599
|
+
if (offset < payloadStart || offset >= payloadEnd) {
|
|
17600
|
+
throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
|
|
17601
|
+
}
|
|
17602
|
+
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
17603
|
+
})
|
|
17604
|
+
)
|
|
17605
|
+
};
|
|
17606
|
+
const mediaDuration = sum(merged.durations);
|
|
17607
|
+
const shown = Math.round(
|
|
17608
|
+
(mediaDuration - track.mediaTime) * movieTimescale / track.timescale
|
|
17609
|
+
);
|
|
17610
|
+
const elst = Buffer.alloc(24);
|
|
17611
|
+
elst.writeUInt32BE(1);
|
|
17612
|
+
elst.writeBigUInt64BE(BigInt(shown), 4);
|
|
17613
|
+
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
17614
|
+
elst.writeUInt32BE(65536, 20);
|
|
17615
|
+
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
17616
|
+
const trak = rebuild(template, track.trak, {
|
|
17617
|
+
tkhd: (tkhd) => Buffer.concat([withDuration(template, tkhd, shown), edts]),
|
|
17618
|
+
edts: () => Buffer.alloc(0),
|
|
17619
|
+
mdia: (mdia) => rebuild(template, mdia, {
|
|
17620
|
+
mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
|
|
17621
|
+
minf: (minf) => rebuild(template, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
17622
|
+
})
|
|
17623
|
+
});
|
|
17624
|
+
return { trak, shown };
|
|
17625
|
+
});
|
|
17626
|
+
let next = 0;
|
|
17627
|
+
return rebuild(template, root, {
|
|
17628
|
+
mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
|
|
17629
|
+
trak: () => traks[next++].trak
|
|
17630
|
+
});
|
|
17631
|
+
};
|
|
17632
|
+
const ftyp = layouts[0].ftyp;
|
|
17633
|
+
const dataStart = ftyp.length + buildMoov(0).length + 16;
|
|
17634
|
+
const mdat = Buffer.alloc(16);
|
|
17635
|
+
mdat.writeUInt32BE(1);
|
|
17636
|
+
mdat.write("mdat", 4, "latin1");
|
|
17637
|
+
mdat.writeBigUInt64BE(BigInt(16 + payloadBytes), 8);
|
|
17638
|
+
return Buffer.concat([ftyp, buildMoov(dataStart), mdat]);
|
|
17639
|
+
}
|
|
17640
|
+
function fileRange(path) {
|
|
17641
|
+
return async (start, end) => {
|
|
17642
|
+
const file = await open(path, "r", 384);
|
|
17643
|
+
try {
|
|
17644
|
+
const buf = Buffer.alloc(end - start);
|
|
17645
|
+
const { bytesRead } = await file.read(buf, 0, buf.length, start);
|
|
17646
|
+
return buf.subarray(0, bytesRead);
|
|
17647
|
+
} finally {
|
|
17648
|
+
await file.close();
|
|
17649
|
+
}
|
|
17650
|
+
};
|
|
17651
|
+
}
|
|
17652
|
+
async function copyPayload(path, layout, index, out) {
|
|
17653
|
+
const { payloadStart, payloadEnd } = layout;
|
|
17654
|
+
const payload = createReadStream5(path, { start: payloadStart, end: payloadEnd - 1 });
|
|
17655
|
+
await pipeline(payload, out, { end: false });
|
|
17656
|
+
if (payload.bytesRead !== payloadEnd - payloadStart) {
|
|
17657
|
+
throw new Error(
|
|
17658
|
+
`[mp4Stitch] chunk ${index} mdat holds ${payload.bytesRead} of ${payloadEnd - payloadStart} payload bytes`
|
|
17659
|
+
);
|
|
17660
|
+
}
|
|
17661
|
+
}
|
|
17662
|
+
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
17663
|
+
const layouts = await Promise.all(chunkPaths.map((path) => readMp4Layout(fileRange(path))));
|
|
17664
|
+
const header = buildStitchedHeader(layouts);
|
|
17665
|
+
mkdirSync15(dirname16(outputPath), { recursive: true });
|
|
17666
|
+
const workDir = mkdtempSync7(`${outputPath}.stitch-`);
|
|
17667
|
+
const out = createWriteStream(join37(workDir, "out.mp4"));
|
|
17668
|
+
try {
|
|
17669
|
+
out.write(header);
|
|
17670
|
+
for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
17671
|
+
out.end();
|
|
17672
|
+
await finished(out);
|
|
17673
|
+
renameSync8(join37(workDir, "out.mp4"), outputPath);
|
|
17674
|
+
} finally {
|
|
17675
|
+
out.destroy();
|
|
17676
|
+
rmSync14(workDir, { recursive: true, force: true });
|
|
17677
|
+
}
|
|
17678
|
+
}
|
|
17679
|
+
|
|
17680
|
+
// src/services/distributed/assemble.ts
|
|
17333
17681
|
function cfrRateControlArgs(locked) {
|
|
17334
17682
|
const preset = getEncoderPreset5(locked.quality ?? "standard", "mp4");
|
|
17335
17683
|
if (locked.crf == null && locked.bitrate) {
|
|
@@ -17345,12 +17693,21 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17345
17693
|
if (plan2.dimensions.format === "png-sequence") {
|
|
17346
17694
|
return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
|
|
17347
17695
|
}
|
|
17696
|
+
if (chunksAreStitchedMp4(planDir, cfr)) {
|
|
17697
|
+
await writeStitchedMp4(chunkPaths, outputPath);
|
|
17698
|
+
return {
|
|
17699
|
+
outputPath,
|
|
17700
|
+
durationMs: Date.now() - start,
|
|
17701
|
+
framesEncoded: plan2.totalFrames,
|
|
17702
|
+
fileSize: statSync11(outputPath).size
|
|
17703
|
+
};
|
|
17704
|
+
}
|
|
17348
17705
|
const format = plan2.dimensions.format;
|
|
17349
17706
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
17350
|
-
if (!existsSync25(
|
|
17351
|
-
|
|
17707
|
+
if (!existsSync25(dirname17(outputPath))) {
|
|
17708
|
+
mkdirSync16(dirname17(outputPath), { recursive: true });
|
|
17352
17709
|
}
|
|
17353
|
-
const workDir =
|
|
17710
|
+
const workDir = mkdtempSync8(`${outputPath}.assemble-work-`);
|
|
17354
17711
|
const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
17355
17712
|
const stageMs = {};
|
|
17356
17713
|
const ctx = {
|
|
@@ -17358,7 +17715,6 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17358
17715
|
format,
|
|
17359
17716
|
fps,
|
|
17360
17717
|
fpsArg: fpsToFfmpegArg3(fps),
|
|
17361
|
-
chunkSeconds: chunksCarryAudio(planDir) ? plan2.chunks.map((chunk) => (chunk.endFrame - chunk.startFrame) * fps.den / fps.num) : null,
|
|
17362
17718
|
signal: options?.abortSignal,
|
|
17363
17719
|
timeout: resolveConfig6().ffmpegProcessTimeout,
|
|
17364
17720
|
log,
|
|
@@ -17371,10 +17727,10 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17371
17727
|
};
|
|
17372
17728
|
try {
|
|
17373
17729
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
17374
|
-
const finalPath =
|
|
17730
|
+
const finalPath = join38(workDir, `output.${format}`);
|
|
17375
17731
|
const audio = audioPath !== null && existsSync25(audioPath) ? audioPath : null;
|
|
17376
17732
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
17377
|
-
|
|
17733
|
+
renameSync9(finalPath, outputPath);
|
|
17378
17734
|
log.info("[assemble] stage timings", {
|
|
17379
17735
|
...stageMs,
|
|
17380
17736
|
chunks: chunkPaths.length,
|
|
@@ -17382,7 +17738,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17382
17738
|
});
|
|
17383
17739
|
} finally {
|
|
17384
17740
|
try {
|
|
17385
|
-
|
|
17741
|
+
rmSync15(workDir, { recursive: true, force: true });
|
|
17386
17742
|
} catch (err) {
|
|
17387
17743
|
log.warn("[assemble] failed to remove work dir", {
|
|
17388
17744
|
workDir,
|
|
@@ -17399,8 +17755,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17399
17755
|
};
|
|
17400
17756
|
}
|
|
17401
17757
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
17402
|
-
const planJsonPath =
|
|
17403
|
-
const chunksJsonPath =
|
|
17758
|
+
const planJsonPath = join38(planDir, "plan.json");
|
|
17759
|
+
const chunksJsonPath = join38(planDir, "meta", "chunks.json");
|
|
17404
17760
|
if (!existsSync25(planJsonPath)) {
|
|
17405
17761
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
17406
17762
|
}
|
|
@@ -17421,13 +17777,13 @@ function readAssemblePlan(planDir, chunkPaths) {
|
|
|
17421
17777
|
throw new Error(`[assemble] chunk path does not exist: ${path}`);
|
|
17422
17778
|
}
|
|
17423
17779
|
}
|
|
17424
|
-
return
|
|
17780
|
+
return plan2;
|
|
17425
17781
|
}
|
|
17426
17782
|
function videoInputArgs(ctx, videoSources) {
|
|
17427
|
-
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg
|
|
17783
|
+
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
17428
17784
|
}
|
|
17429
17785
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
17430
|
-
const cfrOutputPath =
|
|
17786
|
+
const cfrOutputPath = join38(ctx.workDir, `cfr.${ctx.format}`);
|
|
17431
17787
|
const cfrArgs = [
|
|
17432
17788
|
...videoInputArgs(ctx, chunkPaths),
|
|
17433
17789
|
"-c:v",
|
|
@@ -17465,7 +17821,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
17465
17821
|
);
|
|
17466
17822
|
return;
|
|
17467
17823
|
}
|
|
17468
|
-
const paddedAudioPath =
|
|
17824
|
+
const paddedAudioPath = join38(ctx.workDir, "audio-padded.m4a");
|
|
17469
17825
|
const padTrimResult = await ctx.timed(
|
|
17470
17826
|
"audioMs",
|
|
17471
17827
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -17504,15 +17860,10 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
17504
17860
|
throw encoderFailureError("[assemble] audio mux failed", muxResult);
|
|
17505
17861
|
}
|
|
17506
17862
|
}
|
|
17507
|
-
function concatDemuxerInput(chunkPaths, workDir, fpsArg
|
|
17508
|
-
const listPath =
|
|
17509
|
-
const
|
|
17510
|
-
|
|
17511
|
-
const line = `file '${path.replace(/'/g, "'\\''")}'`;
|
|
17512
|
-
return chunkSeconds ? `${line}
|
|
17513
|
-
duration ${chunkSeconds[i].toFixed(6)}` : line;
|
|
17514
|
-
});
|
|
17515
|
-
writeFileSync11(listPath, `${[...head, ...body].join("\n")}
|
|
17863
|
+
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
17864
|
+
const listPath = join38(workDir, "concat-list.txt");
|
|
17865
|
+
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
17866
|
+
writeFileSync11(listPath, `${body}
|
|
17516
17867
|
`, "utf-8");
|
|
17517
17868
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
17518
17869
|
}
|
|
@@ -17521,7 +17872,7 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
17521
17872
|
videoPaths.map((path) => defaultProbeVideoFrameInfo(path, signal))
|
|
17522
17873
|
);
|
|
17523
17874
|
return {
|
|
17524
|
-
frameCount: infos.reduce((
|
|
17875
|
+
frameCount: infos.reduce((sum2, info) => sum2 + info.frameCount, 0),
|
|
17525
17876
|
fpsNum: fps.num,
|
|
17526
17877
|
fpsDen: fps.den
|
|
17527
17878
|
};
|
|
@@ -17535,8 +17886,15 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
17535
17886
|
});
|
|
17536
17887
|
}
|
|
17537
17888
|
}
|
|
17889
|
+
function chunksAreStitchedMp4(planDir, cfr) {
|
|
17890
|
+
if (!chunksCarryAudio(planDir)) return false;
|
|
17891
|
+
if (cfr) {
|
|
17892
|
+
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks carry audio");
|
|
17893
|
+
}
|
|
17894
|
+
return true;
|
|
17895
|
+
}
|
|
17538
17896
|
function chunksCarryAudio(planDir) {
|
|
17539
|
-
const encoderJsonPath =
|
|
17897
|
+
const encoderJsonPath = join38(planDir, "meta", "encoder.json");
|
|
17540
17898
|
if (!existsSync25(encoderJsonPath)) return false;
|
|
17541
17899
|
return JSON.parse(readFileSync13(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
|
|
17542
17900
|
}
|
|
@@ -17546,7 +17904,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
17546
17904
|
`[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.`
|
|
17547
17905
|
);
|
|
17548
17906
|
}
|
|
17549
|
-
const encoderJsonPath =
|
|
17907
|
+
const encoderJsonPath = join38(planDir, "meta", "encoder.json");
|
|
17550
17908
|
if (!existsSync25(encoderJsonPath)) {
|
|
17551
17909
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
17552
17910
|
}
|
|
@@ -17559,8 +17917,8 @@ function readCfrEncoder(planDir, format) {
|
|
|
17559
17917
|
return encoderJson;
|
|
17560
17918
|
}
|
|
17561
17919
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
17562
|
-
if (existsSync25(outputPath))
|
|
17563
|
-
|
|
17920
|
+
if (existsSync25(outputPath)) rmSync15(outputPath, { recursive: true, force: true });
|
|
17921
|
+
mkdirSync16(outputPath, { recursive: true });
|
|
17564
17922
|
let globalIdx = 0;
|
|
17565
17923
|
for (const chunkDir of chunkPaths) {
|
|
17566
17924
|
if (!statSync11(chunkDir).isDirectory()) {
|
|
@@ -17573,8 +17931,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
17573
17931
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
17574
17932
|
}
|
|
17575
17933
|
for (const frame of frames) {
|
|
17576
|
-
const dst =
|
|
17577
|
-
cpSync3(
|
|
17934
|
+
const dst = join38(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
17935
|
+
cpSync3(join38(chunkDir, frame), dst);
|
|
17578
17936
|
globalIdx += 1;
|
|
17579
17937
|
}
|
|
17580
17938
|
}
|
|
@@ -17584,13 +17942,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
17584
17942
|
);
|
|
17585
17943
|
}
|
|
17586
17944
|
if (audioPath !== null && existsSync25(audioPath)) {
|
|
17587
|
-
const sidecar =
|
|
17945
|
+
const sidecar = join38(outputPath, MIXED_AUDIO_FILENAME4);
|
|
17588
17946
|
cpSync3(audioPath, sidecar);
|
|
17589
17947
|
}
|
|
17590
17948
|
let fileSize = 0;
|
|
17591
17949
|
for (const name of readdirSync9(outputPath)) {
|
|
17592
17950
|
try {
|
|
17593
|
-
fileSize += statSync11(
|
|
17951
|
+
fileSize += statSync11(join38(outputPath, name)).size;
|
|
17594
17952
|
} catch {
|
|
17595
17953
|
}
|
|
17596
17954
|
}
|
|
@@ -17604,8 +17962,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
17604
17962
|
|
|
17605
17963
|
// src/services/distributed/renderChunk.ts
|
|
17606
17964
|
import { randomBytes } from "node:crypto";
|
|
17607
|
-
import { existsSync as existsSync26, mkdirSync as
|
|
17608
|
-
import { extname as extname6, join as
|
|
17965
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync14, readdirSync as readdirSync10, rmSync as rmSync16, writeFileSync as writeFileSync12 } from "node:fs";
|
|
17966
|
+
import { extname as extname6, join as join39 } from "node:path";
|
|
17609
17967
|
import {
|
|
17610
17968
|
assertSwiftShader,
|
|
17611
17969
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE3,
|
|
@@ -17811,7 +18169,7 @@ function frameNumberFromFileName(name) {
|
|
|
17811
18169
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
17812
18170
|
const result = [];
|
|
17813
18171
|
for (const v of videos) {
|
|
17814
|
-
const outputDir =
|
|
18172
|
+
const outputDir = join39(planDir, "video-frames", v.videoId);
|
|
17815
18173
|
if (!existsSync26(outputDir)) {
|
|
17816
18174
|
throw new Error(
|
|
17817
18175
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
@@ -17830,7 +18188,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
17830
18188
|
if (!frameName) continue;
|
|
17831
18189
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
17832
18190
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
17833
|
-
framePaths.set(frameIndex,
|
|
18191
|
+
framePaths.set(frameIndex, join39(outputDir, frameName));
|
|
17834
18192
|
}
|
|
17835
18193
|
result.push({
|
|
17836
18194
|
videoId: v.videoId,
|
|
@@ -17852,13 +18210,13 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
17852
18210
|
return result;
|
|
17853
18211
|
}
|
|
17854
18212
|
function contiguousRuns(sortedIndexes) {
|
|
17855
|
-
const
|
|
18213
|
+
const runs2 = [];
|
|
17856
18214
|
for (const index of sortedIndexes) {
|
|
17857
|
-
const last =
|
|
18215
|
+
const last = runs2[runs2.length - 1];
|
|
17858
18216
|
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
17859
|
-
else
|
|
18217
|
+
else runs2.push({ firstFrame: index, frames: 1 });
|
|
17860
18218
|
}
|
|
17861
|
-
return
|
|
18219
|
+
return runs2;
|
|
17862
18220
|
}
|
|
17863
18221
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
17864
18222
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
@@ -17883,13 +18241,13 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
17883
18241
|
}
|
|
17884
18242
|
return Promise.all(
|
|
17885
18243
|
deferred.map(async (video) => {
|
|
17886
|
-
const outputDir =
|
|
18244
|
+
const outputDir = join39(input.outputRoot, video.videoId);
|
|
17887
18245
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
17888
18246
|
if (indexes.length === 0) {
|
|
17889
18247
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
17890
18248
|
}
|
|
17891
18249
|
const range = video.deferredRange;
|
|
17892
|
-
const sourcePath =
|
|
18250
|
+
const sourcePath = join39(input.planDir, ...range.sourcePath.split("/"));
|
|
17893
18251
|
if (!existsSync26(sourcePath)) {
|
|
17894
18252
|
throw new RenderChunkValidationError(
|
|
17895
18253
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -17925,7 +18283,7 @@ function hashChunkOutput(outputPath, kind) {
|
|
|
17925
18283
|
if (kind === "file") return sha256Hex(readFileSync14(outputPath));
|
|
17926
18284
|
const entries = readdirSync10(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
17927
18285
|
const lines = entries.map(
|
|
17928
|
-
(name) => `${name}\0${sha256Hex(readFileSync14(
|
|
18286
|
+
(name) => `${name}\0${sha256Hex(readFileSync14(join39(outputPath, name)))}`
|
|
17929
18287
|
);
|
|
17930
18288
|
return sha256Hex(lines.join("\0"));
|
|
17931
18289
|
}
|
|
@@ -17942,9 +18300,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
17942
18300
|
async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
17943
18301
|
const start = Date.now();
|
|
17944
18302
|
const log = defaultLogger;
|
|
17945
|
-
const planJsonPath =
|
|
17946
|
-
const encoderJsonPath =
|
|
17947
|
-
const chunksJsonPath =
|
|
18303
|
+
const planJsonPath = join39(planDir, "plan.json");
|
|
18304
|
+
const encoderJsonPath = join39(planDir, "meta", "encoder.json");
|
|
18305
|
+
const chunksJsonPath = join39(planDir, "meta", "chunks.json");
|
|
17948
18306
|
if (!existsSync26(planJsonPath)) {
|
|
17949
18307
|
throw new RenderChunkValidationError(
|
|
17950
18308
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -17968,7 +18326,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
17968
18326
|
}
|
|
17969
18327
|
const encoder = JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
|
|
17970
18328
|
const chunks = JSON.parse(readFileSync14(chunksJsonPath, "utf-8"));
|
|
17971
|
-
const videosJsonPath =
|
|
18329
|
+
const videosJsonPath = join39(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
17972
18330
|
let planVideos = null;
|
|
17973
18331
|
if (existsSync26(videosJsonPath)) {
|
|
17974
18332
|
try {
|
|
@@ -17998,7 +18356,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
17998
18356
|
`[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
|
|
17999
18357
|
);
|
|
18000
18358
|
}
|
|
18001
|
-
const compiledDir =
|
|
18359
|
+
const compiledDir = join39(planDir, "compiled");
|
|
18002
18360
|
if (!existsSync26(compiledDir)) {
|
|
18003
18361
|
throw new RenderChunkValidationError(
|
|
18004
18362
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -18055,9 +18413,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18055
18413
|
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
|
|
18056
18414
|
};
|
|
18057
18415
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
18058
|
-
|
|
18059
|
-
const framesDir =
|
|
18060
|
-
|
|
18416
|
+
mkdirSync17(workDir, { recursive: true });
|
|
18417
|
+
const framesDir = join39(workDir, "captured-frames");
|
|
18418
|
+
mkdirSync17(framesDir, { recursive: true });
|
|
18061
18419
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
18062
18420
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable3(
|
|
18063
18421
|
planVideos.videos,
|
|
@@ -18110,7 +18468,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18110
18468
|
// `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
|
|
18111
18469
|
lockWarmupTicks: true,
|
|
18112
18470
|
requiresWebGpu: compositionRequiresWebGpu3(
|
|
18113
|
-
readFileSync14(
|
|
18471
|
+
readFileSync14(join39(compiledDir, "index.html"), "utf-8")
|
|
18114
18472
|
)
|
|
18115
18473
|
};
|
|
18116
18474
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
@@ -18241,8 +18599,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18241
18599
|
return observedMode;
|
|
18242
18600
|
},
|
|
18243
18601
|
resetForScreenshotRetry: () => {
|
|
18244
|
-
|
|
18245
|
-
|
|
18602
|
+
rmSync16(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
18603
|
+
mkdirSync17(framesDir, { recursive: true });
|
|
18246
18604
|
capturePerfs.length = 0;
|
|
18247
18605
|
captureAttempts.length = 0;
|
|
18248
18606
|
job.framesRendered = 0;
|
|
@@ -18273,10 +18631,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18273
18631
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
18274
18632
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
18275
18633
|
if (isPngSequence) {
|
|
18276
|
-
if (!existsSync26(outputChunkPath))
|
|
18634
|
+
if (!existsSync26(outputChunkPath)) mkdirSync17(outputChunkPath, { recursive: true });
|
|
18277
18635
|
} else {
|
|
18278
|
-
const outDir =
|
|
18279
|
-
if (!existsSync26(outDir))
|
|
18636
|
+
const outDir = join39(outputChunkPath, "..");
|
|
18637
|
+
if (!existsSync26(outDir)) mkdirSync17(outDir, { recursive: true });
|
|
18280
18638
|
}
|
|
18281
18639
|
const encodeStarted = Date.now();
|
|
18282
18640
|
await runEncodeStage({
|
|
@@ -18328,7 +18686,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18328
18686
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
18329
18687
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
18330
18688
|
});
|
|
18331
|
-
|
|
18689
|
+
rmSync16(videoOnlyPath, { force: true });
|
|
18332
18690
|
}
|
|
18333
18691
|
encodeStageMs = Date.now() - encodeStarted;
|
|
18334
18692
|
} finally {
|
|
@@ -18370,8 +18728,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18370
18728
|
writeFileSync12(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
18371
18729
|
`, "utf-8");
|
|
18372
18730
|
try {
|
|
18373
|
-
|
|
18374
|
-
|
|
18731
|
+
rmSync16(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
18732
|
+
rmSync16(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
18375
18733
|
} catch (err) {
|
|
18376
18734
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
18377
18735
|
workDir,
|
|
@@ -18399,18 +18757,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18399
18757
|
|
|
18400
18758
|
// src/services/distributed/planV2Execution.ts
|
|
18401
18759
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
|
|
18402
|
-
const workRoot =
|
|
18403
|
-
const materializedPlanDir =
|
|
18760
|
+
const workRoot = mkdtempSync9(join40(tmpdir5(), "hf-plan-v2-chunk-"));
|
|
18761
|
+
const materializedPlanDir = join40(workRoot, "plan");
|
|
18404
18762
|
try {
|
|
18405
18763
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
18406
18764
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
|
|
18407
18765
|
} finally {
|
|
18408
|
-
|
|
18766
|
+
rmSync17(workRoot, { recursive: true, force: true });
|
|
18409
18767
|
}
|
|
18410
18768
|
}
|
|
18411
18769
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
18412
|
-
const workRoot =
|
|
18413
|
-
const materializedPlanDir =
|
|
18770
|
+
const workRoot = mkdtempSync9(join40(tmpdir5(), "hf-plan-v2-assembler-"));
|
|
18771
|
+
const materializedPlanDir = join40(workRoot, "plan");
|
|
18414
18772
|
try {
|
|
18415
18773
|
const materialized = materializePlanV2Target(
|
|
18416
18774
|
planV2Dir,
|
|
@@ -18425,7 +18783,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
18425
18783
|
options
|
|
18426
18784
|
);
|
|
18427
18785
|
} finally {
|
|
18428
|
-
|
|
18786
|
+
rmSync17(workRoot, { recursive: true, force: true });
|
|
18429
18787
|
}
|
|
18430
18788
|
}
|
|
18431
18789
|
export {
|