@hyperframes/producer 0.8.177 → 0.8.179
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 -3
- package/dist/distributed.js +1472 -1239
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +57 -55
- package/dist/index.js +1364 -1154
- package/dist/public-server.js +40 -13
- package/dist/services/distributed/chunkAudio.d.ts +11 -0
- package/dist/services/distributed/chunkFrames.d.ts +4 -0
- package/dist/services/distributed/mp4Stitch.d.ts +1 -0
- package/dist/services/distributed/plan.d.ts +10 -4
- package/dist/services/distributed/planV2.d.ts +15 -2
- package/dist/services/distributed/planV2Publisher.d.ts +1 -0
- package/dist/services/distributed/projectHash.d.ts +13 -8
- package/dist/services/htmlCompiler.d.ts +2 -0
- package/dist/services/render/stages/audioStage.d.ts +1 -0
- package/dist/services/render/stages/compileStage.d.ts +2 -0
- package/dist/services/render/stages/extractVideosStage.d.ts +1 -0
- package/dist/services/render/stages/freezePlan.d.ts +8 -0
- package/package.json +7 -7
- package/dist/services/audioExtractor.d.ts +0 -38
package/dist/index.js
CHANGED
|
@@ -1387,9 +1387,9 @@ import {
|
|
|
1387
1387
|
} from "node:fs";
|
|
1388
1388
|
import { hostname, uptime } from "node:os";
|
|
1389
1389
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1390
|
+
import { RENDER_TEMP_DIR } from "@hyperframes/core/project-folders";
|
|
1390
1391
|
var OWNER_FILE = "owner.json";
|
|
1391
1392
|
var TRANSACTION_BACKUP = "backup";
|
|
1392
|
-
var RENDER_TEMP_DIR = /^(work-[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}|hf-render|\..+\.hf-transaction)-[A-Za-z0-9]{6}$/;
|
|
1393
1393
|
var RENDER_JOB_DIR = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/;
|
|
1394
1394
|
function pidScope() {
|
|
1395
1395
|
const attempt = (read) => {
|
|
@@ -6067,7 +6067,7 @@ function detectShaderTransitionUsage(html) {
|
|
|
6067
6067
|
}
|
|
6068
6068
|
return false;
|
|
6069
6069
|
}
|
|
6070
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log) {
|
|
6070
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
6071
6071
|
let filePath = src;
|
|
6072
6072
|
if (isHttpUrl(src)) {
|
|
6073
6073
|
if (!existsSync10(downloadDir)) mkdirSync6(downloadDir, { recursive: true });
|
|
@@ -6081,8 +6081,9 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6081
6081
|
} else {
|
|
6082
6082
|
filePath = decodedUrlPath(src);
|
|
6083
6083
|
if (!filePath.startsWith("/")) filePath = join13(baseDir, filePath);
|
|
6084
|
+
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
6084
6085
|
}
|
|
6085
|
-
if (!existsSync10(filePath)) {
|
|
6086
|
+
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
6086
6087
|
return { duration: null, resolvedPath: filePath };
|
|
6087
6088
|
}
|
|
6088
6089
|
const withSrcContext = (error) => {
|
|
@@ -6145,7 +6146,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
6145
6146
|
}
|
|
6146
6147
|
return changed ? document2.toString() : html;
|
|
6147
6148
|
}
|
|
6148
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log) {
|
|
6149
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
6149
6150
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
6150
6151
|
const mediaUnresolved = unresolved.filter(
|
|
6151
6152
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -6161,7 +6162,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log) {
|
|
|
6161
6162
|
downloadDir,
|
|
6162
6163
|
el.tagName,
|
|
6163
6164
|
el.id,
|
|
6164
|
-
log
|
|
6165
|
+
log,
|
|
6166
|
+
urlByAbsentLocalPath
|
|
6165
6167
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
6166
6168
|
)
|
|
6167
6169
|
);
|
|
@@ -6203,7 +6205,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log) {
|
|
|
6203
6205
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
6204
6206
|
return { html: compiledHtml, unresolvedCompositions };
|
|
6205
6207
|
}
|
|
6206
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set()) {
|
|
6208
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
6207
6209
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
6208
6210
|
const { document: document2 } = parseHTML4(html);
|
|
6209
6211
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
@@ -6229,13 +6231,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6229
6231
|
const { html: compiledSub } = await compileHtmlFile(
|
|
6230
6232
|
item.rawSubHtml,
|
|
6231
6233
|
dirname6(item.filePath),
|
|
6232
|
-
downloadDir
|
|
6234
|
+
downloadDir,
|
|
6235
|
+
void 0,
|
|
6236
|
+
urlByAbsentLocalPath
|
|
6233
6237
|
);
|
|
6234
6238
|
const nested = await parseSubCompositions(
|
|
6235
6239
|
compiledSub,
|
|
6236
6240
|
projectDir,
|
|
6237
6241
|
downloadDir,
|
|
6238
|
-
item.nestedVisited
|
|
6242
|
+
item.nestedVisited,
|
|
6243
|
+
urlByAbsentLocalPath
|
|
6239
6244
|
);
|
|
6240
6245
|
return {
|
|
6241
6246
|
srcPath: item.srcPath,
|
|
@@ -6993,9 +6998,16 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
6993
6998
|
rawHtml,
|
|
6994
6999
|
projectDir,
|
|
6995
7000
|
downloadDir,
|
|
6996
|
-
options.log
|
|
7001
|
+
options.log,
|
|
7002
|
+
options.urlByAbsentLocalPath
|
|
7003
|
+
);
|
|
7004
|
+
const { subCompositions } = await parseSubCompositions(
|
|
7005
|
+
compiledHtml,
|
|
7006
|
+
projectDir,
|
|
7007
|
+
downloadDir,
|
|
7008
|
+
void 0,
|
|
7009
|
+
options.urlByAbsentLocalPath
|
|
6997
7010
|
);
|
|
6998
|
-
const { subCompositions } = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
|
|
6999
7011
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
7000
7012
|
const inlinedHtml = inlineSubCompositions(
|
|
7001
7013
|
fullHtml,
|
|
@@ -7491,6 +7503,7 @@ async function runCompileStage(input) {
|
|
|
7491
7503
|
allowSystemFontCapture,
|
|
7492
7504
|
abortSignal,
|
|
7493
7505
|
variables: input.variables,
|
|
7506
|
+
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
7494
7507
|
animatedGifCacheDir: cfg.extractCacheDir ? join14(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
7495
7508
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
7496
7509
|
});
|
|
@@ -8489,7 +8502,8 @@ async function runExtractVideosStage(input) {
|
|
|
8489
8502
|
abortSignal,
|
|
8490
8503
|
assertNotAborted,
|
|
8491
8504
|
materializeSymlinks,
|
|
8492
|
-
deferRangeExtraction
|
|
8505
|
+
deferRangeExtraction,
|
|
8506
|
+
urlByAbsentLocalPath
|
|
8493
8507
|
} = input;
|
|
8494
8508
|
const stage2Start = Date.now();
|
|
8495
8509
|
const extractionPolicy = resolveVideoExtractionPolicy();
|
|
@@ -8583,7 +8597,8 @@ async function runExtractVideosStage(input) {
|
|
|
8583
8597
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
8584
8598
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
8585
8599
|
deferRangeExtraction,
|
|
8586
|
-
contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)]
|
|
8600
|
+
contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
8601
|
+
urlByAbsentLocalPath
|
|
8587
8602
|
},
|
|
8588
8603
|
abortSignal,
|
|
8589
8604
|
cfg,
|
|
@@ -8666,6 +8681,7 @@ async function runAudioStage(input) {
|
|
|
8666
8681
|
audioGain,
|
|
8667
8682
|
audios,
|
|
8668
8683
|
masterFxChain,
|
|
8684
|
+
urlByAbsentLocalPath,
|
|
8669
8685
|
abortSignal,
|
|
8670
8686
|
assertNotAborted,
|
|
8671
8687
|
log
|
|
@@ -8685,7 +8701,7 @@ async function runAudioStage(input) {
|
|
|
8685
8701
|
audioOutputPath,
|
|
8686
8702
|
duration,
|
|
8687
8703
|
abortSignal,
|
|
8688
|
-
{ ffmpegProcessTimeout, audioGain, masterFxChain },
|
|
8704
|
+
{ ffmpegProcessTimeout, audioGain, masterFxChain, urlByAbsentLocalPath },
|
|
8689
8705
|
compiledDir
|
|
8690
8706
|
);
|
|
8691
8707
|
} catch (err) {
|
|
@@ -13577,14 +13593,14 @@ import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframe
|
|
|
13577
13593
|
import {
|
|
13578
13594
|
copyFileSync as copyFileSync5,
|
|
13579
13595
|
cpSync as cpSync2,
|
|
13580
|
-
existsSync as
|
|
13596
|
+
existsSync as existsSync21,
|
|
13581
13597
|
linkSync,
|
|
13582
|
-
mkdirSync as
|
|
13583
|
-
renameSync as
|
|
13584
|
-
rmSync as
|
|
13585
|
-
writeFileSync as
|
|
13598
|
+
mkdirSync as mkdirSync13,
|
|
13599
|
+
renameSync as renameSync6,
|
|
13600
|
+
rmSync as rmSync11,
|
|
13601
|
+
writeFileSync as writeFileSync8
|
|
13586
13602
|
} from "node:fs";
|
|
13587
|
-
import { extname as extname6, isAbsolute as isAbsolute3, join as
|
|
13603
|
+
import { extname as extname6, isAbsolute as isAbsolute3, join as join34, relative as relative5, sep as sep2 } from "node:path";
|
|
13588
13604
|
import {
|
|
13589
13605
|
fpsToNumber as fpsToNumber10,
|
|
13590
13606
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -13990,6 +14006,8 @@ function buildChunkCaptureOptions(input) {
|
|
|
13990
14006
|
// The controller's snapshot, so the first capture sees the in-process renderer's `__hfVariables`.
|
|
13991
14007
|
variables: encoder.variables,
|
|
13992
14008
|
subTimelineWaitMemo: {},
|
|
14009
|
+
skipReadinessVideoIds: input.skipReadinessVideoIds,
|
|
14010
|
+
videoMetadataHints: input.videoMetadataHints,
|
|
13993
14011
|
...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
|
|
13994
14012
|
// A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
|
|
13995
14013
|
lockWarmupTicks: true,
|
|
@@ -13998,6 +14016,19 @@ function buildChunkCaptureOptions(input) {
|
|
|
13998
14016
|
}
|
|
13999
14017
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
14000
14018
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
14019
|
+
const remoteSources = input.remoteSources ?? {};
|
|
14020
|
+
for (const path of Object.keys(remoteSources)) {
|
|
14021
|
+
if (!deferred.some((video) => video.deferredRange.sourcePath === path)) {
|
|
14022
|
+
throw new RenderChunkValidationError(
|
|
14023
|
+
MISSING_PLAN_ARTIFACT,
|
|
14024
|
+
`[renderChunk] ${path} is read by URL but no deferred video uses it`
|
|
14025
|
+
);
|
|
14026
|
+
}
|
|
14027
|
+
defaultLogger.info("[renderChunk] reading video by URL", {
|
|
14028
|
+
path,
|
|
14029
|
+
url: remoteSources[path].split("?")[0]
|
|
14030
|
+
});
|
|
14031
|
+
}
|
|
14001
14032
|
if (deferred.length === 0) return [];
|
|
14002
14033
|
const table = createFrameLookupTable2(
|
|
14003
14034
|
input.planVideos.videos,
|
|
@@ -14025,8 +14056,9 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14025
14056
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
14026
14057
|
}
|
|
14027
14058
|
const range = video.deferredRange;
|
|
14028
|
-
const
|
|
14029
|
-
|
|
14059
|
+
const remoteUrl = remoteSources[range.sourcePath];
|
|
14060
|
+
const sourcePath = remoteUrl ?? join28(input.planDir, ...range.sourcePath.split("/"));
|
|
14061
|
+
if (remoteUrl === void 0 && !existsSync18(sourcePath)) {
|
|
14030
14062
|
throw new RenderChunkValidationError(
|
|
14031
14063
|
MISSING_PLAN_ARTIFACT,
|
|
14032
14064
|
`[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
|
|
@@ -14366,6 +14398,16 @@ function collectPlanAssetShas(planDir, digests) {
|
|
|
14366
14398
|
}
|
|
14367
14399
|
return { compositionHtml, assets };
|
|
14368
14400
|
}
|
|
14401
|
+
var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
14402
|
+
function writePlanRemoteFiles(planDir, files) {
|
|
14403
|
+
if (files.length === 0) return;
|
|
14404
|
+
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
14405
|
+
writeFileSync6(join30(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
14406
|
+
}
|
|
14407
|
+
function readPlanRemoteFiles(planDir) {
|
|
14408
|
+
const path = join30(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
14409
|
+
return existsSync19(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
|
|
14410
|
+
}
|
|
14369
14411
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
14370
14412
|
const planJsonPath = join30(planDir, "plan.json");
|
|
14371
14413
|
const encoderJsonPath = join30(planDir, "meta", "encoder.json");
|
|
@@ -14481,170 +14523,680 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
14481
14523
|
};
|
|
14482
14524
|
}
|
|
14483
14525
|
|
|
14484
|
-
// src/services/distributed/
|
|
14485
|
-
import {
|
|
14486
|
-
import {
|
|
14487
|
-
import {
|
|
14488
|
-
|
|
14489
|
-
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
|
|
14504
|
-
|
|
14505
|
-
|
|
14506
|
-
|
|
14507
|
-
|
|
14508
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
meta: "metadata",
|
|
14512
|
-
"plan.json": "metadata"
|
|
14513
|
-
};
|
|
14514
|
-
function hashComponent(value) {
|
|
14515
|
-
return createHash7("sha256").update(value).digest("hex").slice(0, 12);
|
|
14526
|
+
// src/services/distributed/chunkAudio.ts
|
|
14527
|
+
import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
|
|
14528
|
+
import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
|
|
14529
|
+
import { join as join32 } from "node:path";
|
|
14530
|
+
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure2, runFfmpeg as runFfmpeg5 } from "@hyperframes/engine";
|
|
14531
|
+
|
|
14532
|
+
// src/services/distributed/mp4Stitch.ts
|
|
14533
|
+
import {
|
|
14534
|
+
createReadStream as createReadStream4,
|
|
14535
|
+
createWriteStream,
|
|
14536
|
+
mkdirSync as mkdirSync11,
|
|
14537
|
+
mkdtempSync as mkdtempSync4,
|
|
14538
|
+
renameSync as renameSync5,
|
|
14539
|
+
rmSync as rmSync10
|
|
14540
|
+
} from "node:fs";
|
|
14541
|
+
import { open } from "node:fs/promises";
|
|
14542
|
+
import { dirname as dirname12, join as join31 } from "node:path";
|
|
14543
|
+
import { finished, pipeline } from "node:stream/promises";
|
|
14544
|
+
function boxAt(src, off, at, limit) {
|
|
14545
|
+
let size = src.readUInt32BE(off);
|
|
14546
|
+
let bodyStart = at + 8;
|
|
14547
|
+
if (size === 1) {
|
|
14548
|
+
size = Number(src.readBigUInt64BE(off + 8));
|
|
14549
|
+
bodyStart = at + 16;
|
|
14550
|
+
}
|
|
14551
|
+
if (size < 8 || at + size > limit) throw new Error(`[mp4Stitch] malformed box at ${at}`);
|
|
14552
|
+
return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
|
|
14516
14553
|
}
|
|
14517
|
-
function
|
|
14518
|
-
const
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
const videoKey = segments[1] ?? "unknown";
|
|
14522
|
-
return {
|
|
14523
|
-
category: "video-frames",
|
|
14524
|
-
componentLabel: `video-frames:${hashComponent(videoKey)}`
|
|
14525
|
-
};
|
|
14554
|
+
function childBoxes(buf, parent) {
|
|
14555
|
+
const boxes = [];
|
|
14556
|
+
for (let at = parent.bodyStart; at < parent.end; at = boxes.at(-1).end) {
|
|
14557
|
+
boxes.push(boxAt(buf, at, at, parent.end));
|
|
14526
14558
|
}
|
|
14527
|
-
|
|
14528
|
-
return { category, componentLabel: category };
|
|
14559
|
+
return boxes;
|
|
14529
14560
|
}
|
|
14530
|
-
function
|
|
14531
|
-
return
|
|
14532
|
-
totalBytes: 0,
|
|
14533
|
-
fileCount: 0,
|
|
14534
|
-
compiledBytes: 0,
|
|
14535
|
-
sourceMediaBytes: 0,
|
|
14536
|
-
videoFramesBytes: 0,
|
|
14537
|
-
videoFrameFileCount: 0,
|
|
14538
|
-
audioBytes: 0,
|
|
14539
|
-
metadataBytes: 0,
|
|
14540
|
-
otherBytes: 0,
|
|
14541
|
-
topComponents: []
|
|
14542
|
-
};
|
|
14561
|
+
function findChild(buf, parent, type) {
|
|
14562
|
+
return childBoxes(buf, parent).find((box2) => box2.type === type);
|
|
14543
14563
|
}
|
|
14544
|
-
function
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
result.sourceMediaBytes += sizeBytes;
|
|
14549
|
-
}
|
|
14550
|
-
return;
|
|
14551
|
-
}
|
|
14552
|
-
if (category === "video-frames") {
|
|
14553
|
-
result.videoFramesBytes += sizeBytes;
|
|
14554
|
-
result.videoFrameFileCount += 1;
|
|
14555
|
-
return;
|
|
14556
|
-
}
|
|
14557
|
-
if (category === "audio") {
|
|
14558
|
-
result.audioBytes += sizeBytes;
|
|
14559
|
-
return;
|
|
14560
|
-
}
|
|
14561
|
-
if (category === "metadata") {
|
|
14562
|
-
result.metadataBytes += sizeBytes;
|
|
14563
|
-
return;
|
|
14564
|
-
}
|
|
14565
|
-
result.otherBytes += sizeBytes;
|
|
14564
|
+
function child(buf, parent, type) {
|
|
14565
|
+
const found = findChild(buf, parent, type);
|
|
14566
|
+
if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
|
|
14567
|
+
return found;
|
|
14566
14568
|
}
|
|
14567
|
-
|
|
14568
|
-
|
|
14569
|
-
|
|
14570
|
-
|
|
14571
|
-
|
|
14572
|
-
|
|
14573
|
-
|
|
14574
|
-
return;
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
const segments = rel.split(/[\\/]+/).filter(Boolean);
|
|
14578
|
-
const classified = classifyFile(segments, rootKind);
|
|
14579
|
-
result.totalBytes += sizeBytes;
|
|
14580
|
-
result.fileCount += 1;
|
|
14581
|
-
addCategoryBytes(result, classified.category, filePath, sizeBytes);
|
|
14582
|
-
const component = components.get(classified.componentLabel) ?? {
|
|
14583
|
-
label: classified.componentLabel,
|
|
14584
|
-
sizeBytes: 0,
|
|
14585
|
-
fileCount: 0
|
|
14569
|
+
var SCAN_BYTES = 64 * 1024;
|
|
14570
|
+
async function readMp4Layout(read) {
|
|
14571
|
+
const found = /* @__PURE__ */ new Map();
|
|
14572
|
+
let payload;
|
|
14573
|
+
let buf = Buffer.alloc(0);
|
|
14574
|
+
let bufAt = 0;
|
|
14575
|
+
const load = async (start, end) => {
|
|
14576
|
+
if (start >= bufAt && end <= bufAt + buf.length) return;
|
|
14577
|
+
buf = await read(start, Math.max(end, start + SCAN_BYTES));
|
|
14578
|
+
bufAt = start;
|
|
14586
14579
|
};
|
|
14587
|
-
|
|
14588
|
-
|
|
14589
|
-
|
|
14590
|
-
}
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
}
|
|
14598
|
-
for (const entry of entries) {
|
|
14599
|
-
const full = join31(dir, entry.name);
|
|
14600
|
-
if (entry.isDirectory()) {
|
|
14601
|
-
walkRegularFiles(full, visit);
|
|
14602
|
-
} else if (entry.isFile()) {
|
|
14603
|
-
visit(full);
|
|
14580
|
+
for (let at = 0; !(found.has("ftyp") && found.has("moov") && payload); ) {
|
|
14581
|
+
await load(at, at + 16);
|
|
14582
|
+
if (at + 8 > bufAt + buf.length) {
|
|
14583
|
+
throw new Error(`[mp4Stitch] chunk needs ftyp, moov and mdat; found ${[...found.keys()]}`);
|
|
14584
|
+
}
|
|
14585
|
+
const box2 = boxAt(buf, at - bufAt, at, Number.MAX_SAFE_INTEGER);
|
|
14586
|
+
if (box2.type === "mdat") payload ??= box2;
|
|
14587
|
+
if (box2.type === "ftyp" || box2.type === "moov") {
|
|
14588
|
+
await load(at, box2.end);
|
|
14589
|
+
found.set(box2.type, buf.subarray(at - bufAt, box2.end - bufAt));
|
|
14604
14590
|
}
|
|
14591
|
+
at = box2.end;
|
|
14605
14592
|
}
|
|
14593
|
+
const [ftyp, moov] = [found.get("ftyp"), found.get("moov")];
|
|
14594
|
+
if (moov.length !== moovSize(moov)) throw new Error("[mp4Stitch] moov is cut off");
|
|
14595
|
+
return { ftyp, moov, payloadStart: payload.bodyStart, payloadEnd: payload.end };
|
|
14606
14596
|
}
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
const
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14597
|
+
var moovSize = (moov) => boxAt(moov, 0, 0, Number.MAX_SAFE_INTEGER).end;
|
|
14598
|
+
function readTracks(moov) {
|
|
14599
|
+
const root = boxAt(moov, 0, 0, moov.length);
|
|
14600
|
+
return childBoxes(moov, root).filter((box2) => box2.type === "trak").map((trak) => readTrack(moov, trak));
|
|
14601
|
+
}
|
|
14602
|
+
function rows(buf, table, columns, countAt = 4) {
|
|
14603
|
+
const count = buf.readUInt32BE(table.bodyStart + countAt);
|
|
14604
|
+
const first = table.bodyStart + countAt + 4;
|
|
14605
|
+
return Array.from(
|
|
14606
|
+
{ length: count },
|
|
14607
|
+
(_, row) => Array.from(
|
|
14608
|
+
{ length: columns },
|
|
14609
|
+
(_2, col) => buf.readUInt32BE(first + (row * columns + col) * 4)
|
|
14610
|
+
)
|
|
14613
14611
|
);
|
|
14614
|
-
result.topComponents = [...components.values()].sort((a, b) => b.sizeBytes - a.sizeBytes || a.label.localeCompare(b.label)).slice(0, MAX_TOP_COMPONENTS);
|
|
14615
|
-
return result;
|
|
14616
14612
|
}
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14613
|
+
function readMediaTime(moov, trak) {
|
|
14614
|
+
const edts = findChild(moov, trak, "edts");
|
|
14615
|
+
if (!edts) return 0;
|
|
14616
|
+
const elst = child(moov, edts, "elst");
|
|
14617
|
+
const wide = moov[elst.bodyStart] === 1;
|
|
14618
|
+
const entries = moov.readUInt32BE(elst.bodyStart + 4);
|
|
14619
|
+
for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
|
|
14620
|
+
const offset = at + (wide ? 8 : 4);
|
|
14621
|
+
const mediaTime = wide ? Number(moov.readBigInt64BE(offset)) : moov.readInt32BE(offset);
|
|
14622
|
+
if (mediaTime !== -1) return mediaTime;
|
|
14623
|
+
}
|
|
14624
|
+
return 0;
|
|
14621
14625
|
}
|
|
14622
|
-
function
|
|
14623
|
-
const
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
job,
|
|
14627
|
-
[
|
|
14628
|
-
{
|
|
14629
|
-
code: "audio_processing_failed",
|
|
14630
|
-
message: `Audio mix failed; output would be video-only: ${audioError}`,
|
|
14631
|
-
details: {
|
|
14632
|
-
mediaType: "audio",
|
|
14633
|
-
audioFailures: audioFailureDiagnostics3(audioFailures),
|
|
14634
|
-
failureReasons: [...new Set(audioFailures.map((failure) => failure.reason))],
|
|
14635
|
-
failureStages: [...new Set(audioFailures.map((failure) => failure.stage))],
|
|
14636
|
-
failureOwner,
|
|
14637
|
-
retryable
|
|
14638
|
-
}
|
|
14639
|
-
}
|
|
14640
|
-
],
|
|
14641
|
-
log
|
|
14642
|
-
);
|
|
14626
|
+
function readSizes(moov, stsz) {
|
|
14627
|
+
const fixedSize = moov.readUInt32BE(stsz.bodyStart + 4);
|
|
14628
|
+
if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
|
|
14629
|
+
return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
|
|
14643
14630
|
}
|
|
14644
|
-
|
|
14645
|
-
"
|
|
14646
|
-
|
|
14647
|
-
|
|
14631
|
+
function readSync4(moov, stbl, sampleCount) {
|
|
14632
|
+
const stss = findChild(moov, stbl, "stss");
|
|
14633
|
+
if (!stss) return null;
|
|
14634
|
+
const sync = Array(sampleCount).fill(false);
|
|
14635
|
+
for (const [n] of rows(moov, stss, 1)) sync[n - 1] = true;
|
|
14636
|
+
return sync;
|
|
14637
|
+
}
|
|
14638
|
+
function readChunkOffsets(moov, stbl) {
|
|
14639
|
+
const stco = findChild(moov, stbl, "stco");
|
|
14640
|
+
if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
|
|
14641
|
+
return rows(moov, child(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
|
|
14642
|
+
}
|
|
14643
|
+
function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
14644
|
+
const stsc = rows(moov, child(moov, stbl, "stsc"), 3);
|
|
14645
|
+
return stsc.flatMap(([firstChunk, perChunk, description], i) => {
|
|
14646
|
+
if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
|
|
14647
|
+
const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
|
|
14648
|
+
return Array(nextFirst - firstChunk).fill(perChunk);
|
|
14649
|
+
});
|
|
14650
|
+
}
|
|
14651
|
+
function readTrack(moov, trak) {
|
|
14652
|
+
const mdia = child(moov, trak, "mdia");
|
|
14653
|
+
const hdlr = child(moov, mdia, "hdlr");
|
|
14654
|
+
const stbl = child(moov, child(moov, mdia, "minf"), "stbl");
|
|
14655
|
+
if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
|
|
14656
|
+
const stts = rows(moov, child(moov, stbl, "stts"), 2);
|
|
14657
|
+
const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
|
|
14658
|
+
const sizes = readSizes(moov, child(moov, stbl, "stsz"));
|
|
14659
|
+
const chunkOffsets = readChunkOffsets(moov, stbl);
|
|
14660
|
+
const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
|
|
14661
|
+
if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
|
|
14662
|
+
throw new Error("[mp4Stitch] sample tables disagree on the sample count");
|
|
14663
|
+
}
|
|
14664
|
+
const stsd = child(moov, stbl, "stsd");
|
|
14665
|
+
return {
|
|
14666
|
+
trak,
|
|
14667
|
+
handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
|
|
14668
|
+
timescale: timescaleOf(moov, child(moov, mdia, "mdhd")),
|
|
14669
|
+
mediaTime: readMediaTime(moov, trak),
|
|
14670
|
+
stsd: moov.subarray(stsd.start, stsd.end),
|
|
14671
|
+
durations,
|
|
14672
|
+
sizes,
|
|
14673
|
+
sync: readSync4(moov, stbl, sizes.length),
|
|
14674
|
+
samplesPerChunk,
|
|
14675
|
+
chunkOffsets
|
|
14676
|
+
};
|
|
14677
|
+
}
|
|
14678
|
+
function sum(values) {
|
|
14679
|
+
return values.reduce((total, value) => total + value, 0);
|
|
14680
|
+
}
|
|
14681
|
+
function box(type, ...body) {
|
|
14682
|
+
const header = Buffer.alloc(8);
|
|
14683
|
+
header.writeUInt32BE(8 + sum(body.map((part) => part.length)));
|
|
14684
|
+
header.write(type, 4, "latin1");
|
|
14685
|
+
return Buffer.concat([header, ...body]);
|
|
14686
|
+
}
|
|
14687
|
+
function fullBox(type, version, body) {
|
|
14688
|
+
return box(type, Buffer.from([version, 0, 0, 0]), body);
|
|
14689
|
+
}
|
|
14690
|
+
function u32s(values) {
|
|
14691
|
+
const buf = Buffer.alloc(values.length * 4);
|
|
14692
|
+
values.forEach((value, i) => buf.writeUInt32BE(value, i * 4));
|
|
14693
|
+
return buf;
|
|
14694
|
+
}
|
|
14695
|
+
function runs(values) {
|
|
14696
|
+
const out = [];
|
|
14697
|
+
for (const value of values) {
|
|
14698
|
+
const last = out.at(-1);
|
|
14699
|
+
if (last && last[1] === value) last[0]++;
|
|
14700
|
+
else out.push([1, value]);
|
|
14701
|
+
}
|
|
14702
|
+
return out;
|
|
14703
|
+
}
|
|
14704
|
+
function rebuild(buf, parent, replace) {
|
|
14705
|
+
const parts = childBoxes(buf, parent).map(
|
|
14706
|
+
(box2) => replace[box2.type]?.(box2) ?? buf.subarray(box2.start, box2.end)
|
|
14707
|
+
);
|
|
14708
|
+
return box(parent.type, ...parts);
|
|
14709
|
+
}
|
|
14710
|
+
var DURATION_AT = {
|
|
14711
|
+
mvhd: [16, 24],
|
|
14712
|
+
mdhd: [16, 24],
|
|
14713
|
+
tkhd: [20, 28]
|
|
14714
|
+
};
|
|
14715
|
+
function withDuration(buf, target, duration) {
|
|
14716
|
+
const copy = Buffer.from(buf.subarray(target.start, target.end));
|
|
14717
|
+
const body = target.bodyStart - target.start;
|
|
14718
|
+
const wide = copy[body] === 1;
|
|
14719
|
+
const at = body + DURATION_AT[target.type][wide ? 1 : 0];
|
|
14720
|
+
if (wide) copy.writeBigUInt64BE(BigInt(duration), at);
|
|
14721
|
+
else if (duration <= 4294967295) copy.writeUInt32BE(duration, at);
|
|
14722
|
+
else throw new Error(`[mp4Stitch] ${target.type} duration ${duration} needs a version 1 box`);
|
|
14723
|
+
return copy;
|
|
14724
|
+
}
|
|
14725
|
+
function timescaleOf(buf, header) {
|
|
14726
|
+
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
14727
|
+
}
|
|
14728
|
+
function buildStbl(stsd, track) {
|
|
14729
|
+
const stts = runs(track.durations);
|
|
14730
|
+
const stsc = [];
|
|
14731
|
+
let firstChunk = 1;
|
|
14732
|
+
for (const [count, perChunk] of runs(track.samplesPerChunk)) {
|
|
14733
|
+
stsc.push([firstChunk, perChunk, 1]);
|
|
14734
|
+
firstChunk += count;
|
|
14735
|
+
}
|
|
14736
|
+
const co64 = Buffer.alloc(4 + track.chunkOffsets.length * 8);
|
|
14737
|
+
co64.writeUInt32BE(track.chunkOffsets.length);
|
|
14738
|
+
track.chunkOffsets.forEach((offset, i) => co64.writeBigUInt64BE(BigInt(offset), 4 + i * 8));
|
|
14739
|
+
const syncSamples = track.sync?.flatMap((isSync, i) => isSync ? [i + 1] : []) ?? null;
|
|
14740
|
+
return box(
|
|
14741
|
+
"stbl",
|
|
14742
|
+
stsd,
|
|
14743
|
+
fullBox("stts", 0, u32s([stts.length, ...stts.flat()])),
|
|
14744
|
+
...syncSamples ? [fullBox("stss", 0, u32s([syncSamples.length, ...syncSamples]))] : [],
|
|
14745
|
+
fullBox("stsc", 0, u32s([stsc.length, ...stsc.flat()])),
|
|
14746
|
+
fullBox("stsz", 0, u32s([0, track.sizes.length, ...track.sizes])),
|
|
14747
|
+
fullBox("co64", 0, co64)
|
|
14748
|
+
);
|
|
14749
|
+
}
|
|
14750
|
+
var codecOf = (track) => track.stsd.toString("latin1", 20, 24);
|
|
14751
|
+
var sameTrack = (track, plan2) => track.handler === plan2.handler && track.timescale === plan2.timescale && codecOf(track) === codecOf(plan2);
|
|
14752
|
+
function planTracksOf(tracks, plan2, index) {
|
|
14753
|
+
let next = 0;
|
|
14754
|
+
const matched = plan2.map((planTrack) => {
|
|
14755
|
+
const track = tracks[next];
|
|
14756
|
+
if (track && sameTrack(track, planTrack)) return tracks[next++];
|
|
14757
|
+
if (planTrack.handler === "soun") return null;
|
|
14758
|
+
throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
|
|
14759
|
+
});
|
|
14760
|
+
if (next !== tracks.length)
|
|
14761
|
+
throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
|
|
14762
|
+
for (const track of tracks) {
|
|
14763
|
+
if (track.handler === "vide" && track.mediaTime !== 0) {
|
|
14764
|
+
throw new Error(`[mp4Stitch] chunk ${index} video starts at media time ${track.mediaTime}`);
|
|
14765
|
+
}
|
|
14766
|
+
}
|
|
14767
|
+
return matched;
|
|
14768
|
+
}
|
|
14769
|
+
function buildStitchedHeader(layouts) {
|
|
14770
|
+
const plan2 = readTracks(layouts[0].moov);
|
|
14771
|
+
const chunks = layouts.map((layout, i) => planTracksOf(readTracks(layout.moov), plan2, i));
|
|
14772
|
+
const payloadBases = [];
|
|
14773
|
+
let payloadBytes = 0;
|
|
14774
|
+
for (const layout of layouts) {
|
|
14775
|
+
payloadBases.push(payloadBytes);
|
|
14776
|
+
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
14777
|
+
}
|
|
14778
|
+
const template = layouts[0].moov;
|
|
14779
|
+
const root = boxAt(template, 0, 0, template.length);
|
|
14780
|
+
const movieTimescale = timescaleOf(template, child(template, root, "mvhd"));
|
|
14781
|
+
const buildMoov = (dataStart2) => {
|
|
14782
|
+
const traks = plan2.map((track, t) => {
|
|
14783
|
+
const parts = chunks.flatMap((tracks, chunk) => {
|
|
14784
|
+
const part = tracks[t];
|
|
14785
|
+
return part ? [{ ...part, chunk }] : [];
|
|
14786
|
+
});
|
|
14787
|
+
const merged = {
|
|
14788
|
+
durations: parts.flatMap((part) => part.durations),
|
|
14789
|
+
sizes: parts.flatMap((part) => part.sizes),
|
|
14790
|
+
sync: parts.every((part) => !part.sync) ? null : parts.flatMap((part) => part.sync ?? part.sizes.map(() => true)),
|
|
14791
|
+
samplesPerChunk: parts.flatMap((part) => part.samplesPerChunk),
|
|
14792
|
+
chunkOffsets: parts.flatMap(
|
|
14793
|
+
(part) => part.chunkOffsets.map((offset) => {
|
|
14794
|
+
const i = part.chunk;
|
|
14795
|
+
const { payloadStart, payloadEnd } = layouts[i];
|
|
14796
|
+
if (offset < payloadStart || offset >= payloadEnd) {
|
|
14797
|
+
throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
|
|
14798
|
+
}
|
|
14799
|
+
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
14800
|
+
})
|
|
14801
|
+
)
|
|
14802
|
+
};
|
|
14803
|
+
const mediaDuration = sum(merged.durations);
|
|
14804
|
+
const shown = Math.round(
|
|
14805
|
+
(mediaDuration - track.mediaTime) * movieTimescale / track.timescale
|
|
14806
|
+
);
|
|
14807
|
+
const elst = Buffer.alloc(24);
|
|
14808
|
+
elst.writeUInt32BE(1);
|
|
14809
|
+
elst.writeBigUInt64BE(BigInt(shown), 4);
|
|
14810
|
+
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
14811
|
+
elst.writeUInt32BE(65536, 20);
|
|
14812
|
+
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
14813
|
+
const trak = rebuild(template, track.trak, {
|
|
14814
|
+
tkhd: (tkhd) => Buffer.concat([withDuration(template, tkhd, shown), edts]),
|
|
14815
|
+
edts: () => Buffer.alloc(0),
|
|
14816
|
+
mdia: (mdia) => rebuild(template, mdia, {
|
|
14817
|
+
mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
|
|
14818
|
+
minf: (minf) => rebuild(template, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
14819
|
+
})
|
|
14820
|
+
});
|
|
14821
|
+
return { trak, shown };
|
|
14822
|
+
});
|
|
14823
|
+
let next = 0;
|
|
14824
|
+
return rebuild(template, root, {
|
|
14825
|
+
mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
|
|
14826
|
+
trak: () => traks[next++].trak
|
|
14827
|
+
});
|
|
14828
|
+
};
|
|
14829
|
+
const ftyp = layouts[0].ftyp;
|
|
14830
|
+
const dataStart = ftyp.length + buildMoov(0).length + 16;
|
|
14831
|
+
const mdat = Buffer.alloc(16);
|
|
14832
|
+
mdat.writeUInt32BE(1);
|
|
14833
|
+
mdat.write("mdat", 4, "latin1");
|
|
14834
|
+
mdat.writeBigUInt64BE(BigInt(16 + payloadBytes), 8);
|
|
14835
|
+
return Buffer.concat([ftyp, buildMoov(dataStart), mdat]);
|
|
14836
|
+
}
|
|
14837
|
+
function fileRange(path) {
|
|
14838
|
+
return async (start, end) => {
|
|
14839
|
+
const file = await open(path, "r", 384);
|
|
14840
|
+
try {
|
|
14841
|
+
const buf = Buffer.alloc(end - start);
|
|
14842
|
+
const { bytesRead } = await file.read(buf, 0, buf.length, start);
|
|
14843
|
+
return buf.subarray(0, bytesRead);
|
|
14844
|
+
} finally {
|
|
14845
|
+
await file.close();
|
|
14846
|
+
}
|
|
14847
|
+
};
|
|
14848
|
+
}
|
|
14849
|
+
async function copyPayload(path, layout, index, out) {
|
|
14850
|
+
const { payloadStart, payloadEnd } = layout;
|
|
14851
|
+
const payload = createReadStream4(path, { start: payloadStart, end: payloadEnd - 1 });
|
|
14852
|
+
await pipeline(payload, out, { end: false });
|
|
14853
|
+
if (payload.bytesRead !== payloadEnd - payloadStart) {
|
|
14854
|
+
throw new Error(
|
|
14855
|
+
`[mp4Stitch] chunk ${index} mdat holds ${payload.bytesRead} of ${payloadEnd - payloadStart} payload bytes`
|
|
14856
|
+
);
|
|
14857
|
+
}
|
|
14858
|
+
}
|
|
14859
|
+
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
14860
|
+
const layouts = await Promise.all(chunkPaths.map((path) => readMp4Layout(fileRange(path))));
|
|
14861
|
+
const header = buildStitchedHeader(layouts);
|
|
14862
|
+
mkdirSync11(dirname12(outputPath), { recursive: true });
|
|
14863
|
+
const workDir = mkdtempSync4(`${outputPath}.stitch-`);
|
|
14864
|
+
const out = createWriteStream(join31(workDir, "out.mp4"));
|
|
14865
|
+
try {
|
|
14866
|
+
out.write(header);
|
|
14867
|
+
for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
14868
|
+
out.end();
|
|
14869
|
+
await finished(out);
|
|
14870
|
+
renameSync5(join31(workDir, "out.mp4"), outputPath);
|
|
14871
|
+
} finally {
|
|
14872
|
+
out.destroy();
|
|
14873
|
+
rmSync10(workDir, { recursive: true, force: true });
|
|
14874
|
+
}
|
|
14875
|
+
}
|
|
14876
|
+
|
|
14877
|
+
// src/services/distributed/chunkAudio.ts
|
|
14878
|
+
function audioCutForChunk(grid, chunks, index, fps) {
|
|
14879
|
+
const firstPacketAt = (frame) => Math.min(
|
|
14880
|
+
grid.packetCount,
|
|
14881
|
+
Math.max(
|
|
14882
|
+
0,
|
|
14883
|
+
Math.ceil(
|
|
14884
|
+
(frame * fps.den * grid.sampleRate - grid.firstStart * fps.num) / (grid.packetSamples * fps.num)
|
|
14885
|
+
)
|
|
14886
|
+
)
|
|
14887
|
+
);
|
|
14888
|
+
const first = index === 0 ? 0 : firstPacketAt(chunks[index].startFrame);
|
|
14889
|
+
const end = index === chunks.length - 1 ? grid.packetCount : firstPacketAt(chunks[index + 1].startFrame);
|
|
14890
|
+
if (end <= first) return null;
|
|
14891
|
+
const cutSeconds = (packet) => (grid.firstStart + packet * grid.packetSamples - grid.packetSamples / 2) / grid.sampleRate;
|
|
14892
|
+
const seekSeconds = index === 0 ? null : cutSeconds(first);
|
|
14893
|
+
const durationSeconds = end === grid.packetCount ? null : cutSeconds(end) - (seekSeconds ?? 0);
|
|
14894
|
+
return { seekSeconds, durationSeconds, firstPacket: first, packetCount: end - first };
|
|
14895
|
+
}
|
|
14896
|
+
async function probeAacPacketGrid(audioPath) {
|
|
14897
|
+
const probe = await runFfprobeJson([
|
|
14898
|
+
"-v",
|
|
14899
|
+
"error",
|
|
14900
|
+
"-select_streams",
|
|
14901
|
+
"a:0",
|
|
14902
|
+
"-show_entries",
|
|
14903
|
+
"stream=sample_rate,time_base,nb_frames:packet=pts,duration",
|
|
14904
|
+
"-read_intervals",
|
|
14905
|
+
"%+#1",
|
|
14906
|
+
"-of",
|
|
14907
|
+
"json",
|
|
14908
|
+
"--",
|
|
14909
|
+
audioPath
|
|
14910
|
+
]);
|
|
14911
|
+
const stream = probe.streams?.[0] ?? {};
|
|
14912
|
+
const packet = probe.packets?.[0] ?? {};
|
|
14913
|
+
const sampleRate = Number(stream.sample_rate);
|
|
14914
|
+
const [tbNum = NaN, tbDen = NaN] = String(stream.time_base).split("/").map(Number);
|
|
14915
|
+
const toSamples = (ticks) => Number(ticks) * sampleRate * tbNum / tbDen;
|
|
14916
|
+
const grid = {
|
|
14917
|
+
sampleRate,
|
|
14918
|
+
firstStart: toSamples(packet.pts),
|
|
14919
|
+
packetSamples: toSamples(packet.duration),
|
|
14920
|
+
packetCount: Number(stream.nb_frames)
|
|
14921
|
+
};
|
|
14922
|
+
const counts = [grid.sampleRate, grid.packetSamples, grid.packetCount];
|
|
14923
|
+
if (!Number.isFinite(grid.firstStart) || !counts.every((n) => Number.isFinite(n) && n > 0)) {
|
|
14924
|
+
throw new Error(`[chunkAudio] could not read the AAC packet grid: ${JSON.stringify(probe)}`);
|
|
14925
|
+
}
|
|
14926
|
+
return grid;
|
|
14927
|
+
}
|
|
14928
|
+
var SLICE_DIR = "audio-chunks";
|
|
14929
|
+
var SLICE_FRAME = `${SLICE_DIR}/frame.m4a`;
|
|
14930
|
+
var SLICE_LAYOUT = `${SLICE_DIR}/layout.json`;
|
|
14931
|
+
function chunkAudioSlicePath(index) {
|
|
14932
|
+
return `${SLICE_DIR}/${String(index).padStart(4, "0")}.bin`;
|
|
14933
|
+
}
|
|
14934
|
+
function chunkAudioSliceTarget(path) {
|
|
14935
|
+
if (path === SLICE_FRAME || path === SLICE_LAYOUT) return "all";
|
|
14936
|
+
const match = /^audio-chunks\/(\d{4,})\.bin$/.exec(path);
|
|
14937
|
+
return match ? Number(match[1]) : null;
|
|
14938
|
+
}
|
|
14939
|
+
async function writeChunkAudioSlices(input) {
|
|
14940
|
+
const grid = await probeAacPacketGrid(input.audioPath);
|
|
14941
|
+
const { payloadStart, payloadEnd, moov } = await readMp4Layout(fileRange(input.audioPath));
|
|
14942
|
+
const [track] = readTracks(moov);
|
|
14943
|
+
const offsets = [];
|
|
14944
|
+
track.chunkOffsets.forEach((offset, chunk) => {
|
|
14945
|
+
for (let i = 0; i < track.samplesPerChunk[chunk]; i++) {
|
|
14946
|
+
offsets.push(offset);
|
|
14947
|
+
offset += track.sizes[offsets.length - 1];
|
|
14948
|
+
}
|
|
14949
|
+
});
|
|
14950
|
+
const bytes = readFileSync11(input.audioPath);
|
|
14951
|
+
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
14952
|
+
mkdirSync12(join32(input.planDir, SLICE_DIR), { recursive: true });
|
|
14953
|
+
input.chunks.forEach((_, index) => {
|
|
14954
|
+
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
14955
|
+
if (!cut) return;
|
|
14956
|
+
const last = cut.firstPacket + cut.packetCount - 1;
|
|
14957
|
+
const start = offsets[cut.firstPacket];
|
|
14958
|
+
layout.partStarts[index] = start;
|
|
14959
|
+
const end = offsets[last] + track.sizes[last];
|
|
14960
|
+
writeFileSync7(join32(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
14961
|
+
});
|
|
14962
|
+
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
14963
|
+
writeFileSync7(join32(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
14964
|
+
writeFileSync7(join32(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
14965
|
+
}
|
|
14966
|
+
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
14967
|
+
const layoutPath = join32(planDir, SLICE_LAYOUT);
|
|
14968
|
+
if (!existsSync20(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
14969
|
+
const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
|
|
14970
|
+
const frame = readFileSync11(join32(planDir, SLICE_FRAME));
|
|
14971
|
+
const fd = openSync5(scratchPath, "w");
|
|
14972
|
+
try {
|
|
14973
|
+
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
14974
|
+
writeSync(
|
|
14975
|
+
fd,
|
|
14976
|
+
frame,
|
|
14977
|
+
layout.payloadStart,
|
|
14978
|
+
frame.length - layout.payloadStart,
|
|
14979
|
+
layout.payloadEnd
|
|
14980
|
+
);
|
|
14981
|
+
const start = layout.partStarts[chunkIndex];
|
|
14982
|
+
if (start !== void 0) {
|
|
14983
|
+
const part = readFileSync11(join32(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
14984
|
+
writeSync(fd, part, 0, part.length, start);
|
|
14985
|
+
}
|
|
14986
|
+
ftruncateSync(fd, layout.sizeBytes);
|
|
14987
|
+
} finally {
|
|
14988
|
+
closeSync6(fd);
|
|
14989
|
+
}
|
|
14990
|
+
return scratchPath;
|
|
14991
|
+
}
|
|
14992
|
+
function chunkMuxArgs(input, cut) {
|
|
14993
|
+
if (!cut) return ["-i", input.videoPath, "-map", "0:v:0"];
|
|
14994
|
+
const seek = cut.seekSeconds === null ? [] : ["-ss", cut.seekSeconds.toFixed(6)];
|
|
14995
|
+
const length = cut.durationSeconds === null ? [] : ["-t", cut.durationSeconds.toFixed(6)];
|
|
14996
|
+
const exact = seek.length ? ["-copypriorss", "0"] : [];
|
|
14997
|
+
return [
|
|
14998
|
+
...["-i", input.videoPath, ...seek, ...length, "-i", input.audioPath],
|
|
14999
|
+
...["-map", "0:v:0", "-map", "1:a:0", ...exact]
|
|
15000
|
+
];
|
|
15001
|
+
}
|
|
15002
|
+
async function muxChunkWithAudio(input) {
|
|
15003
|
+
const grid = input.audioPath ? await probeAacPacketGrid(input.audioPath) : null;
|
|
15004
|
+
const cut = grid ? audioCutForChunk(grid, input.chunks, input.chunkIndex, input.fps) : null;
|
|
15005
|
+
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
15006
|
+
appendRenderProvenanceArgs(args, input.outputPath);
|
|
15007
|
+
args.push("-y", input.outputPath);
|
|
15008
|
+
const result = await runFfmpeg5(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
15009
|
+
if (!result.success) {
|
|
15010
|
+
throw new Error(
|
|
15011
|
+
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure2(result, input.timeoutMs)}`
|
|
15012
|
+
);
|
|
15013
|
+
}
|
|
15014
|
+
const slice = input.chunks[input.chunkIndex];
|
|
15015
|
+
const expected = { video: slice.endFrame - slice.startFrame };
|
|
15016
|
+
if (cut) expected.audio = cut.packetCount;
|
|
15017
|
+
const probe = await runFfprobeJson([
|
|
15018
|
+
"-v",
|
|
15019
|
+
"error",
|
|
15020
|
+
"-count_packets",
|
|
15021
|
+
"-show_entries",
|
|
15022
|
+
"stream=codec_type,nb_read_packets",
|
|
15023
|
+
"-of",
|
|
15024
|
+
"json",
|
|
15025
|
+
"--",
|
|
15026
|
+
input.outputPath
|
|
15027
|
+
]);
|
|
15028
|
+
for (const [type, count] of Object.entries(expected)) {
|
|
15029
|
+
const packets = Number(probe.streams?.find((s) => s.codec_type === type)?.nb_read_packets ?? 0);
|
|
15030
|
+
if (packets !== count) {
|
|
15031
|
+
throw new Error(`[chunkAudio] ${type} packets in chunk ${packets}, expected ${count}`);
|
|
15032
|
+
}
|
|
15033
|
+
}
|
|
15034
|
+
}
|
|
15035
|
+
|
|
15036
|
+
// src/services/distributed/planSize.ts
|
|
15037
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
15038
|
+
import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
|
|
15039
|
+
import { extname as extname5, join as join33, relative as relative4 } from "node:path";
|
|
15040
|
+
var MAX_TOP_COMPONENTS = 10;
|
|
15041
|
+
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
15042
|
+
".aac",
|
|
15043
|
+
".aiff",
|
|
15044
|
+
".avi",
|
|
15045
|
+
".flac",
|
|
15046
|
+
".m4a",
|
|
15047
|
+
".m4v",
|
|
15048
|
+
".mkv",
|
|
15049
|
+
".mov",
|
|
15050
|
+
".mp3",
|
|
15051
|
+
".mp4",
|
|
15052
|
+
".mpeg",
|
|
15053
|
+
".mpg",
|
|
15054
|
+
".ogg",
|
|
15055
|
+
".ogv",
|
|
15056
|
+
".ts",
|
|
15057
|
+
".wav",
|
|
15058
|
+
".webm"
|
|
15059
|
+
]);
|
|
15060
|
+
var PLAN_ROOT_CATEGORY = {
|
|
15061
|
+
compiled: "compiled",
|
|
15062
|
+
[PLAN_AUDIO_RELATIVE_PATH]: "audio",
|
|
15063
|
+
meta: "metadata",
|
|
15064
|
+
"plan.json": "metadata"
|
|
15065
|
+
};
|
|
15066
|
+
function hashComponent(value) {
|
|
15067
|
+
return createHash7("sha256").update(value).digest("hex").slice(0, 12);
|
|
15068
|
+
}
|
|
15069
|
+
function classifyFile(segments, rootKind) {
|
|
15070
|
+
const rootName = segments[0] ?? "";
|
|
15071
|
+
const extractedRootName = rootKind === "compiled" ? "__hyperframes_video_frames" : "video-frames";
|
|
15072
|
+
if (rootName === extractedRootName) {
|
|
15073
|
+
const videoKey = segments[1] ?? "unknown";
|
|
15074
|
+
return {
|
|
15075
|
+
category: "video-frames",
|
|
15076
|
+
componentLabel: `video-frames:${hashComponent(videoKey)}`
|
|
15077
|
+
};
|
|
15078
|
+
}
|
|
15079
|
+
const category = rootKind === "compiled" ? "compiled" : PLAN_ROOT_CATEGORY[rootName] ?? "other";
|
|
15080
|
+
return { category, componentLabel: category };
|
|
15081
|
+
}
|
|
15082
|
+
function emptyBreakdown() {
|
|
15083
|
+
return {
|
|
15084
|
+
totalBytes: 0,
|
|
15085
|
+
fileCount: 0,
|
|
15086
|
+
compiledBytes: 0,
|
|
15087
|
+
sourceMediaBytes: 0,
|
|
15088
|
+
videoFramesBytes: 0,
|
|
15089
|
+
videoFrameFileCount: 0,
|
|
15090
|
+
audioBytes: 0,
|
|
15091
|
+
metadataBytes: 0,
|
|
15092
|
+
otherBytes: 0,
|
|
15093
|
+
topComponents: []
|
|
15094
|
+
};
|
|
15095
|
+
}
|
|
15096
|
+
function addCategoryBytes(result, category, filePath, sizeBytes) {
|
|
15097
|
+
if (category === "compiled") {
|
|
15098
|
+
result.compiledBytes += sizeBytes;
|
|
15099
|
+
if (SOURCE_MEDIA_EXTENSIONS.has(extname5(filePath).toLowerCase())) {
|
|
15100
|
+
result.sourceMediaBytes += sizeBytes;
|
|
15101
|
+
}
|
|
15102
|
+
return;
|
|
15103
|
+
}
|
|
15104
|
+
if (category === "video-frames") {
|
|
15105
|
+
result.videoFramesBytes += sizeBytes;
|
|
15106
|
+
result.videoFrameFileCount += 1;
|
|
15107
|
+
return;
|
|
15108
|
+
}
|
|
15109
|
+
if (category === "audio") {
|
|
15110
|
+
result.audioBytes += sizeBytes;
|
|
15111
|
+
return;
|
|
15112
|
+
}
|
|
15113
|
+
if (category === "metadata") {
|
|
15114
|
+
result.metadataBytes += sizeBytes;
|
|
15115
|
+
return;
|
|
15116
|
+
}
|
|
15117
|
+
result.otherBytes += sizeBytes;
|
|
15118
|
+
}
|
|
15119
|
+
function recordFile(result, components, rootDir, rootKind, filePath) {
|
|
15120
|
+
let sizeBytes;
|
|
15121
|
+
try {
|
|
15122
|
+
const stat = lstatSync2(filePath);
|
|
15123
|
+
if (!stat.isFile()) return;
|
|
15124
|
+
sizeBytes = stat.size;
|
|
15125
|
+
} catch {
|
|
15126
|
+
return;
|
|
15127
|
+
}
|
|
15128
|
+
const rel = relative4(rootDir, filePath);
|
|
15129
|
+
const segments = rel.split(/[\\/]+/).filter(Boolean);
|
|
15130
|
+
const classified = classifyFile(segments, rootKind);
|
|
15131
|
+
result.totalBytes += sizeBytes;
|
|
15132
|
+
result.fileCount += 1;
|
|
15133
|
+
addCategoryBytes(result, classified.category, filePath, sizeBytes);
|
|
15134
|
+
const component = components.get(classified.componentLabel) ?? {
|
|
15135
|
+
label: classified.componentLabel,
|
|
15136
|
+
sizeBytes: 0,
|
|
15137
|
+
fileCount: 0
|
|
15138
|
+
};
|
|
15139
|
+
component.sizeBytes += sizeBytes;
|
|
15140
|
+
component.fileCount += 1;
|
|
15141
|
+
components.set(classified.componentLabel, component);
|
|
15142
|
+
}
|
|
15143
|
+
function walkRegularFiles(dir, visit) {
|
|
15144
|
+
let entries;
|
|
15145
|
+
try {
|
|
15146
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
15147
|
+
} catch {
|
|
15148
|
+
return;
|
|
15149
|
+
}
|
|
15150
|
+
for (const entry of entries) {
|
|
15151
|
+
const full = join33(dir, entry.name);
|
|
15152
|
+
if (entry.isDirectory()) {
|
|
15153
|
+
walkRegularFiles(full, visit);
|
|
15154
|
+
} else if (entry.isFile()) {
|
|
15155
|
+
visit(full);
|
|
15156
|
+
}
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
15159
|
+
function measurePlanSizeBreakdown(rootDir, rootKind = "plan") {
|
|
15160
|
+
const result = emptyBreakdown();
|
|
15161
|
+
const components = /* @__PURE__ */ new Map();
|
|
15162
|
+
walkRegularFiles(
|
|
15163
|
+
rootDir,
|
|
15164
|
+
(filePath) => recordFile(result, components, rootDir, rootKind, filePath)
|
|
15165
|
+
);
|
|
15166
|
+
result.topComponents = [...components.values()].sort((a, b) => b.sizeBytes - a.sizeBytes || a.label.localeCompare(b.label)).slice(0, MAX_TOP_COMPONENTS);
|
|
15167
|
+
return result;
|
|
15168
|
+
}
|
|
15169
|
+
|
|
15170
|
+
// src/services/distributed/plan.ts
|
|
15171
|
+
function logPlanStage(log, stage, startedAt) {
|
|
15172
|
+
log.info("[plan] stage", { stage, ms: Date.now() - startedAt });
|
|
15173
|
+
}
|
|
15174
|
+
function applyDistributedAudioWarningPolicy(job, audioError, audioFailures = [], log = defaultLogger) {
|
|
15175
|
+
const failureOwner = audioFailures.length === 0 ? void 0 : audioFailures.some((failure) => failure.owner === "system") ? "system" : "user";
|
|
15176
|
+
const retryable = audioFailures.length === 0 ? void 0 : audioFailures.every((failure) => failure.retryable);
|
|
15177
|
+
applyRenderWarningPolicy(
|
|
15178
|
+
job,
|
|
15179
|
+
[
|
|
15180
|
+
{
|
|
15181
|
+
code: "audio_processing_failed",
|
|
15182
|
+
message: `Audio mix failed; output would be video-only: ${audioError}`,
|
|
15183
|
+
details: {
|
|
15184
|
+
mediaType: "audio",
|
|
15185
|
+
audioFailures: audioFailureDiagnostics3(audioFailures),
|
|
15186
|
+
failureReasons: [...new Set(audioFailures.map((failure) => failure.reason))],
|
|
15187
|
+
failureStages: [...new Set(audioFailures.map((failure) => failure.stage))],
|
|
15188
|
+
failureOwner,
|
|
15189
|
+
retryable
|
|
15190
|
+
}
|
|
15191
|
+
}
|
|
15192
|
+
],
|
|
15193
|
+
log
|
|
15194
|
+
);
|
|
15195
|
+
}
|
|
15196
|
+
var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
|
|
15197
|
+
"node_modules",
|
|
15198
|
+
".git",
|
|
15199
|
+
".cache",
|
|
14648
15200
|
"output",
|
|
14649
15201
|
"failures",
|
|
14650
15202
|
"dist",
|
|
@@ -14806,13 +15358,13 @@ function assertPositiveInteger2(name, value) {
|
|
|
14806
15358
|
}
|
|
14807
15359
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
14808
15360
|
"plan.json",
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
15361
|
+
join34("meta", "composition.json"),
|
|
15362
|
+
join34("meta", "encoder.json"),
|
|
15363
|
+
join34("meta", "chunks.json")
|
|
14812
15364
|
];
|
|
14813
15365
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
14814
15366
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
14815
|
-
|
|
15367
|
+
rmSync11(join34(planDir, relativePath), { force: true });
|
|
14816
15368
|
}
|
|
14817
15369
|
}
|
|
14818
15370
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -14943,19 +15495,19 @@ function createVideoSourceShipper(input) {
|
|
|
14943
15495
|
const known = shipped.get(srcPath);
|
|
14944
15496
|
if (known) return known;
|
|
14945
15497
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
14946
|
-
const current = stagedRel === null ? srcPath :
|
|
15498
|
+
const current = stagedRel === null ? srcPath : join34(input.planDir, "video-frames", stagedRel);
|
|
14947
15499
|
let result = null;
|
|
14948
15500
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
14949
15501
|
const rel = pathInside(root, srcPath);
|
|
14950
|
-
if (rel !== null &&
|
|
15502
|
+
if (rel !== null && (existsSync21(join34(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
14951
15503
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
14952
15504
|
break;
|
|
14953
15505
|
}
|
|
14954
15506
|
}
|
|
14955
15507
|
if (result === null) {
|
|
14956
15508
|
result = `video-sources/${shipped.size}${extname6(current)}`;
|
|
14957
|
-
const destination =
|
|
14958
|
-
|
|
15509
|
+
const destination = join34(input.planDir, ...result.split("/"));
|
|
15510
|
+
mkdirSync13(join34(input.planDir, "video-sources"), { recursive: true });
|
|
14959
15511
|
try {
|
|
14960
15512
|
linkSync(current, destination);
|
|
14961
15513
|
} catch {
|
|
@@ -14973,7 +15525,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
14973
15525
|
useGpu: false,
|
|
14974
15526
|
browserGpuMode: "software"
|
|
14975
15527
|
});
|
|
14976
|
-
if (!
|
|
15528
|
+
if (!existsSync21(planDir)) mkdirSync13(planDir, { recursive: true });
|
|
14977
15529
|
const log = config.logger ?? defaultLogger;
|
|
14978
15530
|
const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
|
|
14979
15531
|
const abortSignal = config.abortSignal;
|
|
@@ -15002,15 +15554,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15002
15554
|
variables: config.variables
|
|
15003
15555
|
});
|
|
15004
15556
|
const entryFile = config.entryFile ?? "index.html";
|
|
15005
|
-
const htmlPath =
|
|
15006
|
-
if (!
|
|
15557
|
+
const htmlPath = join34(projectDir, entryFile);
|
|
15558
|
+
if (!existsSync21(htmlPath)) {
|
|
15007
15559
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
15008
15560
|
}
|
|
15009
|
-
const workDir =
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
const compiledDir =
|
|
15013
|
-
|
|
15561
|
+
const workDir = join34(planDir, ".plan-work");
|
|
15562
|
+
rmSync11(workDir, { recursive: true, force: true });
|
|
15563
|
+
mkdirSync13(workDir, { recursive: true });
|
|
15564
|
+
const compiledDir = join34(workDir, "compiled");
|
|
15565
|
+
mkdirSync13(compiledDir, { recursive: true });
|
|
15014
15566
|
let stageStarted = Date.now();
|
|
15015
15567
|
cpSync2(projectDir, compiledDir, {
|
|
15016
15568
|
recursive: true,
|
|
@@ -15023,10 +15575,19 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15023
15575
|
}
|
|
15024
15576
|
});
|
|
15025
15577
|
logPlanStage(log, "copy", stageStarted);
|
|
15026
|
-
const finalCompiledDir =
|
|
15578
|
+
const finalCompiledDir = join34(planDir, "compiled");
|
|
15027
15579
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
15580
|
+
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
15581
|
+
const remoteLocalPath = (rel) => join34(projectDir, ...rel.split("/"));
|
|
15582
|
+
const urlByAbsentLocalPath = new Map(
|
|
15583
|
+
remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
|
|
15584
|
+
);
|
|
15585
|
+
const localPathByUrl = new Map(
|
|
15586
|
+
remoteFiles.map(([rel, file]) => [file.url, remoteLocalPath(rel)])
|
|
15587
|
+
);
|
|
15028
15588
|
stageStarted = Date.now();
|
|
15029
15589
|
const compileResult = await runCompileStage({
|
|
15590
|
+
urlByAbsentLocalPath,
|
|
15030
15591
|
projectDir,
|
|
15031
15592
|
workDir,
|
|
15032
15593
|
htmlPath,
|
|
@@ -15114,30 +15675,32 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15114
15675
|
abortSignal,
|
|
15115
15676
|
assertNotAborted,
|
|
15116
15677
|
materializeSymlinks: true,
|
|
15117
|
-
deferRangeExtraction: options.deferVideoExtraction
|
|
15678
|
+
deferRangeExtraction: options.deferVideoExtraction,
|
|
15679
|
+
urlByAbsentLocalPath
|
|
15118
15680
|
});
|
|
15119
15681
|
logPlanStage(log, "extract", stageStarted);
|
|
15120
15682
|
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
15121
15683
|
if (extractResult.extractionResult) {
|
|
15122
15684
|
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
15123
15685
|
}
|
|
15124
|
-
const stagedVideoFrames =
|
|
15125
|
-
const videoFramesDst =
|
|
15126
|
-
if (
|
|
15127
|
-
const videoSourcesDir =
|
|
15128
|
-
|
|
15129
|
-
if (
|
|
15130
|
-
|
|
15686
|
+
const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
|
|
15687
|
+
const videoFramesDst = join34(planDir, "video-frames");
|
|
15688
|
+
if (existsSync21(videoFramesDst)) rmSync11(videoFramesDst, { recursive: true, force: true });
|
|
15689
|
+
const videoSourcesDir = join34(planDir, "video-sources");
|
|
15690
|
+
rmSync11(videoSourcesDir, { recursive: true, force: true });
|
|
15691
|
+
if (existsSync21(stagedVideoFrames)) {
|
|
15692
|
+
renameSync6(stagedVideoFrames, videoFramesDst);
|
|
15131
15693
|
} else {
|
|
15132
|
-
|
|
15694
|
+
mkdirSync13(videoFramesDst, { recursive: true });
|
|
15133
15695
|
}
|
|
15134
|
-
if (
|
|
15135
|
-
|
|
15696
|
+
if (existsSync21(finalCompiledDir)) rmSync11(finalCompiledDir, { recursive: true, force: true });
|
|
15697
|
+
renameSync6(compiledDir, finalCompiledDir);
|
|
15136
15698
|
const shipVideoSource = createVideoSourceShipper({
|
|
15137
15699
|
planDir,
|
|
15138
15700
|
projectDir,
|
|
15139
15701
|
workCompiledDir: compiledDir,
|
|
15140
|
-
workVideoFramesDir: stagedVideoFrames
|
|
15702
|
+
workVideoFramesDir: stagedVideoFrames,
|
|
15703
|
+
isRemote: (srcPath) => urlByAbsentLocalPath.has(srcPath)
|
|
15141
15704
|
});
|
|
15142
15705
|
const sourceRoots = [
|
|
15143
15706
|
[stagedVideoFrames, "video-frames"],
|
|
@@ -15147,19 +15710,30 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15147
15710
|
const planVideosJson = buildPlanVideosJson({
|
|
15148
15711
|
videos: composition.videos,
|
|
15149
15712
|
compositionEnd: job.duration ?? Number.NaN,
|
|
15150
|
-
extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) =>
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15713
|
+
extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) => {
|
|
15714
|
+
const sourcePath = localPathByUrl.get(ext.srcPath) ?? ext.srcPath;
|
|
15715
|
+
return {
|
|
15716
|
+
videoId: ext.videoId,
|
|
15717
|
+
srcPath: stableSourcePath(sourceRoots, sourcePath),
|
|
15718
|
+
framePattern: ext.framePattern,
|
|
15719
|
+
fps: ext.fps,
|
|
15720
|
+
totalFrames: ext.totalFrames,
|
|
15721
|
+
metadata: ext.metadata,
|
|
15722
|
+
...ext.deferredRange ? { deferredRange: { ...ext.deferredRange, sourcePath: shipVideoSource(sourcePath) } } : {}
|
|
15723
|
+
};
|
|
15724
|
+
})
|
|
15159
15725
|
});
|
|
15160
|
-
|
|
15161
|
-
|
|
15162
|
-
|
|
15726
|
+
mkdirSync13(join34(planDir, "meta"), { recursive: true });
|
|
15727
|
+
writePlanRemoteFiles(
|
|
15728
|
+
planDir,
|
|
15729
|
+
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
15730
|
+
path: `compiled/${rel}`,
|
|
15731
|
+
sha256,
|
|
15732
|
+
sizeBytes
|
|
15733
|
+
}))
|
|
15734
|
+
);
|
|
15735
|
+
writeFileSync8(
|
|
15736
|
+
join34(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
15163
15737
|
JSON.stringify(planVideosJson, null, 2),
|
|
15164
15738
|
"utf-8"
|
|
15165
15739
|
);
|
|
@@ -15172,6 +15746,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15172
15746
|
projectDir,
|
|
15173
15747
|
workDir,
|
|
15174
15748
|
compiledDir: finalCompiledDir,
|
|
15749
|
+
urlByAbsentLocalPath,
|
|
15175
15750
|
duration: audioSeconds,
|
|
15176
15751
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
15177
15752
|
audioGain: cfg.audioGain,
|
|
@@ -15232,11 +15807,11 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15232
15807
|
if (audioResult.audioError) {
|
|
15233
15808
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
15234
15809
|
}
|
|
15235
|
-
const planAudioPath =
|
|
15236
|
-
if (audioResult.hasAudio &&
|
|
15237
|
-
|
|
15810
|
+
const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
15811
|
+
if (audioResult.hasAudio && existsSync21(audioResult.audioOutputPath)) {
|
|
15812
|
+
renameSync6(audioResult.audioOutputPath, planAudioPath);
|
|
15238
15813
|
}
|
|
15239
|
-
if (chunksCarryAudio2 &&
|
|
15814
|
+
if (chunksCarryAudio2 && existsSync21(planAudioPath)) {
|
|
15240
15815
|
stageStarted = Date.now();
|
|
15241
15816
|
const peak = await constrainAacTruePeak({
|
|
15242
15817
|
audioPath: planAudioPath,
|
|
@@ -15246,6 +15821,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15246
15821
|
});
|
|
15247
15822
|
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
15248
15823
|
logPlanStage(log, "audio-peak", stageStarted);
|
|
15824
|
+
if (options.sliceChunkAudio) {
|
|
15825
|
+
stageStarted = Date.now();
|
|
15826
|
+
await writeChunkAudioSlices({
|
|
15827
|
+
audioPath: planAudioPath,
|
|
15828
|
+
planDir,
|
|
15829
|
+
chunks,
|
|
15830
|
+
fps: { num: config.fps, den: 1 }
|
|
15831
|
+
});
|
|
15832
|
+
logPlanStage(log, "audio-slices", stageStarted);
|
|
15833
|
+
}
|
|
15249
15834
|
}
|
|
15250
15835
|
const ffmpegVersion = await readFfmpegVersion();
|
|
15251
15836
|
const producerVersion = readProducerVersion();
|
|
@@ -15279,7 +15864,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15279
15864
|
format: config.format
|
|
15280
15865
|
};
|
|
15281
15866
|
try {
|
|
15282
|
-
|
|
15867
|
+
rmSync11(workDir, { recursive: true, force: true });
|
|
15283
15868
|
} catch (err) {
|
|
15284
15869
|
log.warn("[plan] failed to remove temp work dir", {
|
|
15285
15870
|
workDir,
|
|
@@ -15843,15 +16428,15 @@ import {
|
|
|
15843
16428
|
|
|
15844
16429
|
// src/server.ts
|
|
15845
16430
|
import {
|
|
15846
|
-
existsSync as
|
|
15847
|
-
mkdirSync as
|
|
16431
|
+
existsSync as existsSync24,
|
|
16432
|
+
mkdirSync as mkdirSync14,
|
|
15848
16433
|
statSync as statSync8,
|
|
15849
|
-
mkdtempSync as
|
|
15850
|
-
writeFileSync as
|
|
15851
|
-
rmSync as
|
|
15852
|
-
createReadStream as
|
|
16434
|
+
mkdtempSync as mkdtempSync5,
|
|
16435
|
+
writeFileSync as writeFileSync9,
|
|
16436
|
+
rmSync as rmSync12,
|
|
16437
|
+
createReadStream as createReadStream5
|
|
15853
16438
|
} from "node:fs";
|
|
15854
|
-
import { resolve as resolve10, dirname as
|
|
16439
|
+
import { resolve as resolve10, dirname as dirname14, join as join37 } from "node:path";
|
|
15855
16440
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15856
16441
|
import { parseArgs } from "node:util";
|
|
15857
16442
|
import crypto from "node:crypto";
|
|
@@ -15861,15 +16446,15 @@ import { serve as serve2 } from "@hono/node-server";
|
|
|
15861
16446
|
|
|
15862
16447
|
// src/services/hyperframeLint.ts
|
|
15863
16448
|
import {
|
|
15864
|
-
existsSync as
|
|
15865
|
-
readFileSync as
|
|
16449
|
+
existsSync as existsSync22,
|
|
16450
|
+
readFileSync as readFileSync12,
|
|
15866
16451
|
statSync as statSync7,
|
|
15867
|
-
openSync as
|
|
16452
|
+
openSync as openSync6,
|
|
15868
16453
|
fstatSync as fstatSync4,
|
|
15869
|
-
closeSync as
|
|
16454
|
+
closeSync as closeSync7,
|
|
15870
16455
|
constants as constants3
|
|
15871
16456
|
} from "node:fs";
|
|
15872
|
-
import { resolve as resolve9, join as
|
|
16457
|
+
import { resolve as resolve9, join as join35, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
|
|
15873
16458
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
15874
16459
|
function isStringRecord(value) {
|
|
15875
16460
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -15899,7 +16484,7 @@ function pickEntryFile(files, preferredEntryFile) {
|
|
|
15899
16484
|
function readEntryHtml(filePath) {
|
|
15900
16485
|
let fd;
|
|
15901
16486
|
try {
|
|
15902
|
-
fd =
|
|
16487
|
+
fd = openSync6(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
|
|
15903
16488
|
} catch (error) {
|
|
15904
16489
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
15905
16490
|
return null;
|
|
@@ -15908,14 +16493,14 @@ function readEntryHtml(filePath) {
|
|
|
15908
16493
|
}
|
|
15909
16494
|
try {
|
|
15910
16495
|
if (!fstatSync4(fd).isFile()) return null;
|
|
15911
|
-
return
|
|
16496
|
+
return readFileSync12(fd, "utf-8");
|
|
15912
16497
|
} finally {
|
|
15913
|
-
|
|
16498
|
+
closeSync7(fd);
|
|
15914
16499
|
}
|
|
15915
16500
|
}
|
|
15916
16501
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
15917
16502
|
const absProjectDir = resolve9(projectDir);
|
|
15918
|
-
if (!
|
|
16503
|
+
if (!existsSync22(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
|
|
15919
16504
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
15920
16505
|
}
|
|
15921
16506
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
@@ -15937,7 +16522,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
15937
16522
|
}
|
|
15938
16523
|
}
|
|
15939
16524
|
return {
|
|
15940
|
-
error: `No HTML entry file found in project directory: ${
|
|
16525
|
+
error: `No HTML entry file found in project directory: ${join35(absProjectDir, preferredEntryFile || "index.html")}`
|
|
15941
16526
|
};
|
|
15942
16527
|
}
|
|
15943
16528
|
function prepareHyperframeLintBody(body) {
|
|
@@ -15983,8 +16568,8 @@ async function runHyperframeLint(prepared) {
|
|
|
15983
16568
|
// src/services/healthWorker.ts
|
|
15984
16569
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
15985
16570
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
15986
|
-
import { dirname as
|
|
15987
|
-
import { existsSync as
|
|
16571
|
+
import { dirname as dirname13, join as join36 } from "node:path";
|
|
16572
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
15988
16573
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
15989
16574
|
async function startHealthWorker(options = {}) {
|
|
15990
16575
|
const log = options.logger ?? defaultLogger2();
|
|
@@ -16058,10 +16643,10 @@ function defaultLogger2() {
|
|
|
16058
16643
|
};
|
|
16059
16644
|
}
|
|
16060
16645
|
function resolveWorkerEntry2() {
|
|
16061
|
-
const here =
|
|
16062
|
-
const candidates = [
|
|
16646
|
+
const here = dirname13(fileURLToPath6(import.meta.url));
|
|
16647
|
+
const candidates = [join36(here, "healthWorkerThread.js"), join36(here, "healthWorkerThread.ts")];
|
|
16063
16648
|
for (const candidate of candidates) {
|
|
16064
|
-
if (
|
|
16649
|
+
if (existsSync23(candidate)) return candidate;
|
|
16065
16650
|
}
|
|
16066
16651
|
return void 0;
|
|
16067
16652
|
}
|
|
@@ -16205,8 +16790,8 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
16205
16790
|
function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
16206
16791
|
const { input, cleanupProjectDir } = prepared;
|
|
16207
16792
|
const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
|
|
16208
|
-
const outputDir =
|
|
16209
|
-
if (!
|
|
16793
|
+
const outputDir = dirname14(absoluteOutputPath);
|
|
16794
|
+
if (!existsSync24(outputDir)) mkdirSync14(outputDir, { recursive: true });
|
|
16210
16795
|
return { input, cleanupProjectDir, absoluteOutputPath };
|
|
16211
16796
|
}
|
|
16212
16797
|
function validateRenderOverrides(body) {
|
|
@@ -16246,11 +16831,11 @@ function prepareProjectDirectory(projectDir, options) {
|
|
|
16246
16831
|
const candidate = nonEmptyString(projectDir);
|
|
16247
16832
|
if (!candidate) return null;
|
|
16248
16833
|
const absProjectDir = resolve10(candidate);
|
|
16249
|
-
if (!
|
|
16834
|
+
if (!existsSync24(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
|
|
16250
16835
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
16251
16836
|
}
|
|
16252
16837
|
const entry = options.entryFile || "index.html";
|
|
16253
|
-
if (!
|
|
16838
|
+
if (!existsSync24(resolve10(absProjectDir, entry))) {
|
|
16254
16839
|
return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
|
|
16255
16840
|
}
|
|
16256
16841
|
return { prepared: { input: { projectDir: absProjectDir, ...options } } };
|
|
@@ -16275,8 +16860,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
16275
16860
|
}
|
|
16276
16861
|
function materializeInlineProject(html, options) {
|
|
16277
16862
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
16278
|
-
const tempProjectDir =
|
|
16279
|
-
|
|
16863
|
+
const tempProjectDir = mkdtempSync5(join37(tempRoot, "producer-project-"));
|
|
16864
|
+
writeFileSync9(join37(tempProjectDir, "index.html"), html, "utf-8");
|
|
16280
16865
|
return {
|
|
16281
16866
|
prepared: {
|
|
16282
16867
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -16333,7 +16918,7 @@ function createArtifactStore(ttlMs) {
|
|
|
16333
16918
|
function cleanupTempDir(dir, log) {
|
|
16334
16919
|
if (!dir) return;
|
|
16335
16920
|
try {
|
|
16336
|
-
|
|
16921
|
+
rmSync12(dir, { recursive: true, force: true });
|
|
16337
16922
|
} catch (error) {
|
|
16338
16923
|
log.warn("Failed to cleanup temp project dir", {
|
|
16339
16924
|
cleanupProjectDir: dir,
|
|
@@ -16342,7 +16927,7 @@ function cleanupTempDir(dir, log) {
|
|
|
16342
16927
|
}
|
|
16343
16928
|
}
|
|
16344
16929
|
function outputFileSize(path) {
|
|
16345
|
-
return
|
|
16930
|
+
return existsSync24(path) ? statSync8(path).size : 0;
|
|
16346
16931
|
}
|
|
16347
16932
|
function createBlockingProgressReporter(log, requestId) {
|
|
16348
16933
|
let lastLoggedPct = -10;
|
|
@@ -16638,12 +17223,12 @@ function createRenderHandlers(options = {}) {
|
|
|
16638
17223
|
if (!artifact) {
|
|
16639
17224
|
return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
|
|
16640
17225
|
}
|
|
16641
|
-
if (!
|
|
17226
|
+
if (!existsSync24(artifact.path)) {
|
|
16642
17227
|
store.delete(token);
|
|
16643
17228
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
16644
17229
|
}
|
|
16645
17230
|
const stats = statSync8(artifact.path);
|
|
16646
|
-
return new Response(
|
|
17231
|
+
return new Response(createReadStream5(artifact.path), {
|
|
16647
17232
|
headers: {
|
|
16648
17233
|
"content-type": "video/mp4",
|
|
16649
17234
|
"content-length": String(stats.size),
|
|
@@ -16735,35 +17320,35 @@ import { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes
|
|
|
16735
17320
|
// src/services/distributed/planV2.ts
|
|
16736
17321
|
import {
|
|
16737
17322
|
copyFileSync as copyFileSync7,
|
|
16738
|
-
existsSync as
|
|
17323
|
+
existsSync as existsSync26,
|
|
16739
17324
|
lstatSync as lstatSync3,
|
|
16740
|
-
mkdirSync as
|
|
16741
|
-
mkdtempSync as
|
|
16742
|
-
readFileSync as
|
|
17325
|
+
mkdirSync as mkdirSync16,
|
|
17326
|
+
mkdtempSync as mkdtempSync7,
|
|
17327
|
+
readFileSync as readFileSync13,
|
|
16743
17328
|
readdirSync as readdirSync9,
|
|
16744
|
-
renameSync as
|
|
16745
|
-
rmSync as
|
|
17329
|
+
renameSync as renameSync8,
|
|
17330
|
+
rmSync as rmSync14,
|
|
16746
17331
|
statSync as statSync10,
|
|
16747
|
-
writeFileSync as
|
|
17332
|
+
writeFileSync as writeFileSync11
|
|
16748
17333
|
} from "node:fs";
|
|
16749
17334
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
16750
|
-
import { dirname as
|
|
17335
|
+
import { dirname as dirname16, isAbsolute as isAbsolute5, join as join40, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
|
|
16751
17336
|
import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
|
|
16752
17337
|
import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
|
|
16753
17338
|
|
|
16754
17339
|
// src/services/distributed/planV2Publisher.ts
|
|
16755
17340
|
import {
|
|
16756
17341
|
copyFileSync as copyFileSync6,
|
|
16757
|
-
existsSync as
|
|
17342
|
+
existsSync as existsSync25,
|
|
16758
17343
|
linkSync as linkSync2,
|
|
16759
|
-
mkdirSync as
|
|
16760
|
-
mkdtempSync as
|
|
16761
|
-
renameSync as
|
|
16762
|
-
rmSync as
|
|
17344
|
+
mkdirSync as mkdirSync15,
|
|
17345
|
+
mkdtempSync as mkdtempSync6,
|
|
17346
|
+
renameSync as renameSync7,
|
|
17347
|
+
rmSync as rmSync13,
|
|
16763
17348
|
statSync as statSync9,
|
|
16764
|
-
writeFileSync as
|
|
17349
|
+
writeFileSync as writeFileSync10
|
|
16765
17350
|
} from "node:fs";
|
|
16766
|
-
import { dirname as
|
|
17351
|
+
import { dirname as dirname15, join as join39 } from "node:path";
|
|
16767
17352
|
|
|
16768
17353
|
// src/services/distributed/planV2Errors.ts
|
|
16769
17354
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -16778,7 +17363,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
16778
17363
|
};
|
|
16779
17364
|
|
|
16780
17365
|
// src/services/distributed/planV2Layout.ts
|
|
16781
|
-
import { join as
|
|
17366
|
+
import { join as join38 } from "node:path";
|
|
16782
17367
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
16783
17368
|
function assertPlanV2Sha256(value, field) {
|
|
16784
17369
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -16788,7 +17373,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
16788
17373
|
}
|
|
16789
17374
|
function planV2BlobPath(root, sha256) {
|
|
16790
17375
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
16791
|
-
return
|
|
17376
|
+
return join38(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
16792
17377
|
}
|
|
16793
17378
|
|
|
16794
17379
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -16823,13 +17408,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
16823
17408
|
#linkFile;
|
|
16824
17409
|
#committed = false;
|
|
16825
17410
|
constructor(destinationDir, options = {}) {
|
|
16826
|
-
if (
|
|
17411
|
+
if (existsSync25(destinationDir)) {
|
|
16827
17412
|
throw new PlanV2IntegrityError(`output directory already exists: ${destinationDir}`);
|
|
16828
17413
|
}
|
|
16829
17414
|
this.destinationDir = destinationDir;
|
|
16830
17415
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
16831
|
-
|
|
16832
|
-
this.temporaryDir =
|
|
17416
|
+
mkdirSync15(dirname15(destinationDir), { recursive: true });
|
|
17417
|
+
this.temporaryDir = mkdtempSync6(join39(dirname15(destinationDir), ".plan-v2-publish-"));
|
|
16833
17418
|
}
|
|
16834
17419
|
async putBlob(blob) {
|
|
16835
17420
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
@@ -16840,10 +17425,10 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
16840
17425
|
);
|
|
16841
17426
|
}
|
|
16842
17427
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
16843
|
-
if (
|
|
16844
|
-
|
|
16845
|
-
const stagingDir =
|
|
16846
|
-
const temporaryPath =
|
|
17428
|
+
if (existsSync25(destinationPath)) return;
|
|
17429
|
+
mkdirSync15(dirname15(destinationPath), { recursive: true });
|
|
17430
|
+
const stagingDir = mkdtempSync6(join39(dirname15(destinationPath), ".plan-v2-blob-"));
|
|
17431
|
+
const temporaryPath = join39(stagingDir, "blob");
|
|
16847
17432
|
try {
|
|
16848
17433
|
try {
|
|
16849
17434
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
@@ -16851,26 +17436,26 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
16851
17436
|
if (!canFallbackToCopy(error)) throw error;
|
|
16852
17437
|
copyFileSync6(blob.sourcePath, temporaryPath);
|
|
16853
17438
|
}
|
|
16854
|
-
|
|
17439
|
+
renameSync7(temporaryPath, destinationPath);
|
|
16855
17440
|
} finally {
|
|
16856
|
-
|
|
17441
|
+
rmSync13(stagingDir, { recursive: true, force: true });
|
|
16857
17442
|
}
|
|
16858
17443
|
}
|
|
16859
17444
|
async commitManifest(manifestBytes) {
|
|
16860
17445
|
for (const digest of new Set(manifestDigests(manifestBytes))) {
|
|
16861
|
-
if (!
|
|
17446
|
+
if (!existsSync25(planV2BlobPath(this.temporaryDir, digest))) {
|
|
16862
17447
|
throw new PlanV2IntegrityError(
|
|
16863
17448
|
`cannot commit manifest before referenced blob is durable: ${digest}`
|
|
16864
17449
|
);
|
|
16865
17450
|
}
|
|
16866
17451
|
}
|
|
16867
|
-
|
|
16868
|
-
|
|
17452
|
+
writeFileSync10(join39(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
17453
|
+
renameSync7(this.temporaryDir, this.destinationDir);
|
|
16869
17454
|
this.#committed = true;
|
|
16870
17455
|
}
|
|
16871
17456
|
async abort() {
|
|
16872
17457
|
if (!this.#committed) {
|
|
16873
|
-
|
|
17458
|
+
rmSync13(this.temporaryDir, { recursive: true, force: true });
|
|
16874
17459
|
}
|
|
16875
17460
|
}
|
|
16876
17461
|
};
|
|
@@ -16889,7 +17474,7 @@ function isNonNegativeIntegerArray(value) {
|
|
|
16889
17474
|
return Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0);
|
|
16890
17475
|
}
|
|
16891
17476
|
function readJsonFile(path, label) {
|
|
16892
|
-
const contents =
|
|
17477
|
+
const contents = readFileSync13(path, "utf-8");
|
|
16893
17478
|
try {
|
|
16894
17479
|
const parsed = JSON.parse(contents);
|
|
16895
17480
|
return parsed;
|
|
@@ -16909,7 +17494,7 @@ function listFiles(root) {
|
|
|
16909
17494
|
const rootResolved = resolve11(root);
|
|
16910
17495
|
function walk(dir) {
|
|
16911
17496
|
for (const entry of readdirSync9(dir, { withFileTypes: true })) {
|
|
16912
|
-
const absolutePath =
|
|
17497
|
+
const absolutePath = join40(dir, entry.name);
|
|
16913
17498
|
if (entry.isDirectory()) {
|
|
16914
17499
|
walk(absolutePath);
|
|
16915
17500
|
} else if (entry.isFile()) {
|
|
@@ -16936,13 +17521,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
16936
17521
|
}
|
|
16937
17522
|
}
|
|
16938
17523
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
16939
|
-
const videoRoot =
|
|
16940
|
-
if (!
|
|
17524
|
+
const videoRoot = join40(executionPlanDir, "video-frames");
|
|
17525
|
+
if (!existsSync26(videoRoot)) return;
|
|
16941
17526
|
for (const videoEntry of readdirSync9(videoRoot, { withFileTypes: true })) {
|
|
16942
17527
|
if (!videoEntry.isDirectory()) continue;
|
|
16943
|
-
const videoDir =
|
|
17528
|
+
const videoDir = join40(videoRoot, videoEntry.name);
|
|
16944
17529
|
if (readdirSync9(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
16945
|
-
const sentinelPath =
|
|
17530
|
+
const sentinelPath = join40(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
16946
17531
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
16947
17532
|
}
|
|
16948
17533
|
}
|
|
@@ -16959,31 +17544,37 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
|
|
|
16959
17544
|
}
|
|
16960
17545
|
return outputDir;
|
|
16961
17546
|
}
|
|
16962
|
-
function artifactTargets(path, videoDependencies,
|
|
17547
|
+
function artifactTargets(path, videoDependencies, chunkAudio) {
|
|
16963
17548
|
if (isPlanAudioArtifactPath(path)) {
|
|
16964
|
-
|
|
17549
|
+
if (chunkAudio === "none") return { chunks: [], assembler: true };
|
|
17550
|
+
return { chunks: chunkAudio === "whole" ? "all" : [], assembler: false };
|
|
16965
17551
|
}
|
|
17552
|
+
const slice = chunkAudioSliceTarget(path);
|
|
17553
|
+
if (slice !== null) return { chunks: slice === "all" ? "all" : [slice], assembler: false };
|
|
16966
17554
|
if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
|
|
16967
17555
|
return { chunks: "all", assembler: true };
|
|
16968
17556
|
}
|
|
16969
|
-
if (path
|
|
16970
|
-
return { chunks: "all", assembler: false };
|
|
16971
|
-
}
|
|
16972
|
-
if (path.startsWith("video-frames/") || path.startsWith("video-sources/")) {
|
|
17557
|
+
if (path.startsWith("video-frames/") || path.startsWith("video-sources/") || path.startsWith("compiled/") && videoDependencies?.has(path)) {
|
|
16973
17558
|
return {
|
|
16974
17559
|
chunks: videoDependencies === null ? "all" : videoDependencies.get(path) ?? [],
|
|
16975
17560
|
assembler: false
|
|
16976
17561
|
};
|
|
16977
17562
|
}
|
|
17563
|
+
if (path === "meta/composition.json" || path === "meta/videos.json" || path.startsWith("compiled/")) {
|
|
17564
|
+
return { chunks: "all", assembler: false };
|
|
17565
|
+
}
|
|
16978
17566
|
return { chunks: "all", assembler: true };
|
|
16979
17567
|
}
|
|
17568
|
+
function isPlanV2DeferredSourceArtifact(artifact) {
|
|
17569
|
+
return artifact.path.startsWith("compiled/") && artifact.chunks !== "all";
|
|
17570
|
+
}
|
|
16980
17571
|
function listVideoFramePaths(executionPlanDir, videos) {
|
|
16981
17572
|
return videos.extracted.map((video) => {
|
|
16982
17573
|
const outputDir = resolveExtractedVideoOutputDir(executionPlanDir, video.videoId);
|
|
16983
17574
|
if (video.deferredRange) {
|
|
16984
17575
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
16985
17576
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
16986
|
-
framePaths2.set(index,
|
|
17577
|
+
framePaths2.set(index, join40(outputDir, String(index)));
|
|
16987
17578
|
}
|
|
16988
17579
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
16989
17580
|
}
|
|
@@ -16991,7 +17582,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
16991
17582
|
const framePaths = /* @__PURE__ */ new Map();
|
|
16992
17583
|
for (const frameName of frameNames) {
|
|
16993
17584
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
16994
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
17585
|
+
assertValidExtractionCacheCompleteSentinel(join40(outputDir, frameName));
|
|
16995
17586
|
continue;
|
|
16996
17587
|
}
|
|
16997
17588
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -17010,7 +17601,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
17010
17601
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
17011
17602
|
);
|
|
17012
17603
|
}
|
|
17013
|
-
framePaths.set(frameIndex,
|
|
17604
|
+
framePaths.set(frameIndex, join40(outputDir, frameName));
|
|
17014
17605
|
}
|
|
17015
17606
|
return {
|
|
17016
17607
|
...video,
|
|
@@ -17031,11 +17622,11 @@ function parsePlanVideosJson2(value) {
|
|
|
17031
17622
|
}
|
|
17032
17623
|
}
|
|
17033
17624
|
function materializeExtractedVideoDirectories(planDir) {
|
|
17034
|
-
const videosPath =
|
|
17035
|
-
if (!
|
|
17625
|
+
const videosPath = join40(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
17626
|
+
if (!existsSync26(videosPath)) return;
|
|
17036
17627
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
17037
17628
|
for (const video of videos.extracted) {
|
|
17038
|
-
|
|
17629
|
+
mkdirSync16(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
|
|
17039
17630
|
}
|
|
17040
17631
|
}
|
|
17041
17632
|
function parseChunkSlices(value) {
|
|
@@ -17060,13 +17651,13 @@ function parseChunkSlices(value) {
|
|
|
17060
17651
|
});
|
|
17061
17652
|
}
|
|
17062
17653
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
17063
|
-
const videoRoot =
|
|
17064
|
-
const hasExtractedFrames =
|
|
17065
|
-
const videosPath =
|
|
17066
|
-
if (!
|
|
17654
|
+
const videoRoot = join40(executionPlanDir, "video-frames");
|
|
17655
|
+
const hasExtractedFrames = existsSync26(videoRoot) && listFiles(videoRoot).length > 0;
|
|
17656
|
+
const videosPath = join40(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
17657
|
+
if (!existsSync26(videosPath)) {
|
|
17067
17658
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
17068
17659
|
}
|
|
17069
|
-
const chunksPath =
|
|
17660
|
+
const chunksPath = join40(executionPlanDir, "meta", "chunks.json");
|
|
17070
17661
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
17071
17662
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
17072
17663
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -17120,15 +17711,15 @@ function computeManifestHash(manifest) {
|
|
|
17120
17711
|
return sha256Hex(`${PLAN_V2_HASH_PREFIX}${canonicalJsonStringify(manifestPayload(manifest))}`);
|
|
17121
17712
|
}
|
|
17122
17713
|
function writeBlob(sourcePath, destinationPath) {
|
|
17123
|
-
if (
|
|
17124
|
-
|
|
17125
|
-
const temporaryDir =
|
|
17126
|
-
const temporaryPath =
|
|
17714
|
+
if (existsSync26(destinationPath)) return;
|
|
17715
|
+
mkdirSync16(dirname16(destinationPath), { recursive: true });
|
|
17716
|
+
const temporaryDir = mkdtempSync7(join40(dirname16(destinationPath), ".plan-v2-blob-"));
|
|
17717
|
+
const temporaryPath = join40(temporaryDir, "blob");
|
|
17127
17718
|
try {
|
|
17128
17719
|
copyFileSync7(sourcePath, temporaryPath);
|
|
17129
|
-
|
|
17720
|
+
renameSync8(temporaryPath, destinationPath);
|
|
17130
17721
|
} finally {
|
|
17131
|
-
|
|
17722
|
+
rmSync14(temporaryDir, { recursive: true, force: true });
|
|
17132
17723
|
}
|
|
17133
17724
|
}
|
|
17134
17725
|
function publishBlobFor(absolutePath, digests) {
|
|
@@ -17139,14 +17730,17 @@ function publishBlobFor(absolutePath, digests) {
|
|
|
17139
17730
|
};
|
|
17140
17731
|
}
|
|
17141
17732
|
async function putBlobs(publisher, blobs) {
|
|
17733
|
+
await putTasks(blobs.map((blob) => () => publisher.putBlob(blob)));
|
|
17734
|
+
}
|
|
17735
|
+
async function putTasks(tasks) {
|
|
17142
17736
|
const uploadSlots = 32;
|
|
17143
17737
|
let next = 0;
|
|
17144
17738
|
let failure;
|
|
17145
17739
|
const upload = async () => {
|
|
17146
|
-
while (!failure && next <
|
|
17147
|
-
const
|
|
17740
|
+
while (!failure && next < tasks.length) {
|
|
17741
|
+
const task = tasks[next++];
|
|
17148
17742
|
try {
|
|
17149
|
-
await
|
|
17743
|
+
await task();
|
|
17150
17744
|
} catch (reason) {
|
|
17151
17745
|
failure ??= { reason };
|
|
17152
17746
|
}
|
|
@@ -17156,8 +17750,8 @@ async function putBlobs(publisher, blobs) {
|
|
|
17156
17750
|
if (failure) throw failure.reason;
|
|
17157
17751
|
}
|
|
17158
17752
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
17159
|
-
const executionPlanPath =
|
|
17160
|
-
if (!
|
|
17753
|
+
const executionPlanPath = join40(executionPlanDir, "plan.json");
|
|
17754
|
+
if (!existsSync26(executionPlanPath)) {
|
|
17161
17755
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
17162
17756
|
}
|
|
17163
17757
|
const executionPlanValue = readJsonFile(executionPlanPath, "execution plan.json");
|
|
@@ -17184,11 +17778,12 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
17184
17778
|
}
|
|
17185
17779
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
17186
17780
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
17187
|
-
const encoder = readJsonFile(
|
|
17188
|
-
const
|
|
17189
|
-
|
|
17781
|
+
const encoder = readJsonFile(join40(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
17782
|
+
const files = listFiles(executionPlanDir);
|
|
17783
|
+
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
17784
|
+
for (const file of files) {
|
|
17190
17785
|
if (isExtractionCacheCompleteSentinelPath(file.path)) continue;
|
|
17191
|
-
const targets = artifactTargets(file.path, videoDependencyPlan.dependencies,
|
|
17786
|
+
const targets = artifactTargets(file.path, videoDependencyPlan.dependencies, chunkAudio);
|
|
17192
17787
|
if ((file.path.startsWith("video-frames/") || file.path.startsWith("video-sources/")) && targets.chunks !== "all" && targets.chunks.length === 0 && !targets.assembler) {
|
|
17193
17788
|
continue;
|
|
17194
17789
|
}
|
|
@@ -17201,6 +17796,15 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
17201
17796
|
...targets
|
|
17202
17797
|
});
|
|
17203
17798
|
}
|
|
17799
|
+
const storedBlobs = /* @__PURE__ */ new Map();
|
|
17800
|
+
for (const remote of readPlanRemoteFiles(executionPlanDir)) {
|
|
17801
|
+
const { sha256, sizeBytes } = remote;
|
|
17802
|
+
if (!blobs.has(sha256)) storedBlobs.set(sha256, { sha256, sizeBytes });
|
|
17803
|
+
artifacts.push({
|
|
17804
|
+
...remote,
|
|
17805
|
+
...artifactTargets(remote.path, videoDependencyPlan.dependencies, chunkAudio)
|
|
17806
|
+
});
|
|
17807
|
+
}
|
|
17204
17808
|
const base = {
|
|
17205
17809
|
protocol: PLAN_PROTOCOL_V2,
|
|
17206
17810
|
// Retain the established manifest key byte-for-byte. It now acts as the
|
|
@@ -17222,718 +17826,407 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
17222
17826
|
...base,
|
|
17223
17827
|
planHash: computeManifestHash(base)
|
|
17224
17828
|
},
|
|
17225
|
-
blobs: [...blobs.values()]
|
|
17829
|
+
blobs: [...blobs.values()],
|
|
17830
|
+
storedBlobs: [...storedBlobs.values()]
|
|
17226
17831
|
};
|
|
17227
17832
|
}
|
|
17228
17833
|
function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
17229
|
-
if (
|
|
17230
|
-
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
17231
|
-
}
|
|
17232
|
-
const publication = buildPlanV2Publication(executionPlanDir);
|
|
17233
|
-
mkdirSync14(dirname15(planV2Dir), { recursive: true });
|
|
17234
|
-
const tempDir = mkdtempSync6(join38(dirname15(planV2Dir), ".plan-v2-build-"));
|
|
17235
|
-
try {
|
|
17236
|
-
for (const blob of publication.blobs) {
|
|
17237
|
-
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
17238
|
-
}
|
|
17239
|
-
writeFileSync10(
|
|
17240
|
-
join38(tempDir, "plan.json"),
|
|
17241
|
-
canonicalJsonStringify(publication.manifest),
|
|
17242
|
-
"utf-8"
|
|
17243
|
-
);
|
|
17244
|
-
renameSync7(tempDir, planV2Dir);
|
|
17245
|
-
return resultFromManifest(planV2Dir, publication.manifest);
|
|
17246
|
-
} catch (error) {
|
|
17247
|
-
rmSync13(tempDir, { recursive: true, force: true });
|
|
17248
|
-
throw error;
|
|
17249
|
-
}
|
|
17250
|
-
}
|
|
17251
|
-
function createPlanV2FromV1(executionPlanDir, planV2Dir) {
|
|
17252
|
-
return createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir);
|
|
17253
|
-
}
|
|
17254
|
-
async function publishPlanV2FromExecutionPlan(executionPlanDir, publisher) {
|
|
17255
|
-
return publishExecutionPlan(executionPlanDir, publisher, {});
|
|
17256
|
-
}
|
|
17257
|
-
async function publishExecutionPlan(executionPlanDir, publisher, { digests, log = defaultLogger }) {
|
|
17258
|
-
try {
|
|
17259
|
-
let stageStarted = Date.now();
|
|
17260
|
-
const publication = buildPlanV2Publication(executionPlanDir, digests);
|
|
17261
|
-
logPlanStage(log, "publication", stageStarted);
|
|
17262
|
-
stageStarted = Date.now();
|
|
17263
|
-
await putBlobs(publisher, publication.blobs);
|
|
17264
|
-
logPlanStage(log, "upload", stageStarted);
|
|
17265
|
-
stageStarted = Date.now();
|
|
17266
|
-
await publisher.commitManifest(canonicalJsonStringify(publication.manifest));
|
|
17267
|
-
logPlanStage(log, "manifest", stageStarted);
|
|
17268
|
-
return publication.manifest;
|
|
17269
|
-
} catch (error) {
|
|
17270
|
-
try {
|
|
17271
|
-
await publisher.abort();
|
|
17272
|
-
} catch {
|
|
17273
|
-
}
|
|
17274
|
-
throw error;
|
|
17275
|
-
}
|
|
17276
|
-
}
|
|
17277
|
-
async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
17278
|
-
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
17279
|
-
}
|
|
17280
|
-
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
17281
|
-
const stagingRoot = mkdtempSync6(join38(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
|
|
17282
|
-
const digests = /* @__PURE__ */ new Map();
|
|
17283
|
-
try {
|
|
17284
|
-
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
17285
|
-
executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER,
|
|
17286
|
-
deferVideoExtraction: true,
|
|
17287
|
-
digests,
|
|
17288
|
-
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
17289
|
-
whileAudioMixes: () => putBlobs(
|
|
17290
|
-
publisher,
|
|
17291
|
-
listFiles(join38(stagingRoot, "compiled")).map(
|
|
17292
|
-
(file) => publishBlobFor(file.absolutePath, digests)
|
|
17293
|
-
)
|
|
17294
|
-
),
|
|
17295
|
-
chunksCarryAudio: options.chunksCarryAudio
|
|
17296
|
-
});
|
|
17297
|
-
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
17298
|
-
} catch (error) {
|
|
17299
|
-
try {
|
|
17300
|
-
await publisher.abort();
|
|
17301
|
-
} catch {
|
|
17302
|
-
}
|
|
17303
|
-
throw error;
|
|
17304
|
-
} finally {
|
|
17305
|
-
rmSync13(stagingRoot, { recursive: true, force: true });
|
|
17306
|
-
}
|
|
17307
|
-
}
|
|
17308
|
-
async function planV2(projectDir, config, planV2Dir) {
|
|
17309
|
-
if (existsSync25(planV2Dir)) {
|
|
17834
|
+
if (existsSync26(planV2Dir)) {
|
|
17310
17835
|
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
17311
17836
|
}
|
|
17312
|
-
const
|
|
17313
|
-
|
|
17314
|
-
|
|
17315
|
-
}
|
|
17316
|
-
|
|
17317
|
-
|
|
17318
|
-
function resultFromManifest(planV2Dir, manifest) {
|
|
17319
|
-
return {
|
|
17320
|
-
planDir: planV2Dir,
|
|
17321
|
-
manifestPath: join38(planV2Dir, "plan.json"),
|
|
17322
|
-
planProtocol: PLAN_PROTOCOL_V2,
|
|
17323
|
-
planHash: manifest.planHash,
|
|
17324
|
-
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
17325
|
-
chunkCount: manifest.chunkCount,
|
|
17326
|
-
totalFrames: manifest.totalFrames,
|
|
17327
|
-
fps: manifest.fps,
|
|
17328
|
-
width: manifest.width,
|
|
17329
|
-
height: manifest.height,
|
|
17330
|
-
format: manifest.format,
|
|
17331
|
-
ffmpegVersion: manifest.ffmpegVersion,
|
|
17332
|
-
producerVersion: manifest.producerVersion,
|
|
17333
|
-
limitations: manifest.limitations
|
|
17334
|
-
};
|
|
17335
|
-
}
|
|
17336
|
-
function getPlanV2ExecutionPlanHash(plan2) {
|
|
17337
|
-
return plan2.sourcePlanV1Hash;
|
|
17338
|
-
}
|
|
17339
|
-
function readString2(value, field) {
|
|
17340
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
17341
|
-
throw new PlanV2IntegrityError(`${field} must be a non-empty string`);
|
|
17342
|
-
}
|
|
17343
|
-
return value;
|
|
17344
|
-
}
|
|
17345
|
-
function readPositiveInteger2(value, field) {
|
|
17346
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
17347
|
-
throw new PlanV2IntegrityError(`${field} must be a positive integer`);
|
|
17348
|
-
}
|
|
17349
|
-
return value;
|
|
17350
|
-
}
|
|
17351
|
-
function readNonNegativeInteger2(value, field) {
|
|
17352
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
17353
|
-
throw new PlanV2IntegrityError(`${field} must be a non-negative integer`);
|
|
17354
|
-
}
|
|
17355
|
-
return value;
|
|
17356
|
-
}
|
|
17357
|
-
function readSupportedFps(value) {
|
|
17358
|
-
if (!isDistributedFps3(value)) {
|
|
17359
|
-
throw new PlanV2IntegrityError(
|
|
17360
|
-
`dimensions.fpsNum must be one of ${DISTRIBUTED_FPS2.join(", ")}`
|
|
17361
|
-
);
|
|
17362
|
-
}
|
|
17363
|
-
return value;
|
|
17364
|
-
}
|
|
17365
|
-
function readExecutionPlanFps(dimensions) {
|
|
17366
|
-
const fpsDen = readPositiveInteger2(dimensions.fpsDen, "dimensions.fpsDen");
|
|
17367
|
-
if (fpsDen !== 1) {
|
|
17368
|
-
throw new PlanV2IntegrityError("dimensions.fpsDen must be 1 for plan v2");
|
|
17369
|
-
}
|
|
17370
|
-
return readSupportedFps(dimensions.fpsNum);
|
|
17371
|
-
}
|
|
17372
|
-
function readDistributedFormat(value) {
|
|
17373
|
-
if (value !== "mp4" && value !== "mov" && value !== "webm" && value !== "png-sequence") {
|
|
17374
|
-
throw new PlanV2IntegrityError("dimensions.format is unsupported");
|
|
17375
|
-
}
|
|
17376
|
-
return value;
|
|
17377
|
-
}
|
|
17378
|
-
function parseArtifact(value, index) {
|
|
17379
|
-
if (!isRecord3(value)) throw new PlanV2IntegrityError(`artifacts[${index}] must be an object`);
|
|
17380
|
-
const path = readString2(value.path, `artifacts[${index}].path`);
|
|
17381
|
-
assertSafeRelativePath(path);
|
|
17382
|
-
if (!isSha256(value.sha256)) {
|
|
17383
|
-
throw new PlanV2IntegrityError(`artifacts[${index}].sha256 must be a lowercase sha256`);
|
|
17384
|
-
}
|
|
17385
|
-
const sizeBytes = value.sizeBytes;
|
|
17386
|
-
if (typeof sizeBytes !== "number" || !Number.isInteger(sizeBytes) || sizeBytes < 0) {
|
|
17387
|
-
throw new PlanV2IntegrityError(`artifacts[${index}].sizeBytes must be a non-negative integer`);
|
|
17388
|
-
}
|
|
17389
|
-
let chunks;
|
|
17390
|
-
if (value.chunks === "all") {
|
|
17391
|
-
chunks = "all";
|
|
17392
|
-
} else if (isNonNegativeIntegerArray(value.chunks)) {
|
|
17393
|
-
chunks = value.chunks;
|
|
17394
|
-
} else {
|
|
17395
|
-
throw new PlanV2IntegrityError(`artifacts[${index}].chunks is invalid`);
|
|
17396
|
-
}
|
|
17397
|
-
if (typeof value.assembler !== "boolean") {
|
|
17398
|
-
throw new PlanV2IntegrityError(`artifacts[${index}].assembler must be boolean`);
|
|
17399
|
-
}
|
|
17400
|
-
return {
|
|
17401
|
-
path,
|
|
17402
|
-
sha256: value.sha256,
|
|
17403
|
-
sizeBytes,
|
|
17404
|
-
chunks,
|
|
17405
|
-
assembler: value.assembler
|
|
17406
|
-
};
|
|
17407
|
-
}
|
|
17408
|
-
function parsePlanV2Manifest(value) {
|
|
17409
|
-
if (!isRecord3(value)) throw new PlanV2IntegrityError("manifest must be an object");
|
|
17410
|
-
if (!isRecord3(value.protocol) || value.protocol.schemaVersion !== PLAN_PROTOCOL_V2.schemaVersion || value.protocol.artifactLayout !== PLAN_PROTOCOL_V2.artifactLayout || value.protocol.hashSchema !== PLAN_PROTOCOL_V2.hashSchema) {
|
|
17411
|
-
throw new PlanV2IntegrityError("manifest protocol is not the supported v2 descriptor");
|
|
17412
|
-
}
|
|
17413
|
-
if (!isSha256(value.planHash) || !isSha256(value.sourcePlanV1Hash)) {
|
|
17414
|
-
throw new PlanV2IntegrityError("manifest hashes must be lowercase sha256 digests");
|
|
17415
|
-
}
|
|
17416
|
-
if (!Array.isArray(value.artifacts)) throw new PlanV2IntegrityError("artifacts must be an array");
|
|
17417
|
-
const artifacts = value.artifacts.map(parseArtifact);
|
|
17418
|
-
const paths = /* @__PURE__ */ new Set();
|
|
17419
|
-
for (const artifact of artifacts) {
|
|
17420
|
-
if (paths.has(artifact.path))
|
|
17421
|
-
throw new PlanV2IntegrityError(`duplicate artifact path: ${artifact.path}`);
|
|
17422
|
-
paths.add(artifact.path);
|
|
17423
|
-
}
|
|
17424
|
-
if (!paths.has("plan.json"))
|
|
17425
|
-
throw new PlanV2IntegrityError("manifest must include the local execution plan.json artifact");
|
|
17426
|
-
if (!isRecord3(value.limitations) || value.limitations.videoDependencyMode !== "exact-rendered-frames" && value.limitations.videoDependencyMode !== "full-source-pack" && value.limitations.videoDependencyMode !== "source-extract") {
|
|
17427
|
-
throw new PlanV2IntegrityError("unsupported video dependency mode");
|
|
17428
|
-
}
|
|
17429
|
-
const parsed = {
|
|
17430
|
-
protocol: PLAN_PROTOCOL_V2,
|
|
17431
|
-
planHash: value.planHash,
|
|
17432
|
-
sourcePlanV1Hash: value.sourcePlanV1Hash,
|
|
17433
|
-
chunkCount: readPositiveInteger2(value.chunkCount, "chunkCount"),
|
|
17434
|
-
totalFrames: readPositiveInteger2(value.totalFrames, "totalFrames"),
|
|
17435
|
-
fps: readSupportedFps(value.fps),
|
|
17436
|
-
width: readPositiveInteger2(value.width, "width"),
|
|
17437
|
-
height: readPositiveInteger2(value.height, "height"),
|
|
17438
|
-
format: readDistributedFormat(value.format),
|
|
17439
|
-
ffmpegVersion: readString2(value.ffmpegVersion, "ffmpegVersion"),
|
|
17440
|
-
producerVersion: readString2(value.producerVersion, "producerVersion"),
|
|
17441
|
-
limitations: { videoDependencyMode: value.limitations.videoDependencyMode },
|
|
17442
|
-
artifacts
|
|
17443
|
-
};
|
|
17444
|
-
const { planHash: _planHash, ...payload } = parsed;
|
|
17445
|
-
const expectedHash = computeManifestHash(payload);
|
|
17446
|
-
if (expectedHash !== parsed.planHash) {
|
|
17447
|
-
throw new PlanV2IntegrityError("manifest integrity hash mismatch");
|
|
17448
|
-
}
|
|
17449
|
-
for (const [index, artifact] of parsed.artifacts.entries()) {
|
|
17450
|
-
if (artifact.chunks !== "all" && artifact.chunks.some((chunkIndex) => chunkIndex >= parsed.chunkCount)) {
|
|
17451
|
-
throw new PlanV2IntegrityError(`artifacts[${index}].chunks contains an out-of-range index`);
|
|
17452
|
-
}
|
|
17453
|
-
}
|
|
17454
|
-
return parsed;
|
|
17455
|
-
}
|
|
17456
|
-
function readPlanV2Manifest(planV2Dir) {
|
|
17457
|
-
const manifestPath = join38(planV2Dir, "plan.json");
|
|
17458
|
-
if (!existsSync25(manifestPath)) {
|
|
17459
|
-
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
17460
|
-
}
|
|
17461
|
-
const value = readJsonFile(manifestPath, "v2 manifest");
|
|
17462
|
-
return parsePlanV2Manifest(value);
|
|
17463
|
-
}
|
|
17464
|
-
function listPlanV2ArtifactsForTarget(manifest, target) {
|
|
17465
|
-
if (target.role === "chunk") {
|
|
17466
|
-
if (!Number.isInteger(target.chunkIndex) || target.chunkIndex < 0 || target.chunkIndex >= manifest.chunkCount) {
|
|
17467
|
-
throw new PlanV2IntegrityError(`chunkIndex ${String(target.chunkIndex)} is out of range`);
|
|
17468
|
-
}
|
|
17469
|
-
return manifest.artifacts.filter(
|
|
17470
|
-
(artifact) => artifact.chunks === "all" || artifact.chunks.some((chunkIndex) => chunkIndex === target.chunkIndex)
|
|
17471
|
-
);
|
|
17472
|
-
}
|
|
17473
|
-
return manifest.artifacts.filter((artifact) => artifact.assembler);
|
|
17474
|
-
}
|
|
17475
|
-
function verifyBlob(planV2Dir, artifact) {
|
|
17476
|
-
const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
|
|
17477
|
-
if (!existsSync25(sourcePath)) {
|
|
17478
|
-
throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
|
|
17479
|
-
}
|
|
17480
|
-
const stats = statSync10(sourcePath);
|
|
17481
|
-
if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
|
|
17482
|
-
throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
|
|
17483
|
-
}
|
|
17484
|
-
if (sha256File(sourcePath) !== artifact.sha256) {
|
|
17485
|
-
throw new PlanV2IntegrityError(`artifact hash mismatch for ${artifact.path}`);
|
|
17486
|
-
}
|
|
17487
|
-
return sourcePath;
|
|
17488
|
-
}
|
|
17489
|
-
function materializePlanV2Target(planV2Dir, target, destinationDir) {
|
|
17490
|
-
if (existsSync25(destinationDir)) {
|
|
17491
|
-
throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
|
|
17492
|
-
}
|
|
17493
|
-
const manifest = readPlanV2Manifest(planV2Dir);
|
|
17494
|
-
const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
|
|
17495
|
-
const audioArtifact = artifacts.find((artifact) => isPlanAudioArtifactPath(artifact.path));
|
|
17496
|
-
const verified = artifacts.map((artifact) => ({
|
|
17497
|
-
artifact,
|
|
17498
|
-
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
17499
|
-
}));
|
|
17500
|
-
mkdirSync14(dirname15(destinationDir), { recursive: true });
|
|
17501
|
-
const tempDir = mkdtempSync6(join38(dirname15(destinationDir), ".plan-v2-materialize-"));
|
|
17837
|
+
const publication = buildPlanV2Publication(executionPlanDir);
|
|
17838
|
+
if (publication.storedBlobs.length > 0) {
|
|
17839
|
+
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
17840
|
+
}
|
|
17841
|
+
mkdirSync16(dirname16(planV2Dir), { recursive: true });
|
|
17842
|
+
const tempDir = mkdtempSync7(join40(dirname16(planV2Dir), ".plan-v2-build-"));
|
|
17502
17843
|
try {
|
|
17503
|
-
for (const
|
|
17504
|
-
|
|
17505
|
-
mkdirSync14(dirname15(destinationPath), { recursive: true });
|
|
17506
|
-
copyFileSync7(sourcePath, destinationPath);
|
|
17844
|
+
for (const blob of publication.blobs) {
|
|
17845
|
+
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
17507
17846
|
}
|
|
17508
|
-
|
|
17509
|
-
|
|
17510
|
-
|
|
17511
|
-
canonicalJsonStringify({ manifest, target }),
|
|
17847
|
+
writeFileSync11(
|
|
17848
|
+
join40(tempDir, "plan.json"),
|
|
17849
|
+
canonicalJsonStringify(publication.manifest),
|
|
17512
17850
|
"utf-8"
|
|
17513
17851
|
);
|
|
17514
|
-
|
|
17852
|
+
renameSync8(tempDir, planV2Dir);
|
|
17853
|
+
return resultFromManifest(planV2Dir, publication.manifest);
|
|
17515
17854
|
} catch (error) {
|
|
17516
|
-
|
|
17855
|
+
rmSync14(tempDir, { recursive: true, force: true });
|
|
17517
17856
|
throw error;
|
|
17518
17857
|
}
|
|
17519
|
-
return {
|
|
17520
|
-
planDir: destinationDir,
|
|
17521
|
-
target,
|
|
17522
|
-
planHash: manifest.planHash,
|
|
17523
|
-
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
17524
|
-
artifactCount: artifacts.length,
|
|
17525
|
-
sizeBytes: artifacts.reduce((sum2, artifact) => sum2 + artifact.sizeBytes, 0),
|
|
17526
|
-
// Join the artifact's OWN name, not the current constant: a plan written
|
|
17527
|
-
// before the container change carries the legacy name, and materializing it
|
|
17528
|
-
// under the new one would point at a file that was never written.
|
|
17529
|
-
audioPath: target.role === "assembler" ? audioArtifact ? join38(destinationDir, audioArtifact.path) : null : null
|
|
17530
|
-
};
|
|
17531
|
-
}
|
|
17532
|
-
function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
17533
|
-
const markerPath = join38(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
17534
|
-
if (!existsSync25(markerPath)) return null;
|
|
17535
|
-
const marker = readJsonFile(markerPath, "materialization marker");
|
|
17536
|
-
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
17537
|
-
throw new PlanV2IntegrityError("malformed materialization marker");
|
|
17538
|
-
}
|
|
17539
|
-
const target = marker.target;
|
|
17540
|
-
if (target.role !== expectedTarget.role || expectedTarget.role === "chunk" && target.chunkIndex !== expectedTarget.chunkIndex) {
|
|
17541
|
-
throw new PlanV2IntegrityError(
|
|
17542
|
-
"materialization target does not match requested execution role"
|
|
17543
|
-
);
|
|
17544
|
-
}
|
|
17545
|
-
const manifest = parsePlanV2Manifest(marker.manifest);
|
|
17546
|
-
const required = listPlanV2ArtifactsForTarget(manifest, expectedTarget);
|
|
17547
|
-
for (const artifact of required) {
|
|
17548
|
-
const path = join38(planDir, ...artifact.path.split("/"));
|
|
17549
|
-
if (!existsSync25(path) || statSync10(path).size !== artifact.sizeBytes) {
|
|
17550
|
-
throw new PlanV2IntegrityError(
|
|
17551
|
-
`materialized artifact missing or truncated: ${artifact.path}`
|
|
17552
|
-
);
|
|
17553
|
-
}
|
|
17554
|
-
if (sha256File(path) !== artifact.sha256) {
|
|
17555
|
-
throw new PlanV2IntegrityError(`materialized artifact hash mismatch: ${artifact.path}`);
|
|
17556
|
-
}
|
|
17557
|
-
}
|
|
17558
|
-
return manifest;
|
|
17559
|
-
}
|
|
17560
|
-
|
|
17561
|
-
// src/services/distributed/planV2Execution.ts
|
|
17562
|
-
import { mkdtempSync as mkdtempSync9, rmSync as rmSync17 } from "node:fs";
|
|
17563
|
-
import { tmpdir as tmpdir5 } from "node:os";
|
|
17564
|
-
import { join as join42 } from "node:path";
|
|
17565
|
-
|
|
17566
|
-
// src/services/distributed/assemble.ts
|
|
17567
|
-
import {
|
|
17568
|
-
cpSync as cpSync3,
|
|
17569
|
-
existsSync as existsSync26,
|
|
17570
|
-
mkdirSync as mkdirSync16,
|
|
17571
|
-
mkdtempSync as mkdtempSync8,
|
|
17572
|
-
readFileSync as readFileSync13,
|
|
17573
|
-
readdirSync as readdirSync10,
|
|
17574
|
-
renameSync as renameSync9,
|
|
17575
|
-
rmSync as rmSync15,
|
|
17576
|
-
statSync as statSync11,
|
|
17577
|
-
writeFileSync as writeFileSync11
|
|
17578
|
-
} from "node:fs";
|
|
17579
|
-
import { dirname as dirname17, join as join40 } from "node:path";
|
|
17580
|
-
import {
|
|
17581
|
-
appendRenderProvenanceArgs,
|
|
17582
|
-
describeFfmpegFailure as describeFfmpegFailure2,
|
|
17583
|
-
getEncoderPreset as getEncoderPreset5,
|
|
17584
|
-
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
17585
|
-
muxVideoWithAudio as muxVideoWithAudio2,
|
|
17586
|
-
resolveConfig as resolveConfig6,
|
|
17587
|
-
runFfmpeg as runFfmpeg5
|
|
17588
|
-
} from "@hyperframes/engine";
|
|
17589
|
-
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
17590
|
-
|
|
17591
|
-
// src/services/distributed/mp4Stitch.ts
|
|
17592
|
-
import {
|
|
17593
|
-
createReadStream as createReadStream5,
|
|
17594
|
-
createWriteStream,
|
|
17595
|
-
mkdirSync as mkdirSync15,
|
|
17596
|
-
mkdtempSync as mkdtempSync7,
|
|
17597
|
-
renameSync as renameSync8,
|
|
17598
|
-
rmSync as rmSync14
|
|
17599
|
-
} from "node:fs";
|
|
17600
|
-
import { open } from "node:fs/promises";
|
|
17601
|
-
import { dirname as dirname16, join as join39 } from "node:path";
|
|
17602
|
-
import { finished, pipeline } from "node:stream/promises";
|
|
17603
|
-
function boxAt(src, off, at, limit) {
|
|
17604
|
-
let size = src.readUInt32BE(off);
|
|
17605
|
-
let bodyStart = at + 8;
|
|
17606
|
-
if (size === 1) {
|
|
17607
|
-
size = Number(src.readBigUInt64BE(off + 8));
|
|
17608
|
-
bodyStart = at + 16;
|
|
17609
|
-
}
|
|
17610
|
-
if (size < 8 || at + size > limit) throw new Error(`[mp4Stitch] malformed box at ${at}`);
|
|
17611
|
-
return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
|
|
17612
|
-
}
|
|
17613
|
-
function childBoxes(buf, parent) {
|
|
17614
|
-
const boxes = [];
|
|
17615
|
-
for (let at = parent.bodyStart; at < parent.end; at = boxes.at(-1).end) {
|
|
17616
|
-
boxes.push(boxAt(buf, at, at, parent.end));
|
|
17617
|
-
}
|
|
17618
|
-
return boxes;
|
|
17619
17858
|
}
|
|
17620
|
-
function
|
|
17621
|
-
return
|
|
17859
|
+
function createPlanV2FromV1(executionPlanDir, planV2Dir) {
|
|
17860
|
+
return createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir);
|
|
17622
17861
|
}
|
|
17623
|
-
function
|
|
17624
|
-
|
|
17625
|
-
if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
|
|
17626
|
-
return found;
|
|
17862
|
+
async function publishPlanV2FromExecutionPlan(executionPlanDir, publisher) {
|
|
17863
|
+
return publishExecutionPlan(executionPlanDir, publisher, {});
|
|
17627
17864
|
}
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
if (start >= bufAt && end <= bufAt + buf.length) return;
|
|
17636
|
-
buf = await read(start, Math.max(end, start + SCAN_BYTES));
|
|
17637
|
-
bufAt = start;
|
|
17638
|
-
};
|
|
17639
|
-
for (let at = 0; !(found.has("ftyp") && found.has("moov") && payload); ) {
|
|
17640
|
-
await load(at, at + 16);
|
|
17641
|
-
if (at + 8 > bufAt + buf.length) {
|
|
17642
|
-
throw new Error(`[mp4Stitch] chunk needs ftyp, moov and mdat; found ${[...found.keys()]}`);
|
|
17865
|
+
async function publishExecutionPlan(executionPlanDir, publisher, { digests, log = defaultLogger }) {
|
|
17866
|
+
try {
|
|
17867
|
+
let stageStarted = Date.now();
|
|
17868
|
+
const publication = buildPlanV2Publication(executionPlanDir, digests);
|
|
17869
|
+
logPlanStage(log, "publication", stageStarted);
|
|
17870
|
+
if (publication.storedBlobs.length > 0 && !publisher.referenceStoredBlob) {
|
|
17871
|
+
throw new PlanV2IntegrityError("this publisher cannot reference remote files");
|
|
17643
17872
|
}
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17873
|
+
stageStarted = Date.now();
|
|
17874
|
+
await putTasks([
|
|
17875
|
+
...publication.blobs.map((blob) => () => publisher.putBlob(blob)),
|
|
17876
|
+
...publication.storedBlobs.map((blob) => () => publisher.referenceStoredBlob(blob))
|
|
17877
|
+
]);
|
|
17878
|
+
logPlanStage(log, "upload", stageStarted);
|
|
17879
|
+
stageStarted = Date.now();
|
|
17880
|
+
await publisher.commitManifest(canonicalJsonStringify(publication.manifest));
|
|
17881
|
+
logPlanStage(log, "manifest", stageStarted);
|
|
17882
|
+
return publication.manifest;
|
|
17883
|
+
} catch (error) {
|
|
17884
|
+
try {
|
|
17885
|
+
await publisher.abort();
|
|
17886
|
+
} catch {
|
|
17649
17887
|
}
|
|
17650
|
-
|
|
17888
|
+
throw error;
|
|
17651
17889
|
}
|
|
17652
|
-
const [ftyp, moov] = [found.get("ftyp"), found.get("moov")];
|
|
17653
|
-
if (moov.length !== moovSize(moov)) throw new Error("[mp4Stitch] moov is cut off");
|
|
17654
|
-
return { ftyp, moov, payloadStart: payload.bodyStart, payloadEnd: payload.end };
|
|
17655
|
-
}
|
|
17656
|
-
var moovSize = (moov) => boxAt(moov, 0, 0, Number.MAX_SAFE_INTEGER).end;
|
|
17657
|
-
function readTracks(moov) {
|
|
17658
|
-
const root = boxAt(moov, 0, 0, moov.length);
|
|
17659
|
-
return childBoxes(moov, root).filter((box2) => box2.type === "trak").map((trak) => readTrack(moov, trak));
|
|
17660
17890
|
}
|
|
17661
|
-
function
|
|
17662
|
-
|
|
17663
|
-
const first = table.bodyStart + countAt + 4;
|
|
17664
|
-
return Array.from(
|
|
17665
|
-
{ length: count },
|
|
17666
|
-
(_, row) => Array.from(
|
|
17667
|
-
{ length: columns },
|
|
17668
|
-
(_2, col) => buf.readUInt32BE(first + (row * columns + col) * 4)
|
|
17669
|
-
)
|
|
17670
|
-
);
|
|
17891
|
+
async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
17892
|
+
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
17671
17893
|
}
|
|
17672
|
-
function
|
|
17673
|
-
const
|
|
17674
|
-
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17894
|
+
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
17895
|
+
const stagingRoot = mkdtempSync7(join40(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
|
|
17896
|
+
const digests = /* @__PURE__ */ new Map();
|
|
17897
|
+
try {
|
|
17898
|
+
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
17899
|
+
executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER,
|
|
17900
|
+
deferVideoExtraction: true,
|
|
17901
|
+
remoteFiles: options.remoteFiles,
|
|
17902
|
+
digests,
|
|
17903
|
+
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
17904
|
+
whileAudioMixes: () => putBlobs(
|
|
17905
|
+
publisher,
|
|
17906
|
+
listFiles(join40(stagingRoot, "compiled")).map(
|
|
17907
|
+
(file) => publishBlobFor(file.absolutePath, digests)
|
|
17908
|
+
)
|
|
17909
|
+
),
|
|
17910
|
+
chunksCarryAudio: options.chunksCarryAudio,
|
|
17911
|
+
sliceChunkAudio: true
|
|
17912
|
+
});
|
|
17913
|
+
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
17914
|
+
} catch (error) {
|
|
17915
|
+
try {
|
|
17916
|
+
await publisher.abort();
|
|
17917
|
+
} catch {
|
|
17918
|
+
}
|
|
17919
|
+
throw error;
|
|
17920
|
+
} finally {
|
|
17921
|
+
rmSync14(stagingRoot, { recursive: true, force: true });
|
|
17682
17922
|
}
|
|
17683
|
-
return 0;
|
|
17684
|
-
}
|
|
17685
|
-
function readSizes(moov, stsz) {
|
|
17686
|
-
const fixedSize = moov.readUInt32BE(stsz.bodyStart + 4);
|
|
17687
|
-
if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
|
|
17688
|
-
return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
|
|
17689
|
-
}
|
|
17690
|
-
function readSync4(moov, stbl, sampleCount) {
|
|
17691
|
-
const stss = findChild(moov, stbl, "stss");
|
|
17692
|
-
if (!stss) return null;
|
|
17693
|
-
const sync = Array(sampleCount).fill(false);
|
|
17694
|
-
for (const [n] of rows(moov, stss, 1)) sync[n - 1] = true;
|
|
17695
|
-
return sync;
|
|
17696
|
-
}
|
|
17697
|
-
function readChunkOffsets(moov, stbl) {
|
|
17698
|
-
const stco = findChild(moov, stbl, "stco");
|
|
17699
|
-
if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
|
|
17700
|
-
return rows(moov, child(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
|
|
17701
|
-
}
|
|
17702
|
-
function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
17703
|
-
const stsc = rows(moov, child(moov, stbl, "stsc"), 3);
|
|
17704
|
-
return stsc.flatMap(([firstChunk, perChunk, description], i) => {
|
|
17705
|
-
if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
|
|
17706
|
-
const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
|
|
17707
|
-
return Array(nextFirst - firstChunk).fill(perChunk);
|
|
17708
|
-
});
|
|
17709
17923
|
}
|
|
17710
|
-
function
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
const stbl = child(moov, child(moov, mdia, "minf"), "stbl");
|
|
17714
|
-
if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
|
|
17715
|
-
const stts = rows(moov, child(moov, stbl, "stts"), 2);
|
|
17716
|
-
const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
|
|
17717
|
-
const sizes = readSizes(moov, child(moov, stbl, "stsz"));
|
|
17718
|
-
const chunkOffsets = readChunkOffsets(moov, stbl);
|
|
17719
|
-
const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
|
|
17720
|
-
if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
|
|
17721
|
-
throw new Error("[mp4Stitch] sample tables disagree on the sample count");
|
|
17924
|
+
async function planV2(projectDir, config, planV2Dir) {
|
|
17925
|
+
if (existsSync26(planV2Dir)) {
|
|
17926
|
+
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
17722
17927
|
}
|
|
17723
|
-
const
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17732
|
-
|
|
17733
|
-
|
|
17734
|
-
|
|
17928
|
+
const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
|
|
17929
|
+
const manifest = await planV2WithPublisher(projectDir, config, publisher, {
|
|
17930
|
+
stagingParentDir: dirname16(planV2Dir)
|
|
17931
|
+
});
|
|
17932
|
+
return resultFromManifest(planV2Dir, manifest);
|
|
17933
|
+
}
|
|
17934
|
+
function resultFromManifest(planV2Dir, manifest) {
|
|
17935
|
+
return {
|
|
17936
|
+
planDir: planV2Dir,
|
|
17937
|
+
manifestPath: join40(planV2Dir, "plan.json"),
|
|
17938
|
+
planProtocol: PLAN_PROTOCOL_V2,
|
|
17939
|
+
planHash: manifest.planHash,
|
|
17940
|
+
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
17941
|
+
chunkCount: manifest.chunkCount,
|
|
17942
|
+
totalFrames: manifest.totalFrames,
|
|
17943
|
+
fps: manifest.fps,
|
|
17944
|
+
width: manifest.width,
|
|
17945
|
+
height: manifest.height,
|
|
17946
|
+
format: manifest.format,
|
|
17947
|
+
ffmpegVersion: manifest.ffmpegVersion,
|
|
17948
|
+
producerVersion: manifest.producerVersion,
|
|
17949
|
+
limitations: manifest.limitations
|
|
17735
17950
|
};
|
|
17736
17951
|
}
|
|
17737
|
-
function
|
|
17738
|
-
return
|
|
17952
|
+
function getPlanV2ExecutionPlanHash(plan2) {
|
|
17953
|
+
return plan2.sourcePlanV1Hash;
|
|
17739
17954
|
}
|
|
17740
|
-
function
|
|
17741
|
-
|
|
17742
|
-
|
|
17743
|
-
|
|
17744
|
-
return
|
|
17955
|
+
function readString2(value, field) {
|
|
17956
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
17957
|
+
throw new PlanV2IntegrityError(`${field} must be a non-empty string`);
|
|
17958
|
+
}
|
|
17959
|
+
return value;
|
|
17745
17960
|
}
|
|
17746
|
-
function
|
|
17747
|
-
|
|
17961
|
+
function readPositiveInteger2(value, field) {
|
|
17962
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
17963
|
+
throw new PlanV2IntegrityError(`${field} must be a positive integer`);
|
|
17964
|
+
}
|
|
17965
|
+
return value;
|
|
17748
17966
|
}
|
|
17749
|
-
function
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17967
|
+
function readNonNegativeInteger2(value, field) {
|
|
17968
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
17969
|
+
throw new PlanV2IntegrityError(`${field} must be a non-negative integer`);
|
|
17970
|
+
}
|
|
17971
|
+
return value;
|
|
17753
17972
|
}
|
|
17754
|
-
function
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
else out.push([1, value]);
|
|
17973
|
+
function readSupportedFps(value) {
|
|
17974
|
+
if (!isDistributedFps3(value)) {
|
|
17975
|
+
throw new PlanV2IntegrityError(
|
|
17976
|
+
`dimensions.fpsNum must be one of ${DISTRIBUTED_FPS2.join(", ")}`
|
|
17977
|
+
);
|
|
17760
17978
|
}
|
|
17761
|
-
return
|
|
17979
|
+
return value;
|
|
17762
17980
|
}
|
|
17763
|
-
function
|
|
17764
|
-
const
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17981
|
+
function readExecutionPlanFps(dimensions) {
|
|
17982
|
+
const fpsDen = readPositiveInteger2(dimensions.fpsDen, "dimensions.fpsDen");
|
|
17983
|
+
if (fpsDen !== 1) {
|
|
17984
|
+
throw new PlanV2IntegrityError("dimensions.fpsDen must be 1 for plan v2");
|
|
17985
|
+
}
|
|
17986
|
+
return readSupportedFps(dimensions.fpsNum);
|
|
17768
17987
|
}
|
|
17769
|
-
|
|
17770
|
-
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
function withDuration(buf, target, duration) {
|
|
17775
|
-
const copy = Buffer.from(buf.subarray(target.start, target.end));
|
|
17776
|
-
const body = target.bodyStart - target.start;
|
|
17777
|
-
const wide = copy[body] === 1;
|
|
17778
|
-
const at = body + DURATION_AT[target.type][wide ? 1 : 0];
|
|
17779
|
-
if (wide) copy.writeBigUInt64BE(BigInt(duration), at);
|
|
17780
|
-
else if (duration <= 4294967295) copy.writeUInt32BE(duration, at);
|
|
17781
|
-
else throw new Error(`[mp4Stitch] ${target.type} duration ${duration} needs a version 1 box`);
|
|
17782
|
-
return copy;
|
|
17988
|
+
function readDistributedFormat(value) {
|
|
17989
|
+
if (value !== "mp4" && value !== "mov" && value !== "webm" && value !== "png-sequence") {
|
|
17990
|
+
throw new PlanV2IntegrityError("dimensions.format is unsupported");
|
|
17991
|
+
}
|
|
17992
|
+
return value;
|
|
17783
17993
|
}
|
|
17784
|
-
function
|
|
17785
|
-
|
|
17994
|
+
function parseArtifact(value, index) {
|
|
17995
|
+
if (!isRecord3(value)) throw new PlanV2IntegrityError(`artifacts[${index}] must be an object`);
|
|
17996
|
+
const path = readString2(value.path, `artifacts[${index}].path`);
|
|
17997
|
+
assertSafeRelativePath(path);
|
|
17998
|
+
if (!isSha256(value.sha256)) {
|
|
17999
|
+
throw new PlanV2IntegrityError(`artifacts[${index}].sha256 must be a lowercase sha256`);
|
|
18000
|
+
}
|
|
18001
|
+
const sizeBytes = value.sizeBytes;
|
|
18002
|
+
if (typeof sizeBytes !== "number" || !Number.isInteger(sizeBytes) || sizeBytes < 0) {
|
|
18003
|
+
throw new PlanV2IntegrityError(`artifacts[${index}].sizeBytes must be a non-negative integer`);
|
|
18004
|
+
}
|
|
18005
|
+
let chunks;
|
|
18006
|
+
if (value.chunks === "all") {
|
|
18007
|
+
chunks = "all";
|
|
18008
|
+
} else if (isNonNegativeIntegerArray(value.chunks)) {
|
|
18009
|
+
chunks = value.chunks;
|
|
18010
|
+
} else {
|
|
18011
|
+
throw new PlanV2IntegrityError(`artifacts[${index}].chunks is invalid`);
|
|
18012
|
+
}
|
|
18013
|
+
if (typeof value.assembler !== "boolean") {
|
|
18014
|
+
throw new PlanV2IntegrityError(`artifacts[${index}].assembler must be boolean`);
|
|
18015
|
+
}
|
|
18016
|
+
return {
|
|
18017
|
+
path,
|
|
18018
|
+
sha256: value.sha256,
|
|
18019
|
+
sizeBytes,
|
|
18020
|
+
chunks,
|
|
18021
|
+
assembler: value.assembler
|
|
18022
|
+
};
|
|
17786
18023
|
}
|
|
17787
|
-
function
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
for (const [count, perChunk] of runs(track.samplesPerChunk)) {
|
|
17792
|
-
stsc.push([firstChunk, perChunk, 1]);
|
|
17793
|
-
firstChunk += count;
|
|
18024
|
+
function parsePlanV2Manifest(value) {
|
|
18025
|
+
if (!isRecord3(value)) throw new PlanV2IntegrityError("manifest must be an object");
|
|
18026
|
+
if (!isRecord3(value.protocol) || value.protocol.schemaVersion !== PLAN_PROTOCOL_V2.schemaVersion || value.protocol.artifactLayout !== PLAN_PROTOCOL_V2.artifactLayout || value.protocol.hashSchema !== PLAN_PROTOCOL_V2.hashSchema) {
|
|
18027
|
+
throw new PlanV2IntegrityError("manifest protocol is not the supported v2 descriptor");
|
|
17794
18028
|
}
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
|
|
17804
|
-
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
18029
|
+
if (!isSha256(value.planHash) || !isSha256(value.sourcePlanV1Hash)) {
|
|
18030
|
+
throw new PlanV2IntegrityError("manifest hashes must be lowercase sha256 digests");
|
|
18031
|
+
}
|
|
18032
|
+
if (!Array.isArray(value.artifacts)) throw new PlanV2IntegrityError("artifacts must be an array");
|
|
18033
|
+
const artifacts = value.artifacts.map(parseArtifact);
|
|
18034
|
+
const paths = /* @__PURE__ */ new Set();
|
|
18035
|
+
for (const artifact of artifacts) {
|
|
18036
|
+
if (paths.has(artifact.path))
|
|
18037
|
+
throw new PlanV2IntegrityError(`duplicate artifact path: ${artifact.path}`);
|
|
18038
|
+
paths.add(artifact.path);
|
|
18039
|
+
}
|
|
18040
|
+
if (!paths.has("plan.json"))
|
|
18041
|
+
throw new PlanV2IntegrityError("manifest must include the local execution plan.json artifact");
|
|
18042
|
+
if (!isRecord3(value.limitations) || value.limitations.videoDependencyMode !== "exact-rendered-frames" && value.limitations.videoDependencyMode !== "full-source-pack" && value.limitations.videoDependencyMode !== "source-extract") {
|
|
18043
|
+
throw new PlanV2IntegrityError("unsupported video dependency mode");
|
|
18044
|
+
}
|
|
18045
|
+
const parsed = {
|
|
18046
|
+
protocol: PLAN_PROTOCOL_V2,
|
|
18047
|
+
planHash: value.planHash,
|
|
18048
|
+
sourcePlanV1Hash: value.sourcePlanV1Hash,
|
|
18049
|
+
chunkCount: readPositiveInteger2(value.chunkCount, "chunkCount"),
|
|
18050
|
+
totalFrames: readPositiveInteger2(value.totalFrames, "totalFrames"),
|
|
18051
|
+
fps: readSupportedFps(value.fps),
|
|
18052
|
+
width: readPositiveInteger2(value.width, "width"),
|
|
18053
|
+
height: readPositiveInteger2(value.height, "height"),
|
|
18054
|
+
format: readDistributedFormat(value.format),
|
|
18055
|
+
ffmpegVersion: readString2(value.ffmpegVersion, "ffmpegVersion"),
|
|
18056
|
+
producerVersion: readString2(value.producerVersion, "producerVersion"),
|
|
18057
|
+
limitations: { videoDependencyMode: value.limitations.videoDependencyMode },
|
|
18058
|
+
artifacts
|
|
18059
|
+
};
|
|
18060
|
+
const { planHash: _planHash, ...payload } = parsed;
|
|
18061
|
+
const expectedHash = computeManifestHash(payload);
|
|
18062
|
+
if (expectedHash !== parsed.planHash) {
|
|
18063
|
+
throw new PlanV2IntegrityError("manifest integrity hash mismatch");
|
|
18064
|
+
}
|
|
18065
|
+
for (const [index, artifact] of parsed.artifacts.entries()) {
|
|
18066
|
+
if (artifact.chunks !== "all" && artifact.chunks.some((chunkIndex) => chunkIndex >= parsed.chunkCount)) {
|
|
18067
|
+
throw new PlanV2IntegrityError(`artifacts[${index}].chunks contains an out-of-range index`);
|
|
18068
|
+
}
|
|
18069
|
+
}
|
|
18070
|
+
return parsed;
|
|
17808
18071
|
}
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17820
|
-
|
|
17821
|
-
for (const track of tracks) {
|
|
17822
|
-
if (track.handler === "vide" && track.mediaTime !== 0) {
|
|
17823
|
-
throw new Error(`[mp4Stitch] chunk ${index} video starts at media time ${track.mediaTime}`);
|
|
18072
|
+
function readPlanV2Manifest(planV2Dir) {
|
|
18073
|
+
const manifestPath = join40(planV2Dir, "plan.json");
|
|
18074
|
+
if (!existsSync26(manifestPath)) {
|
|
18075
|
+
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
18076
|
+
}
|
|
18077
|
+
const value = readJsonFile(manifestPath, "v2 manifest");
|
|
18078
|
+
return parsePlanV2Manifest(value);
|
|
18079
|
+
}
|
|
18080
|
+
function listPlanV2ArtifactsForTarget(manifest, target) {
|
|
18081
|
+
if (target.role === "chunk") {
|
|
18082
|
+
if (!Number.isInteger(target.chunkIndex) || target.chunkIndex < 0 || target.chunkIndex >= manifest.chunkCount) {
|
|
18083
|
+
throw new PlanV2IntegrityError(`chunkIndex ${String(target.chunkIndex)} is out of range`);
|
|
17824
18084
|
}
|
|
18085
|
+
return manifest.artifacts.filter(
|
|
18086
|
+
(artifact) => artifact.chunks === "all" || artifact.chunks.some((chunkIndex) => chunkIndex === target.chunkIndex)
|
|
18087
|
+
);
|
|
17825
18088
|
}
|
|
17826
|
-
return
|
|
18089
|
+
return manifest.artifacts.filter((artifact) => artifact.assembler);
|
|
17827
18090
|
}
|
|
17828
|
-
function
|
|
17829
|
-
const
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
18091
|
+
function verifyBlob(planV2Dir, artifact) {
|
|
18092
|
+
const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
|
|
18093
|
+
if (!existsSync26(sourcePath)) {
|
|
18094
|
+
throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
|
|
18095
|
+
}
|
|
18096
|
+
const stats = statSync10(sourcePath);
|
|
18097
|
+
if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
|
|
18098
|
+
throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
|
|
18099
|
+
}
|
|
18100
|
+
if (sha256File(sourcePath) !== artifact.sha256) {
|
|
18101
|
+
throw new PlanV2IntegrityError(`artifact hash mismatch for ${artifact.path}`);
|
|
18102
|
+
}
|
|
18103
|
+
return sourcePath;
|
|
18104
|
+
}
|
|
18105
|
+
function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}) {
|
|
18106
|
+
if (existsSync26(destinationDir)) {
|
|
18107
|
+
throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
|
|
18108
|
+
}
|
|
18109
|
+
const manifest = readPlanV2Manifest(planV2Dir);
|
|
18110
|
+
const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
|
|
18111
|
+
const remoteSources = options.remoteSources ?? {};
|
|
18112
|
+
assertRemoteSources(remoteSources, artifacts);
|
|
18113
|
+
const audioArtifact = artifacts.find((artifact) => isPlanAudioArtifactPath(artifact.path));
|
|
18114
|
+
const verified = artifacts.filter((artifact) => !Object.hasOwn(remoteSources, artifact.path)).map((artifact) => ({
|
|
18115
|
+
artifact,
|
|
18116
|
+
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
18117
|
+
}));
|
|
18118
|
+
mkdirSync16(dirname16(destinationDir), { recursive: true });
|
|
18119
|
+
const tempDir = mkdtempSync7(join40(dirname16(destinationDir), ".plan-v2-materialize-"));
|
|
18120
|
+
try {
|
|
18121
|
+
for (const { artifact, sourcePath } of verified) {
|
|
18122
|
+
const destinationPath = join40(tempDir, ...artifact.path.split("/"));
|
|
18123
|
+
mkdirSync16(dirname16(destinationPath), { recursive: true });
|
|
18124
|
+
copyFileSync7(sourcePath, destinationPath);
|
|
18125
|
+
}
|
|
18126
|
+
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
18127
|
+
writeFileSync11(
|
|
18128
|
+
join40(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
18129
|
+
canonicalJsonStringify({
|
|
18130
|
+
manifest,
|
|
18131
|
+
target,
|
|
18132
|
+
...Object.keys(remoteSources).length > 0 ? { remoteSources } : {}
|
|
18133
|
+
}),
|
|
18134
|
+
"utf-8"
|
|
18135
|
+
);
|
|
18136
|
+
renameSync8(tempDir, destinationDir);
|
|
18137
|
+
} catch (error) {
|
|
18138
|
+
rmSync14(tempDir, { recursive: true, force: true });
|
|
18139
|
+
throw error;
|
|
17836
18140
|
}
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
sizes: parts.flatMap((part) => part.sizes),
|
|
17849
|
-
sync: parts.every((part) => !part.sync) ? null : parts.flatMap((part) => part.sync ?? part.sizes.map(() => true)),
|
|
17850
|
-
samplesPerChunk: parts.flatMap((part) => part.samplesPerChunk),
|
|
17851
|
-
chunkOffsets: parts.flatMap(
|
|
17852
|
-
(part) => part.chunkOffsets.map((offset) => {
|
|
17853
|
-
const i = part.chunk;
|
|
17854
|
-
const { payloadStart, payloadEnd } = layouts[i];
|
|
17855
|
-
if (offset < payloadStart || offset >= payloadEnd) {
|
|
17856
|
-
throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
|
|
17857
|
-
}
|
|
17858
|
-
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
17859
|
-
})
|
|
17860
|
-
)
|
|
17861
|
-
};
|
|
17862
|
-
const mediaDuration = sum(merged.durations);
|
|
17863
|
-
const shown = Math.round(
|
|
17864
|
-
(mediaDuration - track.mediaTime) * movieTimescale / track.timescale
|
|
17865
|
-
);
|
|
17866
|
-
const elst = Buffer.alloc(24);
|
|
17867
|
-
elst.writeUInt32BE(1);
|
|
17868
|
-
elst.writeBigUInt64BE(BigInt(shown), 4);
|
|
17869
|
-
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
17870
|
-
elst.writeUInt32BE(65536, 20);
|
|
17871
|
-
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
17872
|
-
const trak = rebuild(template, track.trak, {
|
|
17873
|
-
tkhd: (tkhd) => Buffer.concat([withDuration(template, tkhd, shown), edts]),
|
|
17874
|
-
edts: () => Buffer.alloc(0),
|
|
17875
|
-
mdia: (mdia) => rebuild(template, mdia, {
|
|
17876
|
-
mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
|
|
17877
|
-
minf: (minf) => rebuild(template, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
17878
|
-
})
|
|
17879
|
-
});
|
|
17880
|
-
return { trak, shown };
|
|
17881
|
-
});
|
|
17882
|
-
let next = 0;
|
|
17883
|
-
return rebuild(template, root, {
|
|
17884
|
-
mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
|
|
17885
|
-
trak: () => traks[next++].trak
|
|
17886
|
-
});
|
|
18141
|
+
return {
|
|
18142
|
+
planDir: destinationDir,
|
|
18143
|
+
target,
|
|
18144
|
+
planHash: manifest.planHash,
|
|
18145
|
+
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
18146
|
+
artifactCount: artifacts.length,
|
|
18147
|
+
sizeBytes: artifacts.reduce((sum2, artifact) => sum2 + artifact.sizeBytes, 0),
|
|
18148
|
+
// Join the artifact's OWN name, not the current constant: a plan written
|
|
18149
|
+
// before the container change carries the legacy name, and materializing it
|
|
18150
|
+
// under the new one would point at a file that was never written.
|
|
18151
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join40(destinationDir, audioArtifact.path) : null : null
|
|
17887
18152
|
};
|
|
17888
|
-
const ftyp = layouts[0].ftyp;
|
|
17889
|
-
const dataStart = ftyp.length + buildMoov(0).length + 16;
|
|
17890
|
-
const mdat = Buffer.alloc(16);
|
|
17891
|
-
mdat.writeUInt32BE(1);
|
|
17892
|
-
mdat.write("mdat", 4, "latin1");
|
|
17893
|
-
mdat.writeBigUInt64BE(BigInt(16 + payloadBytes), 8);
|
|
17894
|
-
return Buffer.concat([ftyp, buildMoov(dataStart), mdat]);
|
|
17895
18153
|
}
|
|
17896
|
-
function
|
|
17897
|
-
|
|
17898
|
-
|
|
17899
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
return buf.subarray(0, bytesRead);
|
|
17903
|
-
} finally {
|
|
17904
|
-
await file.close();
|
|
18154
|
+
function assertRemoteSources(remoteSources, artifacts) {
|
|
18155
|
+
if (!isRecord3(remoteSources)) throw new PlanV2IntegrityError("remoteSources must be an object");
|
|
18156
|
+
for (const [path, url] of Object.entries(remoteSources)) {
|
|
18157
|
+
const artifact = artifacts.find((candidate) => candidate.path === path);
|
|
18158
|
+
if (!artifact || !isPlanV2DeferredSourceArtifact(artifact)) {
|
|
18159
|
+
throw new PlanV2IntegrityError(`remote source is not a selected deferred video: ${path}`);
|
|
17905
18160
|
}
|
|
17906
|
-
|
|
18161
|
+
if (typeof url !== "string" || !/^https?:\/\//.test(url)) {
|
|
18162
|
+
throw new PlanV2IntegrityError(`remote source for ${path} must be an http(s) URL`);
|
|
18163
|
+
}
|
|
18164
|
+
}
|
|
17907
18165
|
}
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
18166
|
+
function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
18167
|
+
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
18168
|
+
}
|
|
18169
|
+
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
18170
|
+
const markerPath = join40(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
18171
|
+
if (!existsSync26(markerPath)) return null;
|
|
18172
|
+
const marker = readJsonFile(markerPath, "materialization marker");
|
|
18173
|
+
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
18174
|
+
throw new PlanV2IntegrityError("malformed materialization marker");
|
|
18175
|
+
}
|
|
18176
|
+
const target = marker.target;
|
|
18177
|
+
if (target.role !== expectedTarget.role || expectedTarget.role === "chunk" && target.chunkIndex !== expectedTarget.chunkIndex) {
|
|
18178
|
+
throw new PlanV2IntegrityError(
|
|
18179
|
+
"materialization target does not match requested execution role"
|
|
17915
18180
|
);
|
|
17916
18181
|
}
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
const
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
|
|
17923
|
-
|
|
17924
|
-
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
rmSync14(workDir, { recursive: true, force: true });
|
|
18182
|
+
const manifest = parsePlanV2Manifest(marker.manifest);
|
|
18183
|
+
const required = listPlanV2ArtifactsForTarget(manifest, expectedTarget);
|
|
18184
|
+
const remoteSources = marker.remoteSources ?? {};
|
|
18185
|
+
assertRemoteSources(remoteSources, required);
|
|
18186
|
+
for (const artifact of required) {
|
|
18187
|
+
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
18188
|
+
const path = join40(planDir, ...artifact.path.split("/"));
|
|
18189
|
+
if (!existsSync26(path) || statSync10(path).size !== artifact.sizeBytes) {
|
|
18190
|
+
throw new PlanV2IntegrityError(
|
|
18191
|
+
`materialized artifact missing or truncated: ${artifact.path}`
|
|
18192
|
+
);
|
|
18193
|
+
}
|
|
18194
|
+
if (sha256File(path) !== artifact.sha256) {
|
|
18195
|
+
throw new PlanV2IntegrityError(`materialized artifact hash mismatch: ${artifact.path}`);
|
|
18196
|
+
}
|
|
17933
18197
|
}
|
|
18198
|
+
return { manifest, remoteSources };
|
|
17934
18199
|
}
|
|
17935
18200
|
|
|
18201
|
+
// src/services/distributed/planV2Execution.ts
|
|
18202
|
+
import { mkdtempSync as mkdtempSync9, rmSync as rmSync17 } from "node:fs";
|
|
18203
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
18204
|
+
import { join as join43 } from "node:path";
|
|
18205
|
+
|
|
17936
18206
|
// src/services/distributed/assemble.ts
|
|
18207
|
+
import {
|
|
18208
|
+
cpSync as cpSync3,
|
|
18209
|
+
existsSync as existsSync27,
|
|
18210
|
+
mkdirSync as mkdirSync17,
|
|
18211
|
+
mkdtempSync as mkdtempSync8,
|
|
18212
|
+
readFileSync as readFileSync14,
|
|
18213
|
+
readdirSync as readdirSync10,
|
|
18214
|
+
renameSync as renameSync9,
|
|
18215
|
+
rmSync as rmSync15,
|
|
18216
|
+
statSync as statSync11,
|
|
18217
|
+
writeFileSync as writeFileSync12
|
|
18218
|
+
} from "node:fs";
|
|
18219
|
+
import { dirname as dirname17, join as join41 } from "node:path";
|
|
18220
|
+
import {
|
|
18221
|
+
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
18222
|
+
describeFfmpegFailure as describeFfmpegFailure3,
|
|
18223
|
+
getEncoderPreset as getEncoderPreset5,
|
|
18224
|
+
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
18225
|
+
muxVideoWithAudio as muxVideoWithAudio2,
|
|
18226
|
+
resolveConfig as resolveConfig6,
|
|
18227
|
+
runFfmpeg as runFfmpeg6
|
|
18228
|
+
} from "@hyperframes/engine";
|
|
18229
|
+
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
17937
18230
|
function cfrRateControlArgs(locked) {
|
|
17938
18231
|
const preset = getEncoderPreset5(locked.quality ?? "standard", "mp4");
|
|
17939
18232
|
if (locked.crf == null && locked.bitrate) {
|
|
@@ -17960,8 +18253,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17960
18253
|
}
|
|
17961
18254
|
const format = plan2.dimensions.format;
|
|
17962
18255
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
17963
|
-
if (!
|
|
17964
|
-
|
|
18256
|
+
if (!existsSync27(dirname17(outputPath))) {
|
|
18257
|
+
mkdirSync17(dirname17(outputPath), { recursive: true });
|
|
17965
18258
|
}
|
|
17966
18259
|
const workDir = mkdtempSync8(`${outputPath}.assemble-work-`);
|
|
17967
18260
|
const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
@@ -17983,8 +18276,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17983
18276
|
};
|
|
17984
18277
|
try {
|
|
17985
18278
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
17986
|
-
const finalPath =
|
|
17987
|
-
const audio = audioPath !== null &&
|
|
18279
|
+
const finalPath = join41(workDir, `output.${format}`);
|
|
18280
|
+
const audio = audioPath !== null && existsSync27(audioPath) ? audioPath : null;
|
|
17988
18281
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
17989
18282
|
renameSync9(finalPath, outputPath);
|
|
17990
18283
|
log.info("[assemble] stage timings", {
|
|
@@ -18002,7 +18295,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
18002
18295
|
});
|
|
18003
18296
|
}
|
|
18004
18297
|
}
|
|
18005
|
-
const fileSize =
|
|
18298
|
+
const fileSize = existsSync27(outputPath) ? statSync11(outputPath).size : 0;
|
|
18006
18299
|
return {
|
|
18007
18300
|
outputPath,
|
|
18008
18301
|
durationMs: Date.now() - start,
|
|
@@ -18011,25 +18304,25 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
18011
18304
|
};
|
|
18012
18305
|
}
|
|
18013
18306
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
18014
|
-
const planJsonPath =
|
|
18015
|
-
const chunksJsonPath =
|
|
18016
|
-
if (!
|
|
18307
|
+
const planJsonPath = join41(planDir, "plan.json");
|
|
18308
|
+
const chunksJsonPath = join41(planDir, "meta", "chunks.json");
|
|
18309
|
+
if (!existsSync27(planJsonPath)) {
|
|
18017
18310
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
18018
18311
|
}
|
|
18019
|
-
const plan2 = JSON.parse(
|
|
18312
|
+
const plan2 = JSON.parse(readFileSync14(planJsonPath, "utf-8"));
|
|
18020
18313
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
|
|
18021
18314
|
validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
|
|
18022
|
-
if (!
|
|
18315
|
+
if (!existsSync27(chunksJsonPath)) {
|
|
18023
18316
|
throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
|
|
18024
18317
|
}
|
|
18025
|
-
const chunks = JSON.parse(
|
|
18318
|
+
const chunks = JSON.parse(readFileSync14(chunksJsonPath, "utf-8"));
|
|
18026
18319
|
if (chunkPaths.length !== chunks.length) {
|
|
18027
18320
|
throw new Error(
|
|
18028
18321
|
`[assemble] chunkPaths length (${chunkPaths.length}) does not match chunks.json length (${chunks.length}). Adapters must pass one path per chunk, ordered by index.`
|
|
18029
18322
|
);
|
|
18030
18323
|
}
|
|
18031
18324
|
for (const path of chunkPaths) {
|
|
18032
|
-
if (!
|
|
18325
|
+
if (!existsSync27(path)) {
|
|
18033
18326
|
throw new Error(`[assemble] chunk path does not exist: ${path}`);
|
|
18034
18327
|
}
|
|
18035
18328
|
}
|
|
@@ -18039,7 +18332,7 @@ function videoInputArgs(ctx, videoSources) {
|
|
|
18039
18332
|
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
18040
18333
|
}
|
|
18041
18334
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
18042
|
-
const cfrOutputPath =
|
|
18335
|
+
const cfrOutputPath = join41(ctx.workDir, `cfr.${ctx.format}`);
|
|
18043
18336
|
const cfrArgs = [
|
|
18044
18337
|
...videoInputArgs(ctx, chunkPaths),
|
|
18045
18338
|
"-c:v",
|
|
@@ -18052,7 +18345,7 @@ async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
|
18052
18345
|
"-r",
|
|
18053
18346
|
ctx.fpsArg
|
|
18054
18347
|
];
|
|
18055
|
-
|
|
18348
|
+
appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
|
|
18056
18349
|
cfrArgs.push("-y", cfrOutputPath);
|
|
18057
18350
|
await ctx.timed(
|
|
18058
18351
|
"cfrMs",
|
|
@@ -18069,7 +18362,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
18069
18362
|
if (audioPath === null) {
|
|
18070
18363
|
const joinArgs = [...videoInputArgs(ctx, videoSources), "-c", "copy"];
|
|
18071
18364
|
if (ctx.format === "mp4") joinArgs.push("-movflags", "+faststart");
|
|
18072
|
-
|
|
18365
|
+
appendRenderProvenanceArgs2(joinArgs, finalPath);
|
|
18073
18366
|
joinArgs.push("-r", ctx.fpsArg, "-y", finalPath);
|
|
18074
18367
|
await ctx.timed(
|
|
18075
18368
|
"joinMs",
|
|
@@ -18077,7 +18370,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
18077
18370
|
);
|
|
18078
18371
|
return;
|
|
18079
18372
|
}
|
|
18080
|
-
const paddedAudioPath =
|
|
18373
|
+
const paddedAudioPath = join41(ctx.workDir, "audio-padded.m4a");
|
|
18081
18374
|
const padTrimResult = await ctx.timed(
|
|
18082
18375
|
"audioMs",
|
|
18083
18376
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -18117,9 +18410,9 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
18117
18410
|
}
|
|
18118
18411
|
}
|
|
18119
18412
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
18120
|
-
const listPath =
|
|
18413
|
+
const listPath = join41(workDir, "concat-list.txt");
|
|
18121
18414
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
18122
|
-
|
|
18415
|
+
writeFileSync12(listPath, `${body}
|
|
18123
18416
|
`, "utf-8");
|
|
18124
18417
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
18125
18418
|
}
|
|
@@ -18134,10 +18427,10 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
18134
18427
|
};
|
|
18135
18428
|
}
|
|
18136
18429
|
async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
18137
|
-
const result = await
|
|
18430
|
+
const result = await runFfmpeg6(args, { signal, timeout });
|
|
18138
18431
|
if (!result.success) {
|
|
18139
18432
|
throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
|
|
18140
|
-
error:
|
|
18433
|
+
error: describeFfmpegFailure3(result, timeout),
|
|
18141
18434
|
failureReason: result.failureReason
|
|
18142
18435
|
});
|
|
18143
18436
|
}
|
|
@@ -18150,9 +18443,9 @@ function chunksAreStitchedMp4(planDir, cfr) {
|
|
|
18150
18443
|
return true;
|
|
18151
18444
|
}
|
|
18152
18445
|
function chunksCarryAudio(planDir) {
|
|
18153
|
-
const encoderJsonPath =
|
|
18154
|
-
if (!
|
|
18155
|
-
return JSON.parse(
|
|
18446
|
+
const encoderJsonPath = join41(planDir, "meta", "encoder.json");
|
|
18447
|
+
if (!existsSync27(encoderJsonPath)) return false;
|
|
18448
|
+
return JSON.parse(readFileSync14(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
|
|
18156
18449
|
}
|
|
18157
18450
|
function readCfrEncoder(planDir, format) {
|
|
18158
18451
|
if (format !== "mp4") {
|
|
@@ -18160,11 +18453,11 @@ function readCfrEncoder(planDir, format) {
|
|
|
18160
18453
|
`[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.`
|
|
18161
18454
|
);
|
|
18162
18455
|
}
|
|
18163
|
-
const encoderJsonPath =
|
|
18164
|
-
if (!
|
|
18456
|
+
const encoderJsonPath = join41(planDir, "meta", "encoder.json");
|
|
18457
|
+
if (!existsSync27(encoderJsonPath)) {
|
|
18165
18458
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
18166
18459
|
}
|
|
18167
|
-
const encoderJson = JSON.parse(
|
|
18460
|
+
const encoderJson = JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
|
|
18168
18461
|
if (encoderJson.encoder === "libx265-software") {
|
|
18169
18462
|
throw new Error(
|
|
18170
18463
|
`[assemble] cfr=true is not yet supported with codec: "h265". The cfr re-encode pass uses libx264 and would silently transcode the h265 chunks. Either disable cfr or render with codec: "h264".`
|
|
@@ -18173,8 +18466,8 @@ function readCfrEncoder(planDir, format) {
|
|
|
18173
18466
|
return encoderJson;
|
|
18174
18467
|
}
|
|
18175
18468
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
18176
|
-
if (
|
|
18177
|
-
|
|
18469
|
+
if (existsSync27(outputPath)) rmSync15(outputPath, { recursive: true, force: true });
|
|
18470
|
+
mkdirSync17(outputPath, { recursive: true });
|
|
18178
18471
|
let globalIdx = 0;
|
|
18179
18472
|
for (const chunkDir of chunkPaths) {
|
|
18180
18473
|
if (!statSync11(chunkDir).isDirectory()) {
|
|
@@ -18187,8 +18480,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
18187
18480
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
18188
18481
|
}
|
|
18189
18482
|
for (const frame of frames) {
|
|
18190
|
-
const dst =
|
|
18191
|
-
cpSync3(
|
|
18483
|
+
const dst = join41(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
18484
|
+
cpSync3(join41(chunkDir, frame), dst);
|
|
18192
18485
|
globalIdx += 1;
|
|
18193
18486
|
}
|
|
18194
18487
|
}
|
|
@@ -18197,14 +18490,14 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
18197
18490
|
`[assemble] png-sequence frame count mismatch: merged ${globalIdx} frames vs plan.totalFrames=${totalFrames}. Using on-disk count.`
|
|
18198
18491
|
);
|
|
18199
18492
|
}
|
|
18200
|
-
if (audioPath !== null &&
|
|
18201
|
-
const sidecar =
|
|
18493
|
+
if (audioPath !== null && existsSync27(audioPath)) {
|
|
18494
|
+
const sidecar = join41(outputPath, MIXED_AUDIO_FILENAME4);
|
|
18202
18495
|
cpSync3(audioPath, sidecar);
|
|
18203
18496
|
}
|
|
18204
18497
|
let fileSize = 0;
|
|
18205
18498
|
for (const name of readdirSync10(outputPath)) {
|
|
18206
18499
|
try {
|
|
18207
|
-
fileSize += statSync11(
|
|
18500
|
+
fileSize += statSync11(join41(outputPath, name)).size;
|
|
18208
18501
|
} catch {
|
|
18209
18502
|
}
|
|
18210
18503
|
}
|
|
@@ -18218,8 +18511,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
18218
18511
|
|
|
18219
18512
|
// src/services/distributed/renderChunk.ts
|
|
18220
18513
|
import { randomBytes } from "node:crypto";
|
|
18221
|
-
import { existsSync as
|
|
18222
|
-
import { join as
|
|
18514
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync16, writeFileSync as writeFileSync13 } from "node:fs";
|
|
18515
|
+
import { join as join42 } from "node:path";
|
|
18223
18516
|
import {
|
|
18224
18517
|
assertSwiftShader,
|
|
18225
18518
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -18235,104 +18528,6 @@ import {
|
|
|
18235
18528
|
readWebGlVendorInfoFromCanvas,
|
|
18236
18529
|
resolveConfig as resolveConfig7
|
|
18237
18530
|
} from "@hyperframes/engine";
|
|
18238
|
-
|
|
18239
|
-
// src/services/distributed/chunkAudio.ts
|
|
18240
|
-
import { appendRenderProvenanceArgs as appendRenderProvenanceArgs2, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
18241
|
-
function audioCutForChunk(grid, chunks, index, fps) {
|
|
18242
|
-
const firstPacketAt = (frame) => Math.min(
|
|
18243
|
-
grid.packetCount,
|
|
18244
|
-
Math.max(
|
|
18245
|
-
0,
|
|
18246
|
-
Math.ceil(
|
|
18247
|
-
(frame * fps.den * grid.sampleRate - grid.firstStart * fps.num) / (grid.packetSamples * fps.num)
|
|
18248
|
-
)
|
|
18249
|
-
)
|
|
18250
|
-
);
|
|
18251
|
-
const first = index === 0 ? 0 : firstPacketAt(chunks[index].startFrame);
|
|
18252
|
-
const end = index === chunks.length - 1 ? grid.packetCount : firstPacketAt(chunks[index + 1].startFrame);
|
|
18253
|
-
if (end <= first) return null;
|
|
18254
|
-
const cutSeconds = (packet) => (grid.firstStart + packet * grid.packetSamples - grid.packetSamples / 2) / grid.sampleRate;
|
|
18255
|
-
const seekSeconds = index === 0 ? null : cutSeconds(first);
|
|
18256
|
-
const durationSeconds = end === grid.packetCount ? null : cutSeconds(end) - (seekSeconds ?? 0);
|
|
18257
|
-
return { seekSeconds, durationSeconds, firstPacket: first, packetCount: end - first };
|
|
18258
|
-
}
|
|
18259
|
-
async function probeAacPacketGrid(audioPath) {
|
|
18260
|
-
const probe = await runFfprobeJson([
|
|
18261
|
-
"-v",
|
|
18262
|
-
"error",
|
|
18263
|
-
"-select_streams",
|
|
18264
|
-
"a:0",
|
|
18265
|
-
"-show_entries",
|
|
18266
|
-
"stream=sample_rate,time_base,nb_frames:packet=pts,duration",
|
|
18267
|
-
"-read_intervals",
|
|
18268
|
-
"%+#1",
|
|
18269
|
-
"-of",
|
|
18270
|
-
"json",
|
|
18271
|
-
"--",
|
|
18272
|
-
audioPath
|
|
18273
|
-
]);
|
|
18274
|
-
const stream = probe.streams?.[0] ?? {};
|
|
18275
|
-
const packet = probe.packets?.[0] ?? {};
|
|
18276
|
-
const sampleRate = Number(stream.sample_rate);
|
|
18277
|
-
const [tbNum = NaN, tbDen = NaN] = String(stream.time_base).split("/").map(Number);
|
|
18278
|
-
const toSamples = (ticks) => Number(ticks) * sampleRate * tbNum / tbDen;
|
|
18279
|
-
const grid = {
|
|
18280
|
-
sampleRate,
|
|
18281
|
-
firstStart: toSamples(packet.pts),
|
|
18282
|
-
packetSamples: toSamples(packet.duration),
|
|
18283
|
-
packetCount: Number(stream.nb_frames)
|
|
18284
|
-
};
|
|
18285
|
-
const counts = [grid.sampleRate, grid.packetSamples, grid.packetCount];
|
|
18286
|
-
if (!Number.isFinite(grid.firstStart) || !counts.every((n) => Number.isFinite(n) && n > 0)) {
|
|
18287
|
-
throw new Error(`[chunkAudio] could not read the AAC packet grid: ${JSON.stringify(probe)}`);
|
|
18288
|
-
}
|
|
18289
|
-
return grid;
|
|
18290
|
-
}
|
|
18291
|
-
function chunkMuxArgs(input, cut) {
|
|
18292
|
-
if (!cut) return ["-i", input.videoPath, "-map", "0:v:0"];
|
|
18293
|
-
const seek = cut.seekSeconds === null ? [] : ["-ss", cut.seekSeconds.toFixed(6)];
|
|
18294
|
-
const length = cut.durationSeconds === null ? [] : ["-t", cut.durationSeconds.toFixed(6)];
|
|
18295
|
-
const exact = seek.length ? ["-copypriorss", "0"] : [];
|
|
18296
|
-
return [
|
|
18297
|
-
...["-i", input.videoPath, ...seek, ...length, "-i", input.audioPath],
|
|
18298
|
-
...["-map", "0:v:0", "-map", "1:a:0", ...exact]
|
|
18299
|
-
];
|
|
18300
|
-
}
|
|
18301
|
-
async function muxChunkWithAudio(input) {
|
|
18302
|
-
const grid = input.audioPath ? await probeAacPacketGrid(input.audioPath) : null;
|
|
18303
|
-
const cut = grid ? audioCutForChunk(grid, input.chunks, input.chunkIndex, input.fps) : null;
|
|
18304
|
-
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
18305
|
-
appendRenderProvenanceArgs2(args, input.outputPath);
|
|
18306
|
-
args.push("-y", input.outputPath);
|
|
18307
|
-
const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
18308
|
-
if (!result.success) {
|
|
18309
|
-
throw new Error(
|
|
18310
|
-
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
|
|
18311
|
-
);
|
|
18312
|
-
}
|
|
18313
|
-
const slice = input.chunks[input.chunkIndex];
|
|
18314
|
-
const expected = { video: slice.endFrame - slice.startFrame };
|
|
18315
|
-
if (cut) expected.audio = cut.packetCount;
|
|
18316
|
-
const probe = await runFfprobeJson([
|
|
18317
|
-
"-v",
|
|
18318
|
-
"error",
|
|
18319
|
-
"-count_packets",
|
|
18320
|
-
"-show_entries",
|
|
18321
|
-
"stream=codec_type,nb_read_packets",
|
|
18322
|
-
"-of",
|
|
18323
|
-
"json",
|
|
18324
|
-
"--",
|
|
18325
|
-
input.outputPath
|
|
18326
|
-
]);
|
|
18327
|
-
for (const [type, count] of Object.entries(expected)) {
|
|
18328
|
-
const packets = Number(probe.streams?.find((s) => s.codec_type === type)?.nb_read_packets ?? 0);
|
|
18329
|
-
if (packets !== count) {
|
|
18330
|
-
throw new Error(`[chunkAudio] ${type} packets in chunk ${packets}, expected ${count}`);
|
|
18331
|
-
}
|
|
18332
|
-
}
|
|
18333
|
-
}
|
|
18334
|
-
|
|
18335
|
-
// src/services/distributed/renderChunk.ts
|
|
18336
18531
|
import { readWebGlVendorInfoFromCanvas as readWebGlVendorInfoFromCanvas2 } from "@hyperframes/engine";
|
|
18337
18532
|
var LEGACY_DISTRIBUTED_VP9_CPU_USED = 2;
|
|
18338
18533
|
function validatePlanVideosForChunk(value) {
|
|
@@ -18398,10 +18593,10 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
|
|
|
18398
18593
|
return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
|
|
18399
18594
|
}
|
|
18400
18595
|
function hashChunkOutput(outputPath, kind) {
|
|
18401
|
-
if (kind === "file") return sha256Hex(
|
|
18596
|
+
if (kind === "file") return sha256Hex(readFileSync15(outputPath));
|
|
18402
18597
|
const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
18403
18598
|
const lines = entries.map(
|
|
18404
|
-
(name) => `${name}\0${sha256Hex(
|
|
18599
|
+
(name) => `${name}\0${sha256Hex(readFileSync15(join42(outputPath, name)))}`
|
|
18405
18600
|
);
|
|
18406
18601
|
return sha256Hex(lines.join("\0"));
|
|
18407
18602
|
}
|
|
@@ -18418,37 +18613,39 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
18418
18613
|
async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
18419
18614
|
const start = Date.now();
|
|
18420
18615
|
const log = defaultLogger;
|
|
18421
|
-
const planJsonPath =
|
|
18422
|
-
const encoderJsonPath =
|
|
18423
|
-
const chunksJsonPath =
|
|
18424
|
-
if (!
|
|
18616
|
+
const planJsonPath = join42(planDir, "plan.json");
|
|
18617
|
+
const encoderJsonPath = join42(planDir, "meta", "encoder.json");
|
|
18618
|
+
const chunksJsonPath = join42(planDir, "meta", "chunks.json");
|
|
18619
|
+
if (!existsSync28(planJsonPath)) {
|
|
18425
18620
|
throw new RenderChunkValidationError(
|
|
18426
18621
|
MISSING_PLAN_ARTIFACT,
|
|
18427
18622
|
`[renderChunk] planDir is missing required artifact: ${planJsonPath}`
|
|
18428
18623
|
);
|
|
18429
18624
|
}
|
|
18430
|
-
const plan2 = JSON.parse(
|
|
18625
|
+
const plan2 = JSON.parse(readFileSync15(planJsonPath, "utf-8"));
|
|
18431
18626
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk);
|
|
18432
18627
|
const planHashStarted = Date.now();
|
|
18433
|
-
const
|
|
18628
|
+
const v2Materialization = readValidatedPlanV2Materialization(planDir, {
|
|
18434
18629
|
role: "chunk",
|
|
18435
18630
|
chunkIndex
|
|
18436
18631
|
});
|
|
18632
|
+
const v2Manifest = v2Materialization?.manifest ?? null;
|
|
18633
|
+
const remoteSources = v2Materialization?.remoteSources ?? {};
|
|
18437
18634
|
for (const required of [encoderJsonPath, chunksJsonPath]) {
|
|
18438
|
-
if (!
|
|
18635
|
+
if (!existsSync28(required)) {
|
|
18439
18636
|
throw new RenderChunkValidationError(
|
|
18440
18637
|
MISSING_PLAN_ARTIFACT,
|
|
18441
18638
|
`[renderChunk] planDir is missing required artifact: ${required}`
|
|
18442
18639
|
);
|
|
18443
18640
|
}
|
|
18444
18641
|
}
|
|
18445
|
-
const encoder = JSON.parse(
|
|
18446
|
-
const chunks = JSON.parse(
|
|
18447
|
-
const videosJsonPath =
|
|
18642
|
+
const encoder = JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
|
|
18643
|
+
const chunks = JSON.parse(readFileSync15(chunksJsonPath, "utf-8"));
|
|
18644
|
+
const videosJsonPath = join42(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18448
18645
|
let planVideos = null;
|
|
18449
|
-
if (
|
|
18646
|
+
if (existsSync28(videosJsonPath)) {
|
|
18450
18647
|
try {
|
|
18451
|
-
planVideos = validatePlanVideosForChunk(JSON.parse(
|
|
18648
|
+
planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync15(videosJsonPath, "utf-8")));
|
|
18452
18649
|
} catch (err) {
|
|
18453
18650
|
if (err instanceof RenderChunkValidationError) throw err;
|
|
18454
18651
|
throw new RenderChunkValidationError(
|
|
@@ -18474,8 +18671,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18474
18671
|
`[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
|
|
18475
18672
|
);
|
|
18476
18673
|
}
|
|
18477
|
-
const compiledDir =
|
|
18478
|
-
if (!
|
|
18674
|
+
const compiledDir = join42(planDir, "compiled");
|
|
18675
|
+
if (!existsSync28(compiledDir)) {
|
|
18479
18676
|
throw new RenderChunkValidationError(
|
|
18480
18677
|
MISSING_PLAN_ARTIFACT,
|
|
18481
18678
|
`[renderChunk] planDir missing compiled/ directory: ${compiledDir}`
|
|
@@ -18531,10 +18728,28 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18531
18728
|
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
|
|
18532
18729
|
};
|
|
18533
18730
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
18534
|
-
|
|
18535
|
-
const framesDir =
|
|
18536
|
-
|
|
18731
|
+
mkdirSync18(workDir, { recursive: true });
|
|
18732
|
+
const framesDir = join42(workDir, "captured-frames");
|
|
18733
|
+
mkdirSync18(framesDir, { recursive: true });
|
|
18537
18734
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
18735
|
+
const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
|
|
18736
|
+
planDir,
|
|
18737
|
+
planVideos,
|
|
18738
|
+
frames: Array.from(
|
|
18739
|
+
{ length: slice.endFrame - slice.startFrame },
|
|
18740
|
+
(_, i) => slice.startFrame + i
|
|
18741
|
+
),
|
|
18742
|
+
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
18743
|
+
outputRoot: videoFramesDir,
|
|
18744
|
+
cfg,
|
|
18745
|
+
remoteSources
|
|
18746
|
+
}) : [];
|
|
18747
|
+
const remoteVideoIds = new Set(
|
|
18748
|
+
planVideos?.extracted.filter(
|
|
18749
|
+
(video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
|
|
18750
|
+
).map((video) => video.videoId)
|
|
18751
|
+
);
|
|
18752
|
+
const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
|
|
18538
18753
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
18539
18754
|
planVideos.videos,
|
|
18540
18755
|
[
|
|
@@ -18543,17 +18758,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18543
18758
|
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
18544
18759
|
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
18545
18760
|
),
|
|
18546
|
-
...
|
|
18547
|
-
planDir,
|
|
18548
|
-
planVideos,
|
|
18549
|
-
frames: Array.from(
|
|
18550
|
-
{ length: slice.endFrame - slice.startFrame },
|
|
18551
|
-
(_, i) => slice.startFrame + i
|
|
18552
|
-
),
|
|
18553
|
-
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
18554
|
-
outputRoot: videoFramesDir,
|
|
18555
|
-
cfg
|
|
18556
|
-
})
|
|
18761
|
+
...deferredVideoFrames
|
|
18557
18762
|
],
|
|
18558
18763
|
resolveRenderFpsConfig(job.config.fps).value
|
|
18559
18764
|
) : null;
|
|
@@ -18571,7 +18776,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18571
18776
|
dimensions: plan2.dimensions,
|
|
18572
18777
|
encoder,
|
|
18573
18778
|
videoCount: planVideos?.videos.length ?? 0,
|
|
18574
|
-
html:
|
|
18779
|
+
html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
|
|
18780
|
+
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
18781
|
+
skipReadinessVideoIds: deferredVideoFrames.filter((video) => video.framePaths.size === 0 || remoteVideoIds.has(video.videoId)).map((video) => video.videoId),
|
|
18782
|
+
videoMetadataHints: collectVideoMetadataHints(remoteVideos)
|
|
18575
18783
|
});
|
|
18576
18784
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
18577
18785
|
let session = null;
|
|
@@ -18702,7 +18910,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18702
18910
|
},
|
|
18703
18911
|
resetForScreenshotRetry: () => {
|
|
18704
18912
|
rmSync16(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
18705
|
-
|
|
18913
|
+
mkdirSync18(framesDir, { recursive: true });
|
|
18706
18914
|
capturePerfs.length = 0;
|
|
18707
18915
|
captureAttempts.length = 0;
|
|
18708
18916
|
job.framesRendered = 0;
|
|
@@ -18733,10 +18941,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18733
18941
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
18734
18942
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
18735
18943
|
if (isPngSequence) {
|
|
18736
|
-
if (!
|
|
18944
|
+
if (!existsSync28(outputChunkPath)) mkdirSync18(outputChunkPath, { recursive: true });
|
|
18737
18945
|
} else {
|
|
18738
|
-
const outDir =
|
|
18739
|
-
if (!
|
|
18946
|
+
const outDir = join42(outputChunkPath, "..");
|
|
18947
|
+
if (!existsSync28(outDir)) mkdirSync18(outDir, { recursive: true });
|
|
18740
18948
|
}
|
|
18741
18949
|
const encodeStarted = Date.now();
|
|
18742
18950
|
await runEncodeStage({
|
|
@@ -18779,9 +18987,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18779
18987
|
gopSize: framesInChunk
|
|
18780
18988
|
});
|
|
18781
18989
|
if (encoder.chunksCarryAudio) {
|
|
18990
|
+
const audioScratchPath = `${outputChunkPath}.audio.m4a`;
|
|
18782
18991
|
await muxChunkWithAudio({
|
|
18783
18992
|
videoPath: videoOnlyPath,
|
|
18784
|
-
audioPath:
|
|
18993
|
+
audioPath: chunkAudioSource(planDir, chunkIndex, audioScratchPath),
|
|
18785
18994
|
outputPath: outputChunkPath,
|
|
18786
18995
|
chunks,
|
|
18787
18996
|
chunkIndex,
|
|
@@ -18789,6 +18998,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18789
18998
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
18790
18999
|
});
|
|
18791
19000
|
rmSync16(videoOnlyPath, { force: true });
|
|
19001
|
+
rmSync16(audioScratchPath, { force: true });
|
|
18792
19002
|
}
|
|
18793
19003
|
encodeStageMs = Date.now() - encodeStarted;
|
|
18794
19004
|
} finally {
|
|
@@ -18827,7 +19037,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18827
19037
|
producerVersion: plan2.producerVersion,
|
|
18828
19038
|
ffmpegVersion
|
|
18829
19039
|
};
|
|
18830
|
-
|
|
19040
|
+
writeFileSync13(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
18831
19041
|
`, "utf-8");
|
|
18832
19042
|
try {
|
|
18833
19043
|
rmSync16(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
@@ -18859,8 +19069,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18859
19069
|
|
|
18860
19070
|
// src/services/distributed/planV2Execution.ts
|
|
18861
19071
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
|
|
18862
|
-
const workRoot = mkdtempSync9(
|
|
18863
|
-
const materializedPlanDir =
|
|
19072
|
+
const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-chunk-"));
|
|
19073
|
+
const materializedPlanDir = join43(workRoot, "plan");
|
|
18864
19074
|
try {
|
|
18865
19075
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
18866
19076
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
|
|
@@ -18869,8 +19079,8 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
|
|
|
18869
19079
|
}
|
|
18870
19080
|
}
|
|
18871
19081
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
18872
|
-
const workRoot = mkdtempSync9(
|
|
18873
|
-
const materializedPlanDir =
|
|
19082
|
+
const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-assembler-"));
|
|
19083
|
+
const materializedPlanDir = join43(workRoot, "plan");
|
|
18874
19084
|
try {
|
|
18875
19085
|
const materialized = materializePlanV2Target(
|
|
18876
19086
|
planV2Dir,
|