@hyperframes/producer 0.8.175 → 0.8.176
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.js +1109 -932
- package/dist/index.js +1052 -950
- package/dist/public-server.js +30 -11
- package/dist/services/distributed/chunkFrames.d.ts +48 -0
- package/dist/services/distributed/plan.d.ts +8 -2
- package/dist/services/distributed/planFrameCost.d.ts +24 -0
- package/dist/services/distributed/renderChunk.d.ts +4 -69
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -3,15 +3,18 @@ import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframe
|
|
|
3
3
|
import {
|
|
4
4
|
copyFileSync as copyFileSync5,
|
|
5
5
|
cpSync as cpSync2,
|
|
6
|
-
existsSync as
|
|
6
|
+
existsSync as existsSync14,
|
|
7
7
|
linkSync,
|
|
8
8
|
mkdirSync as mkdirSync8,
|
|
9
9
|
renameSync as renameSync3,
|
|
10
10
|
rmSync as rmSync7,
|
|
11
11
|
writeFileSync as writeFileSync5
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { extname as
|
|
14
|
-
import {
|
|
13
|
+
import { extname as extname4, isAbsolute as isAbsolute3, join as join20, relative as relative5, sep as sep2 } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
fpsToNumber as fpsToNumber10,
|
|
16
|
+
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
17
|
+
} from "@hyperframes/core";
|
|
15
18
|
import {
|
|
16
19
|
getEncoderPreset as getEncoderPreset4,
|
|
17
20
|
normalizeVp9CpuUsed,
|
|
@@ -1340,6 +1343,81 @@ import {
|
|
|
1340
1343
|
createCaptureSession as createCaptureSession3,
|
|
1341
1344
|
initializeSession as initializeSession3
|
|
1342
1345
|
} from "@hyperframes/engine";
|
|
1346
|
+
var CAPTURE_CALIBRATION_TARGET_MS = 600;
|
|
1347
|
+
var MAX_MEASURED_CAPTURE_COST_MULTIPLIER = 8;
|
|
1348
|
+
var CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS = 3e4;
|
|
1349
|
+
function createCaptureCalibrationConfig(cfg) {
|
|
1350
|
+
return {
|
|
1351
|
+
...cfg,
|
|
1352
|
+
protocolTimeout: Math.min(cfg.protocolTimeout, CAPTURE_CALIBRATION_PROTOCOL_TIMEOUT_MS)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function estimateMeasuredCaptureCostMultiplier(samples) {
|
|
1356
|
+
if (samples.length === 0) {
|
|
1357
|
+
return { multiplier: 1, reasons: [] };
|
|
1358
|
+
}
|
|
1359
|
+
const sorted = [...samples].sort((a, b) => a.captureTimeMs - b.captureTimeMs);
|
|
1360
|
+
const p95Index = Math.max(0, Math.ceil(sorted.length * 0.95) - 1);
|
|
1361
|
+
const p95Sample = sorted[p95Index] ?? sorted[sorted.length - 1];
|
|
1362
|
+
if (!p95Sample) {
|
|
1363
|
+
return { multiplier: 1, reasons: [] };
|
|
1364
|
+
}
|
|
1365
|
+
const p95Ms = Math.round(p95Sample.captureTimeMs);
|
|
1366
|
+
const multiplier = Math.min(
|
|
1367
|
+
MAX_MEASURED_CAPTURE_COST_MULTIPLIER,
|
|
1368
|
+
Math.max(1, Math.round(p95Ms / CAPTURE_CALIBRATION_TARGET_MS * 100) / 100)
|
|
1369
|
+
);
|
|
1370
|
+
return {
|
|
1371
|
+
multiplier,
|
|
1372
|
+
reasons: multiplier > 1 ? [`calibration-p95=${p95Ms}ms`] : [],
|
|
1373
|
+
p95Ms
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function selectCaptureCalibrationFrames(totalFrames) {
|
|
1377
|
+
if (totalFrames <= 0) return [];
|
|
1378
|
+
const lastFrame = totalFrames - 1;
|
|
1379
|
+
const candidates = [
|
|
1380
|
+
0,
|
|
1381
|
+
Math.floor(totalFrames * 0.25),
|
|
1382
|
+
Math.floor(totalFrames * 0.5),
|
|
1383
|
+
Math.floor(totalFrames * 0.75),
|
|
1384
|
+
lastFrame
|
|
1385
|
+
];
|
|
1386
|
+
return Array.from(
|
|
1387
|
+
new Set(candidates.map((frame) => Math.max(0, Math.min(lastFrame, frame))))
|
|
1388
|
+
).sort((a, b) => a - b);
|
|
1389
|
+
}
|
|
1390
|
+
async function measureCaptureCostFromSession(session, totalFrames, fps, log) {
|
|
1391
|
+
const sampledFrames = selectCaptureCalibrationFrames(totalFrames);
|
|
1392
|
+
const samples = [];
|
|
1393
|
+
const totalSamples = sampledFrames.length;
|
|
1394
|
+
const savedStaticFrames = session.staticFrames;
|
|
1395
|
+
session.staticFrames = void 0;
|
|
1396
|
+
try {
|
|
1397
|
+
for (let i = 0; i < sampledFrames.length; i++) {
|
|
1398
|
+
const frameIndex = sampledFrames[i];
|
|
1399
|
+
log?.info(`Calibration: capturing test frame ${i + 1}/${totalSamples}...`);
|
|
1400
|
+
const time = frameIndex / fps;
|
|
1401
|
+
const startedAt = Date.now();
|
|
1402
|
+
const result = await captureFrameToBuffer3(session, frameIndex, time);
|
|
1403
|
+
samples.push({
|
|
1404
|
+
frameIndex,
|
|
1405
|
+
captureTimeMs: result.captureTimeMs || Date.now() - startedAt
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
} finally {
|
|
1409
|
+
session.staticFrames = savedStaticFrames;
|
|
1410
|
+
session.lastFrameBuffer = void 0;
|
|
1411
|
+
}
|
|
1412
|
+
const estimate = estimateMeasuredCaptureCostMultiplier(samples);
|
|
1413
|
+
if (estimate.p95Ms !== void 0) {
|
|
1414
|
+
log?.info(`Calibration complete, estimated cost: ${estimate.p95Ms}ms/frame (p95)`);
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
estimate,
|
|
1418
|
+
samples
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1343
1421
|
|
|
1344
1422
|
// src/services/render/observability.ts
|
|
1345
1423
|
import { redactTelemetryString } from "@hyperframes/core";
|
|
@@ -6360,673 +6438,896 @@ function createRenderJob(config) {
|
|
|
6360
6438
|
}
|
|
6361
6439
|
var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
|
|
6362
6440
|
|
|
6363
|
-
// src/services/
|
|
6364
|
-
import {
|
|
6365
|
-
import {
|
|
6441
|
+
// src/services/distributed/planFrameCost.ts
|
|
6442
|
+
import { join as join17 } from "node:path";
|
|
6443
|
+
import {
|
|
6444
|
+
closeCaptureSession as closeCaptureSession9,
|
|
6445
|
+
createCaptureSession as createCaptureSession8,
|
|
6446
|
+
createFrameLookupTable as createFrameLookupTable3,
|
|
6447
|
+
initializeSession as initializeSession8
|
|
6448
|
+
} from "@hyperframes/engine";
|
|
6366
6449
|
|
|
6367
|
-
// src/services/distributed/
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
}
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
|
|
6393
|
-
acceptsLegacyV1WithoutDescriptor: true
|
|
6394
|
-
}),
|
|
6395
|
-
assembler: Object.freeze({
|
|
6396
|
-
accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
|
|
6397
|
-
acceptsLegacyV1WithoutDescriptor: true
|
|
6398
|
-
})
|
|
6399
|
-
})
|
|
6400
|
-
});
|
|
6401
|
-
function getDistributedRenderCapabilities() {
|
|
6402
|
-
return DISTRIBUTED_RENDER_CAPABILITIES;
|
|
6450
|
+
// src/services/distributed/chunkFrames.ts
|
|
6451
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "node:fs";
|
|
6452
|
+
import { extname as extname2, join as join16 } from "node:path";
|
|
6453
|
+
import {
|
|
6454
|
+
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
6455
|
+
createFrameLookupTable as createFrameLookupTable2,
|
|
6456
|
+
createVideoFrameInjector as createVideoFrameInjector2,
|
|
6457
|
+
extractVideoFramesRange
|
|
6458
|
+
} from "@hyperframes/engine";
|
|
6459
|
+
|
|
6460
|
+
// src/services/distributed/shared.ts
|
|
6461
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
6462
|
+
import { dirname as dirname8, join as join15 } from "node:path";
|
|
6463
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
|
|
6464
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6465
|
+
import { promisify as promisify2 } from "node:util";
|
|
6466
|
+
import {
|
|
6467
|
+
getFfmpegBinary,
|
|
6468
|
+
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME3
|
|
6469
|
+
} from "@hyperframes/engine";
|
|
6470
|
+
var PLAN_VIDEOS_META_RELATIVE_PATH = "meta/videos.json";
|
|
6471
|
+
var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
|
|
6472
|
+
var PLAN_AUDIO_LEGACY_RELATIVE_PATH = "audio.aac";
|
|
6473
|
+
function isPlanAudioArtifactPath(path) {
|
|
6474
|
+
return path === PLAN_AUDIO_RELATIVE_PATH || path === PLAN_AUDIO_LEGACY_RELATIVE_PATH;
|
|
6403
6475
|
}
|
|
6404
|
-
|
|
6405
|
-
|
|
6476
|
+
function resolvePlanAudioPath(planDir) {
|
|
6477
|
+
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
6478
|
+
const candidate = join15(planDir, name);
|
|
6479
|
+
if (existsSync11(candidate)) return candidate;
|
|
6480
|
+
}
|
|
6481
|
+
return null;
|
|
6482
|
+
}
|
|
6483
|
+
var INVALID_VIDEO_METADATA = "INVALID_VIDEO_METADATA";
|
|
6484
|
+
var PlanVideosMetadataError = class extends Error {
|
|
6485
|
+
// Read by cloud adapters across the package boundary to classify retries.
|
|
6406
6486
|
// fallow-ignore-next-line unused-class-member
|
|
6407
|
-
code =
|
|
6408
|
-
constructor(
|
|
6409
|
-
super(
|
|
6410
|
-
this.name = "
|
|
6487
|
+
code = INVALID_VIDEO_METADATA;
|
|
6488
|
+
constructor(message) {
|
|
6489
|
+
super(message);
|
|
6490
|
+
this.name = "PlanVideosMetadataError";
|
|
6411
6491
|
}
|
|
6412
6492
|
};
|
|
6413
|
-
function
|
|
6414
|
-
|
|
6415
|
-
}
|
|
6416
|
-
function protocolMatches(descriptor, expected) {
|
|
6417
|
-
return descriptor.schemaVersion === expected.schemaVersion && descriptor.artifactLayout === expected.artifactLayout && descriptor.hashSchema === expected.hashSchema;
|
|
6418
|
-
}
|
|
6419
|
-
function capabilitiesAccept(capabilities, protocol) {
|
|
6420
|
-
return capabilities.accepts.some(
|
|
6421
|
-
(accepted) => accepted.schemaVersion === protocol.schemaVersion && accepted.artifactLayout === protocol.artifactLayout && accepted.hashSchema === protocol.hashSchema
|
|
6422
|
-
);
|
|
6493
|
+
function metadataError(field, expectation) {
|
|
6494
|
+
throw new PlanVideosMetadataError(`${field} ${expectation}`);
|
|
6423
6495
|
}
|
|
6424
|
-
function
|
|
6425
|
-
if (
|
|
6426
|
-
|
|
6427
|
-
}
|
|
6428
|
-
if (!Object.prototype.hasOwnProperty.call(planJson, "protocol")) {
|
|
6429
|
-
if (!capabilities.acceptsLegacyV1WithoutDescriptor || !capabilitiesAccept(capabilities, PLAN_PROTOCOL_V1)) {
|
|
6430
|
-
throw new PlanProtocolUnsupportedError(
|
|
6431
|
-
"legacy v1 plan without a protocol descriptor is not accepted by this worker"
|
|
6432
|
-
);
|
|
6433
|
-
}
|
|
6434
|
-
return PLAN_PROTOCOL_V1;
|
|
6496
|
+
function readRecord(value, field) {
|
|
6497
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
6498
|
+
metadataError(field, "must be an object");
|
|
6435
6499
|
}
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6500
|
+
return value;
|
|
6501
|
+
}
|
|
6502
|
+
function readNonEmptyString(value, field) {
|
|
6503
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
6504
|
+
metadataError(field, "must be a non-empty string");
|
|
6439
6505
|
}
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6506
|
+
return value;
|
|
6507
|
+
}
|
|
6508
|
+
function readString(value, field) {
|
|
6509
|
+
if (typeof value !== "string") {
|
|
6510
|
+
metadataError(field, "must be a string");
|
|
6444
6511
|
}
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6512
|
+
return value;
|
|
6513
|
+
}
|
|
6514
|
+
function readFiniteNumber(value, field) {
|
|
6515
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
6516
|
+
metadataError(field, "must be a finite number");
|
|
6448
6517
|
}
|
|
6449
|
-
return
|
|
6518
|
+
return value;
|
|
6450
6519
|
}
|
|
6451
|
-
function
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
throw new PlanProtocolUnsupportedError(
|
|
6455
|
-
"content-addressed v2 plan must be materialized before v1 layout access"
|
|
6456
|
-
);
|
|
6520
|
+
function readBoolean(value, field) {
|
|
6521
|
+
if (typeof value !== "boolean") {
|
|
6522
|
+
metadataError(field, "must be boolean");
|
|
6457
6523
|
}
|
|
6458
|
-
return
|
|
6524
|
+
return value;
|
|
6459
6525
|
}
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
import { closeSync as closeSync3, createReadStream as createReadStream3, openSync as openSync3, readSync } from "node:fs";
|
|
6464
|
-
var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
|
|
6465
|
-
var FIELD_DELIMITER = Buffer.from([0]);
|
|
6466
|
-
function computePlanHash(input) {
|
|
6467
|
-
const hash = createHash4("sha256");
|
|
6468
|
-
hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
|
|
6469
|
-
hash.update(input.compositionHtml);
|
|
6470
|
-
hash.update(FIELD_DELIMITER);
|
|
6471
|
-
const sortedAssets = [...input.assets].sort(
|
|
6472
|
-
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
6473
|
-
);
|
|
6474
|
-
for (const asset of sortedAssets) {
|
|
6475
|
-
hash.update(asset.path, "utf8");
|
|
6476
|
-
hash.update(FIELD_DELIMITER);
|
|
6477
|
-
hash.update(asset.sha256, "utf8");
|
|
6478
|
-
hash.update(FIELD_DELIMITER);
|
|
6526
|
+
function readPositiveInteger(value, field) {
|
|
6527
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
6528
|
+
metadataError(field, "must be a positive integer");
|
|
6479
6529
|
}
|
|
6480
|
-
|
|
6481
|
-
hash.update(FIELD_DELIMITER);
|
|
6482
|
-
hash.update(input.encoderConfigCanonicalJson, "utf8");
|
|
6483
|
-
hash.update(FIELD_DELIMITER);
|
|
6484
|
-
hash.update(input.producerVersion, "utf8");
|
|
6485
|
-
hash.update(FIELD_DELIMITER);
|
|
6486
|
-
hash.update(input.ffmpegVersion, "utf8");
|
|
6487
|
-
hash.update(FIELD_DELIMITER);
|
|
6488
|
-
const d = input.dimensions;
|
|
6489
|
-
hash.update(`${d.fpsNum}/${d.fpsDen}x${d.width}x${d.height}x${d.format}`, "utf8");
|
|
6490
|
-
return hash.digest("hex");
|
|
6530
|
+
return value;
|
|
6491
6531
|
}
|
|
6492
|
-
function
|
|
6493
|
-
if (value
|
|
6494
|
-
|
|
6495
|
-
if (typeof value === "number") {
|
|
6496
|
-
if (!Number.isFinite(value)) {
|
|
6497
|
-
throw new TypeError(`canonicalJsonStringify: non-finite number ${value}`);
|
|
6498
|
-
}
|
|
6499
|
-
return JSON.stringify(value);
|
|
6532
|
+
function readNonNegativeInteger(value, field) {
|
|
6533
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
6534
|
+
metadataError(field, "must be a non-negative integer");
|
|
6500
6535
|
}
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6536
|
+
return value;
|
|
6537
|
+
}
|
|
6538
|
+
function readDeferredFrameRange(value, field) {
|
|
6539
|
+
const range = readRecord(value, field);
|
|
6540
|
+
const sourcePath = readNonEmptyString(range.sourcePath, `${field}.sourcePath`);
|
|
6541
|
+
if (sourcePath.startsWith("/") || sourcePath.split(/[\\/]/).some((part) => part === "..")) {
|
|
6542
|
+
metadataError(`${field}.sourcePath`, "must stay inside the plan directory");
|
|
6504
6543
|
}
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
return `{${parts.join(",")}}`;
|
|
6544
|
+
const durationSeconds = readFiniteNumber(range.durationSeconds, `${field}.durationSeconds`);
|
|
6545
|
+
if (durationSeconds <= 0) metadataError(`${field}.durationSeconds`, "must be positive");
|
|
6546
|
+
if (range.format !== "jpg" && range.format !== "png") {
|
|
6547
|
+
metadataError(`${field}.format`, 'must be "jpg" or "png"');
|
|
6510
6548
|
}
|
|
6511
|
-
|
|
6549
|
+
return {
|
|
6550
|
+
sourcePath,
|
|
6551
|
+
startTime: readFiniteNumber(range.startTime, `${field}.startTime`),
|
|
6552
|
+
durationSeconds,
|
|
6553
|
+
format: range.format
|
|
6554
|
+
};
|
|
6512
6555
|
}
|
|
6513
|
-
function
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6556
|
+
function readRateSpec(value, field) {
|
|
6557
|
+
if (value === void 0) return void 0;
|
|
6558
|
+
if (typeof value === "number") return readFiniteNumber(value, field);
|
|
6559
|
+
const lane = readRecord(value, field);
|
|
6560
|
+
if (!Array.isArray(lane.points)) {
|
|
6561
|
+
metadataError(`${field}.points`, "must be an array");
|
|
6562
|
+
}
|
|
6563
|
+
return lane;
|
|
6517
6564
|
}
|
|
6518
|
-
function
|
|
6519
|
-
const
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
}
|
|
6528
|
-
} finally {
|
|
6529
|
-
closeSync3(fd);
|
|
6565
|
+
function readVideoMetadata(value, field) {
|
|
6566
|
+
const record = readRecord(value, field);
|
|
6567
|
+
let colorSpace = null;
|
|
6568
|
+
if (record.colorSpace !== null) {
|
|
6569
|
+
const color = readRecord(record.colorSpace, `${field}.colorSpace`);
|
|
6570
|
+
colorSpace = {
|
|
6571
|
+
colorTransfer: readString(color.colorTransfer, `${field}.colorSpace.colorTransfer`),
|
|
6572
|
+
colorPrimaries: readString(color.colorPrimaries, `${field}.colorSpace.colorPrimaries`),
|
|
6573
|
+
colorSpace: readString(color.colorSpace, `${field}.colorSpace.colorSpace`)
|
|
6574
|
+
};
|
|
6530
6575
|
}
|
|
6531
|
-
return
|
|
6576
|
+
return {
|
|
6577
|
+
durationSeconds: readFiniteNumber(record.durationSeconds, `${field}.durationSeconds`),
|
|
6578
|
+
videoStreamDurationSeconds: readFiniteNumber(
|
|
6579
|
+
record.videoStreamDurationSeconds,
|
|
6580
|
+
`${field}.videoStreamDurationSeconds`
|
|
6581
|
+
),
|
|
6582
|
+
// Plans written before stream-start metadata existed implicitly used the
|
|
6583
|
+
// overwhelmingly common start-at-zero domain. Preserve that compatibility
|
|
6584
|
+
// while carrying non-zero edit-list/transport timestamps in new plans.
|
|
6585
|
+
videoStreamStartSeconds: record.videoStreamStartSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamStartSeconds, `${field}.videoStreamStartSeconds`),
|
|
6586
|
+
videoStreamLeadSeconds: record.videoStreamLeadSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamLeadSeconds, `${field}.videoStreamLeadSeconds`),
|
|
6587
|
+
width: readPositiveInteger(record.width, `${field}.width`),
|
|
6588
|
+
height: readPositiveInteger(record.height, `${field}.height`),
|
|
6589
|
+
fps: readFiniteNumber(record.fps, `${field}.fps`),
|
|
6590
|
+
videoCodec: readNonEmptyString(record.videoCodec, `${field}.videoCodec`),
|
|
6591
|
+
hasAudio: readBoolean(record.hasAudio, `${field}.hasAudio`),
|
|
6592
|
+
isVFR: readBoolean(record.isVFR, `${field}.isVFR`),
|
|
6593
|
+
hasAlpha: readBoolean(record.hasAlpha, `${field}.hasAlpha`),
|
|
6594
|
+
colorSpace
|
|
6595
|
+
};
|
|
6532
6596
|
}
|
|
6533
|
-
|
|
6534
|
-
const
|
|
6535
|
-
|
|
6536
|
-
|
|
6597
|
+
function parsePlanVideosJson(value) {
|
|
6598
|
+
const record = readRecord(value, "meta/videos.json");
|
|
6599
|
+
if (!Array.isArray(record.videos) || !Array.isArray(record.extracted)) {
|
|
6600
|
+
metadataError("meta/videos.json", "must contain videos and extracted arrays");
|
|
6537
6601
|
}
|
|
6538
|
-
|
|
6539
|
-
}
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6602
|
+
const videos = record.videos.map((value2, index) => {
|
|
6603
|
+
const field = `meta/videos.json.videos[${index}]`;
|
|
6604
|
+
const video = readRecord(value2, field);
|
|
6605
|
+
return {
|
|
6606
|
+
id: readNonEmptyString(video.id, `${field}.id`),
|
|
6607
|
+
src: readNonEmptyString(video.src, `${field}.src`),
|
|
6608
|
+
start: readFiniteNumber(video.start, `${field}.start`),
|
|
6609
|
+
end: readFiniteNumber(video.end, `${field}.end`),
|
|
6610
|
+
mediaStart: readFiniteNumber(video.mediaStart, `${field}.mediaStart`),
|
|
6611
|
+
playbackRate: readRateSpec(video.playbackRate, `${field}.playbackRate`),
|
|
6612
|
+
loop: readBoolean(video.loop, `${field}.loop`),
|
|
6613
|
+
hasAudio: readBoolean(video.hasAudio, `${field}.hasAudio`)
|
|
6614
|
+
};
|
|
6615
|
+
});
|
|
6616
|
+
const extracted = record.extracted.map((value2, index) => {
|
|
6617
|
+
const field = `meta/videos.json.extracted[${index}]`;
|
|
6618
|
+
const entry = readRecord(value2, field);
|
|
6619
|
+
return {
|
|
6620
|
+
videoId: readNonEmptyString(entry.videoId, `${field}.videoId`),
|
|
6621
|
+
srcPath: readNonEmptyString(entry.srcPath, `${field}.srcPath`),
|
|
6622
|
+
framePattern: readNonEmptyString(entry.framePattern, `${field}.framePattern`),
|
|
6623
|
+
fps: readFiniteNumber(entry.fps, `${field}.fps`),
|
|
6624
|
+
totalFrames: readNonNegativeInteger(entry.totalFrames, `${field}.totalFrames`),
|
|
6625
|
+
metadata: readVideoMetadata(entry.metadata, `${field}.metadata`),
|
|
6626
|
+
...entry.deferredRange === void 0 ? {} : { deferredRange: readDeferredFrameRange(entry.deferredRange, `${field}.deferredRange`) }
|
|
6627
|
+
};
|
|
6628
|
+
});
|
|
6629
|
+
const videoIds = /* @__PURE__ */ new Set();
|
|
6630
|
+
for (const video of videos) {
|
|
6631
|
+
if (videoIds.has(video.id)) {
|
|
6632
|
+
metadataError("meta/videos.json.videos", `contains duplicate id ${JSON.stringify(video.id)}`);
|
|
6633
|
+
}
|
|
6634
|
+
videoIds.add(video.id);
|
|
6553
6635
|
}
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6636
|
+
const extractedIds = /* @__PURE__ */ new Set();
|
|
6637
|
+
for (const entry of extracted) {
|
|
6638
|
+
if (extractedIds.has(entry.videoId)) {
|
|
6639
|
+
metadataError(
|
|
6640
|
+
"meta/videos.json.extracted",
|
|
6641
|
+
`contains duplicate videoId ${JSON.stringify(entry.videoId)}`
|
|
6642
|
+
);
|
|
6643
|
+
}
|
|
6644
|
+
extractedIds.add(entry.videoId);
|
|
6645
|
+
if (!videoIds.has(entry.videoId)) {
|
|
6646
|
+
metadataError(
|
|
6647
|
+
"meta/videos.json.extracted",
|
|
6648
|
+
`references undeclared video ${JSON.stringify(entry.videoId)}`
|
|
6649
|
+
);
|
|
6650
|
+
}
|
|
6651
|
+
}
|
|
6652
|
+
for (const video of videos) {
|
|
6653
|
+
if (!extractedIds.has(video.id)) {
|
|
6654
|
+
metadataError(
|
|
6655
|
+
"meta/videos.json.extracted",
|
|
6656
|
+
`is missing declared video ${JSON.stringify(video.id)}`
|
|
6657
|
+
);
|
|
6658
|
+
}
|
|
6560
6659
|
}
|
|
6660
|
+
return { videos, extracted };
|
|
6561
6661
|
}
|
|
6562
|
-
function
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
out[key] = stripUndefined(v);
|
|
6662
|
+
function buildPlanVideosJson(input) {
|
|
6663
|
+
const videos = input.videos.map((video, index) => {
|
|
6664
|
+
if (Number.isFinite(video.end)) return { ...video };
|
|
6665
|
+
if (!Number.isFinite(input.compositionEnd) || input.compositionEnd <= 0 || input.compositionEnd <= video.start) {
|
|
6666
|
+
metadataError(
|
|
6667
|
+
`meta/videos.json.videos[${index}].end`,
|
|
6668
|
+
"cannot be resolved without a finite composition end after its start"
|
|
6669
|
+
);
|
|
6571
6670
|
}
|
|
6572
|
-
return
|
|
6671
|
+
return { ...video, end: input.compositionEnd };
|
|
6672
|
+
});
|
|
6673
|
+
return parsePlanVideosJson({ videos, extracted: input.extracted });
|
|
6674
|
+
}
|
|
6675
|
+
var execFile = promisify2(execFileCallback);
|
|
6676
|
+
var cachedFfmpegVersions = /* @__PURE__ */ new Map();
|
|
6677
|
+
async function readFfmpegVersion() {
|
|
6678
|
+
const binary = getFfmpegBinary();
|
|
6679
|
+
const cached = cachedFfmpegVersions.get(binary);
|
|
6680
|
+
if (cached !== void 0) return cached;
|
|
6681
|
+
let stdout;
|
|
6682
|
+
try {
|
|
6683
|
+
({ stdout } = await execFile(binary, ["-version"], {
|
|
6684
|
+
maxBuffer: 1024 * 1024,
|
|
6685
|
+
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
|
|
6686
|
+
windowsHide: true
|
|
6687
|
+
}));
|
|
6688
|
+
} catch (error) {
|
|
6689
|
+
if (error.code !== "ENOENT") throw error;
|
|
6690
|
+
throw Object.assign(
|
|
6691
|
+
new Error(`ffmpeg not found at "${binary}": install FFmpeg or set HYPERFRAMES_FFMPEG_PATH.`, {
|
|
6692
|
+
cause: error
|
|
6693
|
+
}),
|
|
6694
|
+
{ code: "ENOENT" }
|
|
6695
|
+
);
|
|
6573
6696
|
}
|
|
6574
|
-
|
|
6697
|
+
const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
|
|
6698
|
+
if (!firstLine) {
|
|
6699
|
+
throw new Error("ffmpeg -version returned empty output");
|
|
6700
|
+
}
|
|
6701
|
+
cachedFfmpegVersions.set(binary, firstLine);
|
|
6702
|
+
return firstLine;
|
|
6575
6703
|
}
|
|
6576
|
-
function
|
|
6577
|
-
const
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6704
|
+
function buildSyntheticRenderJob(input) {
|
|
6705
|
+
const renderConfig = {
|
|
6706
|
+
fps: input.fps,
|
|
6707
|
+
quality: input.quality,
|
|
6708
|
+
format: input.format,
|
|
6709
|
+
crf: input.crf,
|
|
6710
|
+
videoBitrate: input.bitrate,
|
|
6711
|
+
videoFrameFormat: input.videoFrameFormat,
|
|
6712
|
+
outputResolution: input.outputResolution,
|
|
6713
|
+
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
|
6714
|
+
// Distributed mode hard-pins to software GPU. The plan-time validator
|
|
6715
|
+
// refuses to fan out otherwise.
|
|
6716
|
+
useGpu: false,
|
|
6717
|
+
debug: false,
|
|
6718
|
+
entryFile: input.entryFile,
|
|
6719
|
+
logger: input.logger ?? defaultLogger,
|
|
6720
|
+
hdrMode: input.hdrMode,
|
|
6721
|
+
strictness: input.strictness,
|
|
6722
|
+
producerConfig: input.producerConfig,
|
|
6723
|
+
variables: input.variables
|
|
6724
|
+
};
|
|
6725
|
+
return createRenderJob(renderConfig);
|
|
6726
|
+
}
|
|
6727
|
+
var cachedProducerVersion = null;
|
|
6728
|
+
function readProducerVersion() {
|
|
6729
|
+
if (cachedProducerVersion !== null) return cachedProducerVersion;
|
|
6730
|
+
const startDir = dirname8(fileURLToPath3(import.meta.url));
|
|
6731
|
+
let current = startDir;
|
|
6732
|
+
for (let i = 0; i < 10; i++) {
|
|
6733
|
+
const candidate = join15(current, "package.json");
|
|
6734
|
+
if (existsSync11(candidate)) {
|
|
6735
|
+
try {
|
|
6736
|
+
const pkg = JSON.parse(readFileSync7(candidate, "utf-8"));
|
|
6737
|
+
if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
|
|
6738
|
+
cachedProducerVersion = pkg.version;
|
|
6739
|
+
return pkg.version;
|
|
6740
|
+
}
|
|
6741
|
+
} catch {
|
|
6590
6742
|
}
|
|
6591
6743
|
}
|
|
6744
|
+
const parent = dirname8(current);
|
|
6745
|
+
if (parent === current) break;
|
|
6746
|
+
current = parent;
|
|
6592
6747
|
}
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
return results;
|
|
6748
|
+
cachedProducerVersion = "0.0.0-unknown";
|
|
6749
|
+
return cachedProducerVersion;
|
|
6596
6750
|
}
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6751
|
+
|
|
6752
|
+
// src/services/distributed/chunkFrames.ts
|
|
6753
|
+
var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
|
|
6754
|
+
var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
|
|
6755
|
+
var MISSING_PLAN_ARTIFACT = "MISSING_PLAN_ARTIFACT";
|
|
6756
|
+
var CHUNK_INDEX_OUT_OF_RANGE = "CHUNK_INDEX_OUT_OF_RANGE";
|
|
6757
|
+
var MISSING_RUNTIME_ENV_SNAPSHOT = "MISSING_RUNTIME_ENV_SNAPSHOT";
|
|
6758
|
+
var RenderChunkValidationError = class extends Error {
|
|
6759
|
+
code;
|
|
6760
|
+
constructor(code, message) {
|
|
6761
|
+
super(message);
|
|
6762
|
+
this.name = "RenderChunkValidationError";
|
|
6763
|
+
this.code = code;
|
|
6764
|
+
}
|
|
6765
|
+
};
|
|
6766
|
+
function createChunkVideoFrameInjectorFactory(frameLookup) {
|
|
6767
|
+
return () => createVideoFrameInjector2(frameLookup);
|
|
6768
|
+
}
|
|
6769
|
+
function frameNumberFromFileName(name) {
|
|
6770
|
+
const match = /(\d+)(?=\.[^.]+$)/.exec(name);
|
|
6771
|
+
if (!match) return null;
|
|
6772
|
+
const frameNumber = Number(match[1]);
|
|
6773
|
+
return Number.isSafeInteger(frameNumber) ? frameNumber : null;
|
|
6774
|
+
}
|
|
6775
|
+
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
6776
|
+
const result = [];
|
|
6777
|
+
for (const v of videos) {
|
|
6778
|
+
const outputDir = join16(planDir, "video-frames", v.videoId);
|
|
6779
|
+
if (!existsSync12(outputDir)) {
|
|
6780
|
+
throw new Error(
|
|
6781
|
+
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
6782
|
+
);
|
|
6605
6783
|
}
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6784
|
+
const ext = (extname2(v.framePattern) || ".jpg").toLowerCase();
|
|
6785
|
+
const frames = readdirSync4(outputDir).filter((name) => name.toLowerCase().endsWith(ext)).sort((left, right) => {
|
|
6786
|
+
const leftNumber = frameNumberFromFileName(left);
|
|
6787
|
+
const rightNumber = frameNumberFromFileName(right);
|
|
6788
|
+
if (leftNumber === null || rightNumber === null) return left.localeCompare(right);
|
|
6789
|
+
return leftNumber - rightNumber || left.localeCompare(right);
|
|
6790
|
+
});
|
|
6791
|
+
const framePaths = /* @__PURE__ */ new Map();
|
|
6792
|
+
for (let i = 0; i < frames.length; i++) {
|
|
6793
|
+
const frameName = frames[i];
|
|
6794
|
+
if (!frameName) continue;
|
|
6795
|
+
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
6796
|
+
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
6797
|
+
framePaths.set(frameIndex, join16(outputDir, frameName));
|
|
6798
|
+
}
|
|
6799
|
+
result.push({
|
|
6800
|
+
videoId: v.videoId,
|
|
6801
|
+
srcPath: v.srcPath,
|
|
6802
|
+
outputDir,
|
|
6803
|
+
framePattern: v.framePattern,
|
|
6804
|
+
fps: v.fps,
|
|
6805
|
+
totalFrames: v.totalFrames,
|
|
6806
|
+
metadata: v.metadata,
|
|
6807
|
+
framePaths,
|
|
6808
|
+
// The planDir is shared read-only across workers; cleanup must not delete its frames.
|
|
6809
|
+
ownedByLookup: false
|
|
6610
6810
|
});
|
|
6611
6811
|
}
|
|
6612
|
-
|
|
6613
|
-
throw new Error(
|
|
6614
|
-
`[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH} (upstream compile stage did not materialize the expected file).`
|
|
6615
|
-
);
|
|
6616
|
-
}
|
|
6617
|
-
return { compositionHtml, assets };
|
|
6812
|
+
return result;
|
|
6618
6813
|
}
|
|
6619
|
-
function
|
|
6620
|
-
const
|
|
6621
|
-
const
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
if (!existsSync11(encoderJsonPath)) {
|
|
6626
|
-
throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
|
|
6814
|
+
function contiguousRuns(sortedIndexes) {
|
|
6815
|
+
const runs2 = [];
|
|
6816
|
+
for (const index of sortedIndexes) {
|
|
6817
|
+
const last = runs2[runs2.length - 1];
|
|
6818
|
+
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
6819
|
+
else runs2.push({ firstFrame: index, frames: 1 });
|
|
6627
6820
|
}
|
|
6628
|
-
|
|
6629
|
-
const encoderConfigCanonicalJson = readFileSync7(encoderJsonPath, "utf-8");
|
|
6630
|
-
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
6631
|
-
return computePlanHash({
|
|
6632
|
-
compositionHtml,
|
|
6633
|
-
assets,
|
|
6634
|
-
fontSnapshotSha: planJson.fontSnapshotSha,
|
|
6635
|
-
encoderConfigCanonicalJson,
|
|
6636
|
-
producerVersion: planJson.producerVersion,
|
|
6637
|
-
ffmpegVersion: planJson.ffmpegVersion,
|
|
6638
|
-
dimensions: planJson.dimensions
|
|
6639
|
-
});
|
|
6821
|
+
return runs2;
|
|
6640
6822
|
}
|
|
6641
|
-
|
|
6642
|
-
const {
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
dimensions,
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
const metaDir = join15(planDir, "meta");
|
|
6659
|
-
if (!existsSync11(metaDir)) mkdirSync7(metaDir, { recursive: true });
|
|
6660
|
-
writeFileSync4(
|
|
6661
|
-
join15(metaDir, "composition.json"),
|
|
6662
|
-
`${JSON.stringify(composition, null, 2)}
|
|
6663
|
-
`,
|
|
6664
|
-
"utf-8"
|
|
6665
|
-
);
|
|
6666
|
-
const encoderForCanonical = stripUndefined(encoder);
|
|
6667
|
-
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
6668
|
-
writeFileSync4(join15(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
6669
|
-
writeFileSync4(join15(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
6670
|
-
`, "utf-8");
|
|
6671
|
-
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
6672
|
-
const planHash = computePlanHash({
|
|
6673
|
-
compositionHtml,
|
|
6674
|
-
assets,
|
|
6675
|
-
fontSnapshotSha,
|
|
6676
|
-
encoderConfigCanonicalJson,
|
|
6677
|
-
producerVersion,
|
|
6678
|
-
ffmpegVersion: encoder.ffmpegVersion,
|
|
6679
|
-
dimensions
|
|
6680
|
-
});
|
|
6681
|
-
const planJson = {
|
|
6682
|
-
protocol: PLAN_PROTOCOL_V1,
|
|
6683
|
-
planHash,
|
|
6684
|
-
producerVersion,
|
|
6685
|
-
ffmpegVersion: encoder.ffmpegVersion,
|
|
6686
|
-
fontSnapshotSha,
|
|
6687
|
-
dimensions,
|
|
6688
|
-
chunkCount: chunks.length,
|
|
6689
|
-
totalFrames,
|
|
6690
|
-
duration: durationSeconds,
|
|
6691
|
-
hasAudio
|
|
6823
|
+
function buildChunkCaptureOptions(input) {
|
|
6824
|
+
const { dimensions, encoder } = input;
|
|
6825
|
+
const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(input.videoCount);
|
|
6826
|
+
return {
|
|
6827
|
+
width: dimensions.width,
|
|
6828
|
+
height: dimensions.height,
|
|
6829
|
+
fps: { num: dimensions.fpsNum, den: dimensions.fpsDen },
|
|
6830
|
+
format: dimensions.format === "mp4" ? "jpeg" : "png",
|
|
6831
|
+
quality: dimensions.format === "mp4" ? resolveCaptureJpegQuality(encoder.quality) : void 0,
|
|
6832
|
+
deviceScaleFactor: encoder.deviceScaleFactor,
|
|
6833
|
+
// The controller's snapshot, so the first capture sees the in-process renderer's `__hfVariables`.
|
|
6834
|
+
variables: encoder.variables,
|
|
6835
|
+
subTimelineWaitMemo: {},
|
|
6836
|
+
...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
|
|
6837
|
+
// A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
|
|
6838
|
+
lockWarmupTicks: true,
|
|
6839
|
+
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
6692
6840
|
};
|
|
6693
|
-
const planJsonPath = join15(planDir, "plan.json");
|
|
6694
|
-
writeFileSync4(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
6695
|
-
`, "utf-8");
|
|
6696
|
-
return { planJsonPath, planHash };
|
|
6697
6841
|
}
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
if (typeof value !== "string") continue;
|
|
6719
|
-
const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
6720
|
-
if (!matches) continue;
|
|
6721
|
-
previous[key] = env[key];
|
|
6722
|
-
env[key] = value;
|
|
6842
|
+
async function extractDeferredVideoFramesForChunk(input) {
|
|
6843
|
+
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
6844
|
+
if (deferred.length === 0) return [];
|
|
6845
|
+
const table = createFrameLookupTable2(
|
|
6846
|
+
input.planVideos.videos,
|
|
6847
|
+
deferred.map((video) => ({
|
|
6848
|
+
...video,
|
|
6849
|
+
outputDir: input.outputRoot,
|
|
6850
|
+
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
6851
|
+
})),
|
|
6852
|
+
resolveRenderFpsConfig(input.fps).value
|
|
6853
|
+
);
|
|
6854
|
+
const shown = /* @__PURE__ */ new Map();
|
|
6855
|
+
for (const frame of input.frames) {
|
|
6856
|
+
const globalTime = frame * input.fps.den / input.fps.num;
|
|
6857
|
+
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
6858
|
+
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
6859
|
+
indexes.add(payload.frameIndex);
|
|
6860
|
+
shown.set(videoId, indexes);
|
|
6861
|
+
}
|
|
6723
6862
|
}
|
|
6724
|
-
return
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6863
|
+
return Promise.all(
|
|
6864
|
+
deferred.map(async (video) => {
|
|
6865
|
+
const outputDir = join16(input.outputRoot, video.videoId);
|
|
6866
|
+
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
6867
|
+
if (indexes.length === 0) {
|
|
6868
|
+
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
6869
|
+
}
|
|
6870
|
+
const range = video.deferredRange;
|
|
6871
|
+
const sourcePath = join16(input.planDir, ...range.sourcePath.split("/"));
|
|
6872
|
+
if (!existsSync12(sourcePath)) {
|
|
6873
|
+
throw new RenderChunkValidationError(
|
|
6874
|
+
MISSING_PLAN_ARTIFACT,
|
|
6875
|
+
`[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
|
|
6876
|
+
);
|
|
6729
6877
|
}
|
|
6730
|
-
|
|
6731
|
-
|
|
6878
|
+
const extracted = await extractVideoFramesRange(
|
|
6879
|
+
sourcePath,
|
|
6880
|
+
video.videoId,
|
|
6881
|
+
range.startTime,
|
|
6882
|
+
range.durationSeconds,
|
|
6883
|
+
{
|
|
6884
|
+
fps: input.fps,
|
|
6885
|
+
outputDir: input.outputRoot,
|
|
6886
|
+
format: range.format,
|
|
6887
|
+
frameRanges: contiguousRuns(indexes)
|
|
6888
|
+
},
|
|
6889
|
+
void 0,
|
|
6890
|
+
input.cfg
|
|
6891
|
+
);
|
|
6892
|
+
const missing = indexes.find((index) => !extracted.framePaths.has(index));
|
|
6893
|
+
if (missing !== void 0) {
|
|
6894
|
+
throw new RenderChunkValidationError(
|
|
6895
|
+
INVALID_VIDEO_METADATA,
|
|
6896
|
+
`[renderChunk] video ${JSON.stringify(video.videoId)} has no frame ${missing}; the plan expected ${video.totalFrames} frames from ${range.sourcePath}.`
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6899
|
+
return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
|
|
6900
|
+
})
|
|
6901
|
+
);
|
|
6732
6902
|
}
|
|
6733
6903
|
|
|
6734
|
-
// src/services/distributed/
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6904
|
+
// src/services/distributed/planFrameCost.ts
|
|
6905
|
+
async function measurePlanSecondsPerFrame(input) {
|
|
6906
|
+
const { planDir, workDir, planVideos, fps, log } = input;
|
|
6907
|
+
const fpsRatio = { num: fps, den: 1 };
|
|
6908
|
+
const sampled = selectCaptureCalibrationFrames(input.totalFrames);
|
|
6909
|
+
const extracted = [
|
|
6910
|
+
...rebuildExtractedFramesFromPlanDir(
|
|
6911
|
+
planDir,
|
|
6912
|
+
planVideos.extracted.filter((video) => !video.deferredRange)
|
|
6913
|
+
),
|
|
6914
|
+
...await extractDeferredVideoFramesForChunk({
|
|
6915
|
+
planDir,
|
|
6916
|
+
planVideos,
|
|
6917
|
+
frames: sampled,
|
|
6918
|
+
fps: fpsRatio,
|
|
6919
|
+
outputRoot: join17(workDir, "frame-cost-video-frames"),
|
|
6920
|
+
cfg: input.cfg
|
|
6921
|
+
})
|
|
6922
|
+
];
|
|
6923
|
+
const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
6924
|
+
const compiledDir = join17(planDir, "compiled");
|
|
6925
|
+
const fileServer = await createFileServer({
|
|
6926
|
+
projectDir: compiledDir,
|
|
6927
|
+
compiledDir,
|
|
6928
|
+
port: 0,
|
|
6929
|
+
preHeadScripts: [buildVirtualTimeShim({ seedRandomFromFrame: true })],
|
|
6930
|
+
fps: fpsRatio
|
|
6931
|
+
});
|
|
6932
|
+
let session = null;
|
|
6933
|
+
try {
|
|
6934
|
+
session = await createCaptureSession8(
|
|
6935
|
+
fileServer.url,
|
|
6936
|
+
join17(workDir, "frame-cost"),
|
|
6937
|
+
buildChunkCaptureOptions({
|
|
6938
|
+
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
6939
|
+
encoder: input,
|
|
6940
|
+
videoCount: planVideos.videos.length,
|
|
6941
|
+
html: input.html
|
|
6942
|
+
}),
|
|
6943
|
+
createChunkVideoFrameInjectorFactory(lookup)(),
|
|
6944
|
+
createCaptureCalibrationConfig(input.cfg)
|
|
6945
|
+
);
|
|
6946
|
+
await initializeSession8(session);
|
|
6947
|
+
const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
|
|
6948
|
+
return (estimate.p95Ms ?? 0) / 1e3;
|
|
6949
|
+
} finally {
|
|
6950
|
+
if (session) await closeCaptureSession9(session).catch(() => {
|
|
6951
|
+
});
|
|
6952
|
+
closeFileServerSafely(fileServer, "plan frame cost", log);
|
|
6754
6953
|
}
|
|
6755
|
-
return null;
|
|
6756
6954
|
}
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6955
|
+
|
|
6956
|
+
// src/services/render/stages/freezePlan.ts
|
|
6957
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6958
|
+
import { join as join18, relative as relative3, resolve as resolve7 } from "node:path";
|
|
6959
|
+
|
|
6960
|
+
// src/services/distributed/planProtocol.ts
|
|
6961
|
+
var PLAN_SCHEMA_VERSION = 1;
|
|
6962
|
+
var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
|
|
6963
|
+
var PLAN_HASH_SCHEMA = "hyperframes-plan-hash-v1";
|
|
6964
|
+
var PLAN_V2_SCHEMA_VERSION = 2;
|
|
6965
|
+
var PLAN_V2_ARTIFACT_LAYOUT = "content-addressed-plan-v2";
|
|
6966
|
+
var PLAN_V2_HASH_SCHEMA = "hyperframes-plan-manifest-hash-v2";
|
|
6967
|
+
var PLAN_PROTOCOL_UNSUPPORTED = "PLAN_PROTOCOL_UNSUPPORTED";
|
|
6968
|
+
var PLAN_PROTOCOL_V1 = Object.freeze({
|
|
6969
|
+
schemaVersion: PLAN_SCHEMA_VERSION,
|
|
6970
|
+
artifactLayout: PLAN_ARTIFACT_LAYOUT,
|
|
6971
|
+
hashSchema: PLAN_HASH_SCHEMA
|
|
6972
|
+
});
|
|
6973
|
+
var CURRENT_PLAN_PROTOCOL = PLAN_PROTOCOL_V1;
|
|
6974
|
+
var PLAN_PROTOCOL_V2 = Object.freeze({
|
|
6975
|
+
schemaVersion: PLAN_V2_SCHEMA_VERSION,
|
|
6976
|
+
artifactLayout: PLAN_V2_ARTIFACT_LAYOUT,
|
|
6977
|
+
hashSchema: PLAN_V2_HASH_SCHEMA
|
|
6978
|
+
});
|
|
6979
|
+
var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
|
|
6980
|
+
roles: Object.freeze({
|
|
6981
|
+
planner: Object.freeze({
|
|
6982
|
+
produces: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2])
|
|
6983
|
+
}),
|
|
6984
|
+
chunk: Object.freeze({
|
|
6985
|
+
accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
|
|
6986
|
+
acceptsLegacyV1WithoutDescriptor: true
|
|
6987
|
+
}),
|
|
6988
|
+
assembler: Object.freeze({
|
|
6989
|
+
accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
|
|
6990
|
+
acceptsLegacyV1WithoutDescriptor: true
|
|
6991
|
+
})
|
|
6992
|
+
})
|
|
6993
|
+
});
|
|
6994
|
+
function getDistributedRenderCapabilities() {
|
|
6995
|
+
return DISTRIBUTED_RENDER_CAPABILITIES;
|
|
6996
|
+
}
|
|
6997
|
+
var PlanProtocolUnsupportedError = class extends Error {
|
|
6998
|
+
// Public adapters inspect this typed code even though OSS producer does not.
|
|
6760
6999
|
// fallow-ignore-next-line unused-class-member
|
|
6761
|
-
code =
|
|
6762
|
-
constructor(
|
|
6763
|
-
super(
|
|
6764
|
-
this.name = "
|
|
7000
|
+
code = PLAN_PROTOCOL_UNSUPPORTED;
|
|
7001
|
+
constructor(reason) {
|
|
7002
|
+
super(`[planProtocol] ${reason} (${PLAN_PROTOCOL_UNSUPPORTED})`);
|
|
7003
|
+
this.name = "PlanProtocolUnsupportedError";
|
|
6765
7004
|
}
|
|
6766
7005
|
};
|
|
6767
|
-
function
|
|
6768
|
-
|
|
7006
|
+
function isRecord(value) {
|
|
7007
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6769
7008
|
}
|
|
6770
|
-
function
|
|
6771
|
-
|
|
6772
|
-
metadataError(field, "must be an object");
|
|
6773
|
-
}
|
|
6774
|
-
return value;
|
|
7009
|
+
function protocolMatches(descriptor, expected) {
|
|
7010
|
+
return descriptor.schemaVersion === expected.schemaVersion && descriptor.artifactLayout === expected.artifactLayout && descriptor.hashSchema === expected.hashSchema;
|
|
6775
7011
|
}
|
|
6776
|
-
function
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
return value;
|
|
7012
|
+
function capabilitiesAccept(capabilities, protocol) {
|
|
7013
|
+
return capabilities.accepts.some(
|
|
7014
|
+
(accepted) => accepted.schemaVersion === protocol.schemaVersion && accepted.artifactLayout === protocol.artifactLayout && accepted.hashSchema === protocol.hashSchema
|
|
7015
|
+
);
|
|
6781
7016
|
}
|
|
6782
|
-
function
|
|
6783
|
-
if (
|
|
6784
|
-
|
|
7017
|
+
function readPlanProtocol(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
|
|
7018
|
+
if (!isRecord(planJson)) {
|
|
7019
|
+
throw new PlanProtocolUnsupportedError("plan.json must contain a JSON object");
|
|
6785
7020
|
}
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
7021
|
+
if (!Object.prototype.hasOwnProperty.call(planJson, "protocol")) {
|
|
7022
|
+
if (!capabilities.acceptsLegacyV1WithoutDescriptor || !capabilitiesAccept(capabilities, PLAN_PROTOCOL_V1)) {
|
|
7023
|
+
throw new PlanProtocolUnsupportedError(
|
|
7024
|
+
"legacy v1 plan without a protocol descriptor is not accepted by this worker"
|
|
7025
|
+
);
|
|
7026
|
+
}
|
|
7027
|
+
return PLAN_PROTOCOL_V1;
|
|
6791
7028
|
}
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
if (typeof value !== "boolean") {
|
|
6796
|
-
metadataError(field, "must be boolean");
|
|
7029
|
+
const descriptor = planJson.protocol;
|
|
7030
|
+
if (!isRecord(descriptor)) {
|
|
7031
|
+
throw new PlanProtocolUnsupportedError("plan.json protocol descriptor must be an object");
|
|
6797
7032
|
}
|
|
6798
|
-
|
|
7033
|
+
for (const field of ["schemaVersion", "artifactLayout", "hashSchema"]) {
|
|
7034
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, field)) {
|
|
7035
|
+
throw new PlanProtocolUnsupportedError(`plan.json protocol descriptor is missing ${field}`);
|
|
7036
|
+
}
|
|
7037
|
+
}
|
|
7038
|
+
const protocol = protocolMatches(descriptor, PLAN_PROTOCOL_V1) ? PLAN_PROTOCOL_V1 : protocolMatches(descriptor, PLAN_PROTOCOL_V2) ? PLAN_PROTOCOL_V2 : null;
|
|
7039
|
+
if (protocol === null || !capabilitiesAccept(capabilities, protocol)) {
|
|
7040
|
+
throw new PlanProtocolUnsupportedError("unsupported plan.json protocol descriptor");
|
|
7041
|
+
}
|
|
7042
|
+
return protocol;
|
|
6799
7043
|
}
|
|
6800
|
-
function
|
|
6801
|
-
|
|
6802
|
-
|
|
7044
|
+
function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
|
|
7045
|
+
const protocol = readPlanProtocol(planJson, capabilities);
|
|
7046
|
+
if (protocol !== PLAN_PROTOCOL_V1) {
|
|
7047
|
+
throw new PlanProtocolUnsupportedError(
|
|
7048
|
+
"content-addressed v2 plan must be materialized before v1 layout access"
|
|
7049
|
+
);
|
|
6803
7050
|
}
|
|
6804
|
-
return
|
|
7051
|
+
return PLAN_PROTOCOL_V1;
|
|
6805
7052
|
}
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
7053
|
+
|
|
7054
|
+
// src/services/render/stages/planHash.ts
|
|
7055
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
7056
|
+
import { closeSync as closeSync3, createReadStream as createReadStream3, openSync as openSync3, readSync } from "node:fs";
|
|
7057
|
+
var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
|
|
7058
|
+
var FIELD_DELIMITER = Buffer.from([0]);
|
|
7059
|
+
function computePlanHash(input) {
|
|
7060
|
+
const hash = createHash4("sha256");
|
|
7061
|
+
hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
|
|
7062
|
+
hash.update(input.compositionHtml);
|
|
7063
|
+
hash.update(FIELD_DELIMITER);
|
|
7064
|
+
const sortedAssets = [...input.assets].sort(
|
|
7065
|
+
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
7066
|
+
);
|
|
7067
|
+
for (const asset of sortedAssets) {
|
|
7068
|
+
hash.update(asset.path, "utf8");
|
|
7069
|
+
hash.update(FIELD_DELIMITER);
|
|
7070
|
+
hash.update(asset.sha256, "utf8");
|
|
7071
|
+
hash.update(FIELD_DELIMITER);
|
|
6809
7072
|
}
|
|
6810
|
-
|
|
7073
|
+
hash.update(input.fontSnapshotSha, "utf8");
|
|
7074
|
+
hash.update(FIELD_DELIMITER);
|
|
7075
|
+
hash.update(input.encoderConfigCanonicalJson, "utf8");
|
|
7076
|
+
hash.update(FIELD_DELIMITER);
|
|
7077
|
+
hash.update(input.producerVersion, "utf8");
|
|
7078
|
+
hash.update(FIELD_DELIMITER);
|
|
7079
|
+
hash.update(input.ffmpegVersion, "utf8");
|
|
7080
|
+
hash.update(FIELD_DELIMITER);
|
|
7081
|
+
const d = input.dimensions;
|
|
7082
|
+
hash.update(`${d.fpsNum}/${d.fpsDen}x${d.width}x${d.height}x${d.format}`, "utf8");
|
|
7083
|
+
return hash.digest("hex");
|
|
6811
7084
|
}
|
|
6812
|
-
function
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
if (
|
|
6816
|
-
|
|
7085
|
+
function canonicalJsonStringify(value) {
|
|
7086
|
+
if (value === null) return "null";
|
|
7087
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
7088
|
+
if (typeof value === "number") {
|
|
7089
|
+
if (!Number.isFinite(value)) {
|
|
7090
|
+
throw new TypeError(`canonicalJsonStringify: non-finite number ${value}`);
|
|
7091
|
+
}
|
|
7092
|
+
return JSON.stringify(value);
|
|
6817
7093
|
}
|
|
6818
|
-
|
|
6819
|
-
if (
|
|
6820
|
-
|
|
6821
|
-
|
|
7094
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
7095
|
+
if (Array.isArray(value)) {
|
|
7096
|
+
return `[${value.map(canonicalJsonStringify).join(",")}]`;
|
|
7097
|
+
}
|
|
7098
|
+
if (typeof value === "object") {
|
|
7099
|
+
const obj = value;
|
|
7100
|
+
const keys = Object.keys(obj).sort();
|
|
7101
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJsonStringify(obj[k])}`);
|
|
7102
|
+
return `{${parts.join(",")}}`;
|
|
7103
|
+
}
|
|
7104
|
+
throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
|
|
7105
|
+
}
|
|
7106
|
+
function sha256Hex(bytes) {
|
|
7107
|
+
const h = createHash4("sha256");
|
|
7108
|
+
h.update(bytes);
|
|
7109
|
+
return h.digest("hex");
|
|
7110
|
+
}
|
|
7111
|
+
function sha256File(path) {
|
|
7112
|
+
const hash = createHash4("sha256");
|
|
7113
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
7114
|
+
const fd = openSync3(path, "r");
|
|
7115
|
+
try {
|
|
7116
|
+
let bytesRead = 0;
|
|
7117
|
+
do {
|
|
7118
|
+
bytesRead = readSync(fd, buffer, 0, buffer.byteLength, null);
|
|
7119
|
+
if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
|
|
7120
|
+
} while (bytesRead > 0);
|
|
7121
|
+
} finally {
|
|
7122
|
+
closeSync3(fd);
|
|
6822
7123
|
}
|
|
6823
|
-
return
|
|
6824
|
-
sourcePath,
|
|
6825
|
-
startTime: readFiniteNumber(range.startTime, `${field}.startTime`),
|
|
6826
|
-
durationSeconds,
|
|
6827
|
-
format: range.format
|
|
6828
|
-
};
|
|
7124
|
+
return hash.digest("hex");
|
|
6829
7125
|
}
|
|
6830
|
-
function
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
if (!Array.isArray(lane.points)) {
|
|
6835
|
-
metadataError(`${field}.points`, "must be an array");
|
|
7126
|
+
async function sha256FileAsync(path) {
|
|
7127
|
+
const hash = createHash4("sha256");
|
|
7128
|
+
for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
|
|
7129
|
+
hash.update(chunk);
|
|
6836
7130
|
}
|
|
6837
|
-
return
|
|
7131
|
+
return hash.digest("hex");
|
|
6838
7132
|
}
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
7133
|
+
|
|
7134
|
+
// src/services/render/stages/freezePlan.ts
|
|
7135
|
+
var COMPILED_INDEX_RELATIVE_PATH = "compiled/index.html";
|
|
7136
|
+
var HASH_EXCLUDED_PLAN_FILES = /* @__PURE__ */ new Set([
|
|
7137
|
+
"plan.json",
|
|
7138
|
+
"meta/encoder.json",
|
|
7139
|
+
COMPILED_INDEX_RELATIVE_PATH
|
|
7140
|
+
]);
|
|
7141
|
+
function planFileSha256(absolutePath, digests) {
|
|
7142
|
+
let sha256 = digests?.get(absolutePath);
|
|
7143
|
+
if (sha256 === void 0) {
|
|
7144
|
+
sha256 = sha256File(absolutePath);
|
|
7145
|
+
digests?.set(absolutePath, sha256);
|
|
6849
7146
|
}
|
|
6850
|
-
return
|
|
6851
|
-
durationSeconds: readFiniteNumber(record.durationSeconds, `${field}.durationSeconds`),
|
|
6852
|
-
videoStreamDurationSeconds: readFiniteNumber(
|
|
6853
|
-
record.videoStreamDurationSeconds,
|
|
6854
|
-
`${field}.videoStreamDurationSeconds`
|
|
6855
|
-
),
|
|
6856
|
-
// Plans written before stream-start metadata existed implicitly used the
|
|
6857
|
-
// overwhelmingly common start-at-zero domain. Preserve that compatibility
|
|
6858
|
-
// while carrying non-zero edit-list/transport timestamps in new plans.
|
|
6859
|
-
videoStreamStartSeconds: record.videoStreamStartSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamStartSeconds, `${field}.videoStreamStartSeconds`),
|
|
6860
|
-
videoStreamLeadSeconds: record.videoStreamLeadSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamLeadSeconds, `${field}.videoStreamLeadSeconds`),
|
|
6861
|
-
width: readPositiveInteger(record.width, `${field}.width`),
|
|
6862
|
-
height: readPositiveInteger(record.height, `${field}.height`),
|
|
6863
|
-
fps: readFiniteNumber(record.fps, `${field}.fps`),
|
|
6864
|
-
videoCodec: readNonEmptyString(record.videoCodec, `${field}.videoCodec`),
|
|
6865
|
-
hasAudio: readBoolean(record.hasAudio, `${field}.hasAudio`),
|
|
6866
|
-
isVFR: readBoolean(record.isVFR, `${field}.isVFR`),
|
|
6867
|
-
hasAlpha: readBoolean(record.hasAlpha, `${field}.hasAlpha`),
|
|
6868
|
-
colorSpace
|
|
6869
|
-
};
|
|
7147
|
+
return sha256;
|
|
6870
7148
|
}
|
|
6871
|
-
function
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
}
|
|
6876
|
-
const videos = record.videos.map((value2, index) => {
|
|
6877
|
-
const field = `meta/videos.json.videos[${index}]`;
|
|
6878
|
-
const video = readRecord(value2, field);
|
|
6879
|
-
return {
|
|
6880
|
-
id: readNonEmptyString(video.id, `${field}.id`),
|
|
6881
|
-
src: readNonEmptyString(video.src, `${field}.src`),
|
|
6882
|
-
start: readFiniteNumber(video.start, `${field}.start`),
|
|
6883
|
-
end: readFiniteNumber(video.end, `${field}.end`),
|
|
6884
|
-
mediaStart: readFiniteNumber(video.mediaStart, `${field}.mediaStart`),
|
|
6885
|
-
playbackRate: readRateSpec(video.playbackRate, `${field}.playbackRate`),
|
|
6886
|
-
loop: readBoolean(video.loop, `${field}.loop`),
|
|
6887
|
-
hasAudio: readBoolean(video.hasAudio, `${field}.hasAudio`)
|
|
6888
|
-
};
|
|
6889
|
-
});
|
|
6890
|
-
const extracted = record.extracted.map((value2, index) => {
|
|
6891
|
-
const field = `meta/videos.json.extracted[${index}]`;
|
|
6892
|
-
const entry = readRecord(value2, field);
|
|
6893
|
-
return {
|
|
6894
|
-
videoId: readNonEmptyString(entry.videoId, `${field}.videoId`),
|
|
6895
|
-
srcPath: readNonEmptyString(entry.srcPath, `${field}.srcPath`),
|
|
6896
|
-
framePattern: readNonEmptyString(entry.framePattern, `${field}.framePattern`),
|
|
6897
|
-
fps: readFiniteNumber(entry.fps, `${field}.fps`),
|
|
6898
|
-
totalFrames: readNonNegativeInteger(entry.totalFrames, `${field}.totalFrames`),
|
|
6899
|
-
metadata: readVideoMetadata(entry.metadata, `${field}.metadata`),
|
|
6900
|
-
...entry.deferredRange === void 0 ? {} : { deferredRange: readDeferredFrameRange(entry.deferredRange, `${field}.deferredRange`) }
|
|
6901
|
-
};
|
|
6902
|
-
});
|
|
6903
|
-
const videoIds = /* @__PURE__ */ new Set();
|
|
6904
|
-
for (const video of videos) {
|
|
6905
|
-
if (videoIds.has(video.id)) {
|
|
6906
|
-
metadataError("meta/videos.json.videos", `contains duplicate id ${JSON.stringify(video.id)}`);
|
|
6907
|
-
}
|
|
6908
|
-
videoIds.add(video.id);
|
|
7149
|
+
async function prehashPlanFiles(dir, digests) {
|
|
7150
|
+
if (!existsSync13(dir)) return;
|
|
7151
|
+
for (const { absolutePath } of listPlanFiles(dir)) {
|
|
7152
|
+
if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
|
|
6909
7153
|
}
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
metadataError(
|
|
6921
|
-
"meta/videos.json.extracted",
|
|
6922
|
-
`references undeclared video ${JSON.stringify(entry.videoId)}`
|
|
6923
|
-
);
|
|
7154
|
+
}
|
|
7155
|
+
function stripUndefined(value) {
|
|
7156
|
+
if (Array.isArray(value)) return value.map(stripUndefined);
|
|
7157
|
+
if (value !== null && typeof value === "object") {
|
|
7158
|
+
const obj = value;
|
|
7159
|
+
const out = {};
|
|
7160
|
+
for (const key of Object.keys(obj)) {
|
|
7161
|
+
const v = obj[key];
|
|
7162
|
+
if (v === void 0) continue;
|
|
7163
|
+
out[key] = stripUndefined(v);
|
|
6924
7164
|
}
|
|
7165
|
+
return out;
|
|
6925
7166
|
}
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
7167
|
+
return value;
|
|
7168
|
+
}
|
|
7169
|
+
function listPlanFiles(planDir) {
|
|
7170
|
+
const results = [];
|
|
7171
|
+
const rootResolved = resolve7(planDir);
|
|
7172
|
+
function walk(dir) {
|
|
7173
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
7174
|
+
for (const entry of entries) {
|
|
7175
|
+
const full = join18(dir, entry.name);
|
|
7176
|
+
if (entry.isDirectory()) {
|
|
7177
|
+
walk(full);
|
|
7178
|
+
} else if (entry.isFile()) {
|
|
7179
|
+
results.push({
|
|
7180
|
+
planRelativePath: relative3(rootResolved, full).split(/[\\/]+/).join("/"),
|
|
7181
|
+
absolutePath: full
|
|
7182
|
+
});
|
|
7183
|
+
}
|
|
6932
7184
|
}
|
|
6933
7185
|
}
|
|
6934
|
-
|
|
7186
|
+
walk(rootResolved);
|
|
7187
|
+
results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
|
|
7188
|
+
return results;
|
|
6935
7189
|
}
|
|
6936
|
-
function
|
|
6937
|
-
const
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
7190
|
+
function collectPlanAssetShas(planDir, digests) {
|
|
7191
|
+
const files = listPlanFiles(planDir);
|
|
7192
|
+
let compositionHtml = null;
|
|
7193
|
+
const assets = [];
|
|
7194
|
+
for (const file of files) {
|
|
7195
|
+
if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
|
|
7196
|
+
compositionHtml = readFileSync8(file.absolutePath);
|
|
7197
|
+
continue;
|
|
6944
7198
|
}
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
if (cached !== void 0) return cached;
|
|
6955
|
-
let stdout;
|
|
6956
|
-
try {
|
|
6957
|
-
({ stdout } = await execFile(binary, ["-version"], {
|
|
6958
|
-
maxBuffer: 1024 * 1024,
|
|
6959
|
-
// See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
|
|
6960
|
-
windowsHide: true
|
|
6961
|
-
}));
|
|
6962
|
-
} catch (error) {
|
|
6963
|
-
if (error.code !== "ENOENT") throw error;
|
|
6964
|
-
throw Object.assign(
|
|
6965
|
-
new Error(`ffmpeg not found at "${binary}": install FFmpeg or set HYPERFRAMES_FFMPEG_PATH.`, {
|
|
6966
|
-
cause: error
|
|
6967
|
-
}),
|
|
6968
|
-
{ code: "ENOENT" }
|
|
7199
|
+
if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
|
|
7200
|
+
assets.push({
|
|
7201
|
+
path: file.planRelativePath,
|
|
7202
|
+
sha256: planFileSha256(file.absolutePath, digests)
|
|
7203
|
+
});
|
|
7204
|
+
}
|
|
7205
|
+
if (compositionHtml === null) {
|
|
7206
|
+
throw new Error(
|
|
7207
|
+
`[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH} (upstream compile stage did not materialize the expected file).`
|
|
6969
7208
|
);
|
|
6970
7209
|
}
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
7210
|
+
return { compositionHtml, assets };
|
|
7211
|
+
}
|
|
7212
|
+
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
7213
|
+
const planJsonPath = join18(planDir, "plan.json");
|
|
7214
|
+
const encoderJsonPath = join18(planDir, "meta", "encoder.json");
|
|
7215
|
+
if (!existsSync13(planJsonPath)) {
|
|
7216
|
+
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
6974
7217
|
}
|
|
6975
|
-
|
|
6976
|
-
|
|
7218
|
+
if (!existsSync13(encoderJsonPath)) {
|
|
7219
|
+
throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
|
|
7220
|
+
}
|
|
7221
|
+
const planJson = JSON.parse(readFileSync8(planJsonPath, "utf-8"));
|
|
7222
|
+
const encoderConfigCanonicalJson = readFileSync8(encoderJsonPath, "utf-8");
|
|
7223
|
+
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
7224
|
+
return computePlanHash({
|
|
7225
|
+
compositionHtml,
|
|
7226
|
+
assets,
|
|
7227
|
+
fontSnapshotSha: planJson.fontSnapshotSha,
|
|
7228
|
+
encoderConfigCanonicalJson,
|
|
7229
|
+
producerVersion: planJson.producerVersion,
|
|
7230
|
+
ffmpegVersion: planJson.ffmpegVersion,
|
|
7231
|
+
dimensions: planJson.dimensions
|
|
7232
|
+
});
|
|
6977
7233
|
}
|
|
6978
|
-
function
|
|
6979
|
-
const
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
7234
|
+
async function freezePlan(input) {
|
|
7235
|
+
const {
|
|
7236
|
+
planDir,
|
|
7237
|
+
composition,
|
|
7238
|
+
encoder,
|
|
7239
|
+
chunks,
|
|
7240
|
+
dimensions,
|
|
7241
|
+
producerVersion,
|
|
7242
|
+
fontSnapshotSha,
|
|
7243
|
+
durationSeconds,
|
|
7244
|
+
totalFrames,
|
|
7245
|
+
hasAudio,
|
|
7246
|
+
digests
|
|
7247
|
+
} = input;
|
|
7248
|
+
if (!existsSync13(planDir)) {
|
|
7249
|
+
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
7250
|
+
}
|
|
7251
|
+
const metaDir = join18(planDir, "meta");
|
|
7252
|
+
if (!existsSync13(metaDir)) mkdirSync7(metaDir, { recursive: true });
|
|
7253
|
+
writeFileSync4(
|
|
7254
|
+
join18(metaDir, "composition.json"),
|
|
7255
|
+
`${JSON.stringify(composition, null, 2)}
|
|
7256
|
+
`,
|
|
7257
|
+
"utf-8"
|
|
7258
|
+
);
|
|
7259
|
+
const encoderForCanonical = stripUndefined(encoder);
|
|
7260
|
+
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
7261
|
+
writeFileSync4(join18(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
7262
|
+
writeFileSync4(join18(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
7263
|
+
`, "utf-8");
|
|
7264
|
+
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
7265
|
+
const planHash = computePlanHash({
|
|
7266
|
+
compositionHtml,
|
|
7267
|
+
assets,
|
|
7268
|
+
fontSnapshotSha,
|
|
7269
|
+
encoderConfigCanonicalJson,
|
|
7270
|
+
producerVersion,
|
|
7271
|
+
ffmpegVersion: encoder.ffmpegVersion,
|
|
7272
|
+
dimensions
|
|
7273
|
+
});
|
|
7274
|
+
const planJson = {
|
|
7275
|
+
protocol: PLAN_PROTOCOL_V1,
|
|
7276
|
+
planHash,
|
|
7277
|
+
producerVersion,
|
|
7278
|
+
ffmpegVersion: encoder.ffmpegVersion,
|
|
7279
|
+
fontSnapshotSha,
|
|
7280
|
+
dimensions,
|
|
7281
|
+
chunkCount: chunks.length,
|
|
7282
|
+
totalFrames,
|
|
7283
|
+
duration: durationSeconds,
|
|
7284
|
+
hasAudio
|
|
6998
7285
|
};
|
|
6999
|
-
|
|
7286
|
+
const planJsonPath = join18(planDir, "plan.json");
|
|
7287
|
+
writeFileSync4(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
7288
|
+
`, "utf-8");
|
|
7289
|
+
return { planJsonPath, planHash };
|
|
7000
7290
|
}
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7291
|
+
|
|
7292
|
+
// src/services/render/runtimeEnvSnapshot.ts
|
|
7293
|
+
var RUNTIME_ENV_PREFIXES = [
|
|
7294
|
+
"PRODUCER_RUNTIME_",
|
|
7295
|
+
"PRODUCER_RENDER_"
|
|
7296
|
+
];
|
|
7297
|
+
function snapshotRuntimeEnv(env = process.env) {
|
|
7298
|
+
const snapshot = {};
|
|
7299
|
+
for (const key of Object.keys(env)) {
|
|
7300
|
+
const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
7301
|
+
if (!matches) continue;
|
|
7302
|
+
const value = env[key];
|
|
7303
|
+
if (typeof value !== "string") continue;
|
|
7304
|
+
snapshot[key] = value;
|
|
7305
|
+
}
|
|
7306
|
+
return snapshot;
|
|
7307
|
+
}
|
|
7308
|
+
function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
7309
|
+
const previous = {};
|
|
7310
|
+
for (const [key, value] of Object.entries(snapshot)) {
|
|
7311
|
+
if (typeof value !== "string") continue;
|
|
7312
|
+
const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
7313
|
+
if (!matches) continue;
|
|
7314
|
+
previous[key] = env[key];
|
|
7315
|
+
env[key] = value;
|
|
7316
|
+
}
|
|
7317
|
+
return {
|
|
7318
|
+
restore: () => {
|
|
7319
|
+
for (const [key, value] of Object.entries(previous)) {
|
|
7320
|
+
if (value === void 0) delete env[key];
|
|
7321
|
+
else env[key] = value;
|
|
7016
7322
|
}
|
|
7017
7323
|
}
|
|
7018
|
-
|
|
7019
|
-
if (parent === current) break;
|
|
7020
|
-
current = parent;
|
|
7021
|
-
}
|
|
7022
|
-
cachedProducerVersion = "0.0.0-unknown";
|
|
7023
|
-
return cachedProducerVersion;
|
|
7324
|
+
};
|
|
7024
7325
|
}
|
|
7025
7326
|
|
|
7026
7327
|
// src/services/distributed/planSize.ts
|
|
7027
7328
|
import { createHash as createHash5 } from "node:crypto";
|
|
7028
|
-
import { lstatSync, readdirSync as
|
|
7029
|
-
import { extname as
|
|
7329
|
+
import { lstatSync, readdirSync as readdirSync6 } from "node:fs";
|
|
7330
|
+
import { extname as extname3, join as join19, relative as relative4 } from "node:path";
|
|
7030
7331
|
var MAX_TOP_COMPONENTS = 10;
|
|
7031
7332
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7032
7333
|
".aac",
|
|
@@ -7086,7 +7387,7 @@ function emptyBreakdown() {
|
|
|
7086
7387
|
function addCategoryBytes(result, category, filePath, sizeBytes) {
|
|
7087
7388
|
if (category === "compiled") {
|
|
7088
7389
|
result.compiledBytes += sizeBytes;
|
|
7089
|
-
if (SOURCE_MEDIA_EXTENSIONS.has(
|
|
7390
|
+
if (SOURCE_MEDIA_EXTENSIONS.has(extname3(filePath).toLowerCase())) {
|
|
7090
7391
|
result.sourceMediaBytes += sizeBytes;
|
|
7091
7392
|
}
|
|
7092
7393
|
return;
|
|
@@ -7133,12 +7434,12 @@ function recordFile(result, components, rootDir, rootKind, filePath) {
|
|
|
7133
7434
|
function walkRegularFiles(dir, visit) {
|
|
7134
7435
|
let entries;
|
|
7135
7436
|
try {
|
|
7136
|
-
entries =
|
|
7437
|
+
entries = readdirSync6(dir, { withFileTypes: true });
|
|
7137
7438
|
} catch {
|
|
7138
7439
|
return;
|
|
7139
7440
|
}
|
|
7140
7441
|
for (const entry of entries) {
|
|
7141
|
-
const full =
|
|
7442
|
+
const full = join19(dir, entry.name);
|
|
7142
7443
|
if (entry.isDirectory()) {
|
|
7143
7444
|
walkRegularFiles(full, visit);
|
|
7144
7445
|
} else if (entry.isFile()) {
|
|
@@ -7200,8 +7501,17 @@ var MAX_PARALLEL_CHUNKS_CEILING = 256;
|
|
|
7200
7501
|
var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
|
|
7201
7502
|
var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
|
|
7202
7503
|
var WORKER_TIMEOUT_MARGIN = 0.75;
|
|
7203
|
-
function
|
|
7204
|
-
return
|
|
7504
|
+
function estimateFrameSeconds(megapixels, measuredSecondsPerFrame = 0) {
|
|
7505
|
+
return Math.max(ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels, measuredSecondsPerFrame);
|
|
7506
|
+
}
|
|
7507
|
+
function estimateChunkSeconds(frames, megapixels, measuredSecondsPerFrame) {
|
|
7508
|
+
return ESTIMATED_CHUNK_OVERHEAD_SECONDS + frames * estimateFrameSeconds(megapixels, measuredSecondsPerFrame);
|
|
7509
|
+
}
|
|
7510
|
+
function planNeedsFrameCostMeasurement(html) {
|
|
7511
|
+
return new RegExp(`\\b${HF_COLOR_GRADING_ATTR2}=`, "i").test(html);
|
|
7512
|
+
}
|
|
7513
|
+
function chunkSizingUsesFrameCost(config) {
|
|
7514
|
+
return config.maxParallelChunks === void 0 && config.chunkSize === void 0 && config.targetChunkFrames === void 0 && (config.workerTimeoutSeconds !== void 0 || config.workerConcurrency !== void 0);
|
|
7205
7515
|
}
|
|
7206
7516
|
var CHUNK_EXCEEDS_WORKER_TIMEOUT = "CHUNK_EXCEEDS_WORKER_TIMEOUT";
|
|
7207
7517
|
var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
@@ -7214,18 +7524,15 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
|
7214
7524
|
this.name = "ChunkExceedsWorkerTimeoutError";
|
|
7215
7525
|
}
|
|
7216
7526
|
};
|
|
7217
|
-
function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger) {
|
|
7527
|
+
function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
|
|
7218
7528
|
if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
|
|
7219
7529
|
const { workerTimeoutSeconds, workerConcurrency } = config;
|
|
7220
7530
|
const wave = Math.min(
|
|
7221
7531
|
workerConcurrency ?? DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
7222
7532
|
MAX_PARALLEL_CHUNKS_CEILING
|
|
7223
7533
|
);
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
return wave;
|
|
7227
|
-
}
|
|
7228
|
-
const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
|
|
7534
|
+
if (!chunkSizingUsesFrameCost(config)) return wave;
|
|
7535
|
+
const secondsPerFrame = estimateFrameSeconds(megapixels, measuredSecondsPerFrame);
|
|
7229
7536
|
const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
|
|
7230
7537
|
const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
|
|
7231
7538
|
if (workerTimeoutSeconds === void 0) return wide;
|
|
@@ -7236,7 +7543,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
|
|
|
7236
7543
|
if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
|
|
7237
7544
|
const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
|
|
7238
7545
|
throw new ChunkExceedsWorkerTimeoutError(
|
|
7239
|
-
Math.ceil(estimateChunkSeconds(framesAtCeiling, megapixels)),
|
|
7546
|
+
Math.ceil(estimateChunkSeconds(framesAtCeiling, megapixels, measuredSecondsPerFrame)),
|
|
7240
7547
|
workerTimeoutSeconds
|
|
7241
7548
|
);
|
|
7242
7549
|
}
|
|
@@ -7346,13 +7653,13 @@ function assertPositiveInteger(name, value) {
|
|
|
7346
7653
|
}
|
|
7347
7654
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
7348
7655
|
"plan.json",
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7656
|
+
join20("meta", "composition.json"),
|
|
7657
|
+
join20("meta", "encoder.json"),
|
|
7658
|
+
join20("meta", "chunks.json")
|
|
7352
7659
|
];
|
|
7353
7660
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
7354
7661
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
7355
|
-
rmSync7(
|
|
7662
|
+
rmSync7(join20(planDir, relativePath), { force: true });
|
|
7356
7663
|
}
|
|
7357
7664
|
}
|
|
7358
7665
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -7483,19 +7790,19 @@ function createVideoSourceShipper(input) {
|
|
|
7483
7790
|
const known = shipped.get(srcPath);
|
|
7484
7791
|
if (known) return known;
|
|
7485
7792
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
7486
|
-
const current = stagedRel === null ? srcPath :
|
|
7793
|
+
const current = stagedRel === null ? srcPath : join20(input.planDir, "video-frames", stagedRel);
|
|
7487
7794
|
let result = null;
|
|
7488
7795
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
7489
7796
|
const rel = pathInside(root, srcPath);
|
|
7490
|
-
if (rel !== null &&
|
|
7797
|
+
if (rel !== null && existsSync14(join20(input.planDir, "compiled", rel))) {
|
|
7491
7798
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
7492
7799
|
break;
|
|
7493
7800
|
}
|
|
7494
7801
|
}
|
|
7495
7802
|
if (result === null) {
|
|
7496
|
-
result = `video-sources/${shipped.size}${
|
|
7497
|
-
const destination =
|
|
7498
|
-
mkdirSync8(
|
|
7803
|
+
result = `video-sources/${shipped.size}${extname4(current)}`;
|
|
7804
|
+
const destination = join20(input.planDir, ...result.split("/"));
|
|
7805
|
+
mkdirSync8(join20(input.planDir, "video-sources"), { recursive: true });
|
|
7499
7806
|
try {
|
|
7500
7807
|
linkSync(current, destination);
|
|
7501
7808
|
} catch {
|
|
@@ -7513,7 +7820,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7513
7820
|
useGpu: false,
|
|
7514
7821
|
browserGpuMode: "software"
|
|
7515
7822
|
});
|
|
7516
|
-
if (!
|
|
7823
|
+
if (!existsSync14(planDir)) mkdirSync8(planDir, { recursive: true });
|
|
7517
7824
|
const log = config.logger ?? defaultLogger;
|
|
7518
7825
|
const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
|
|
7519
7826
|
const abortSignal = config.abortSignal;
|
|
@@ -7542,14 +7849,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7542
7849
|
variables: config.variables
|
|
7543
7850
|
});
|
|
7544
7851
|
const entryFile = config.entryFile ?? "index.html";
|
|
7545
|
-
const htmlPath =
|
|
7546
|
-
if (!
|
|
7852
|
+
const htmlPath = join20(projectDir, entryFile);
|
|
7853
|
+
if (!existsSync14(htmlPath)) {
|
|
7547
7854
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
7548
7855
|
}
|
|
7549
|
-
const workDir =
|
|
7856
|
+
const workDir = join20(planDir, ".plan-work");
|
|
7550
7857
|
rmSync7(workDir, { recursive: true, force: true });
|
|
7551
7858
|
mkdirSync8(workDir, { recursive: true });
|
|
7552
|
-
const compiledDir =
|
|
7859
|
+
const compiledDir = join20(workDir, "compiled");
|
|
7553
7860
|
mkdirSync8(compiledDir, { recursive: true });
|
|
7554
7861
|
let stageStarted = Date.now();
|
|
7555
7862
|
cpSync2(projectDir, compiledDir, {
|
|
@@ -7563,7 +7870,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7563
7870
|
}
|
|
7564
7871
|
});
|
|
7565
7872
|
logPlanStage(log, "copy", stageStarted);
|
|
7566
|
-
const finalCompiledDir =
|
|
7873
|
+
const finalCompiledDir = join20(planDir, "compiled");
|
|
7567
7874
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
7568
7875
|
stageStarted = Date.now();
|
|
7569
7876
|
const compileResult = await runCompileStage({
|
|
@@ -7626,27 +7933,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7626
7933
|
if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
|
|
7627
7934
|
if (probeResult.probeSession) {
|
|
7628
7935
|
try {
|
|
7629
|
-
const { closeCaptureSession:
|
|
7630
|
-
await
|
|
7936
|
+
const { closeCaptureSession: closeCaptureSession11 } = await import("@hyperframes/engine");
|
|
7937
|
+
await closeCaptureSession11(probeResult.probeSession);
|
|
7631
7938
|
} catch (err) {
|
|
7632
7939
|
log.warn("[plan] probe session close failed", {
|
|
7633
7940
|
error: err instanceof Error ? err.message : String(err)
|
|
7634
7941
|
});
|
|
7635
7942
|
}
|
|
7636
7943
|
}
|
|
7637
|
-
const
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
compileResult.outputWidth * compileResult.outputHeight / 1e6,
|
|
7641
|
-
log
|
|
7642
|
-
);
|
|
7643
|
-
const { chunkCount, effectiveChunkSize } = resolveChunkPlan(
|
|
7644
|
-
totalFrames,
|
|
7645
|
-
config.chunkSize,
|
|
7646
|
-
maxParallel,
|
|
7647
|
-
config.targetChunkFrames
|
|
7648
|
-
);
|
|
7649
|
-
const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
|
|
7944
|
+
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
7945
|
+
let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
|
|
7946
|
+
const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
|
|
7650
7947
|
assertPlanSizeWithinLimit({
|
|
7651
7948
|
rootDir: compiledDir,
|
|
7652
7949
|
rootKind: "compiled",
|
|
@@ -7671,17 +7968,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7671
7968
|
if (extractResult.extractionResult) {
|
|
7672
7969
|
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
7673
7970
|
}
|
|
7674
|
-
const stagedVideoFrames =
|
|
7675
|
-
const videoFramesDst =
|
|
7676
|
-
if (
|
|
7677
|
-
const videoSourcesDir =
|
|
7971
|
+
const stagedVideoFrames = join20(compiledDir, "__hyperframes_video_frames");
|
|
7972
|
+
const videoFramesDst = join20(planDir, "video-frames");
|
|
7973
|
+
if (existsSync14(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
|
|
7974
|
+
const videoSourcesDir = join20(planDir, "video-sources");
|
|
7678
7975
|
rmSync7(videoSourcesDir, { recursive: true, force: true });
|
|
7679
|
-
if (
|
|
7976
|
+
if (existsSync14(stagedVideoFrames)) {
|
|
7680
7977
|
renameSync3(stagedVideoFrames, videoFramesDst);
|
|
7681
7978
|
} else {
|
|
7682
7979
|
mkdirSync8(videoFramesDst, { recursive: true });
|
|
7683
7980
|
}
|
|
7684
|
-
if (
|
|
7981
|
+
if (existsSync14(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
|
|
7685
7982
|
renameSync3(compiledDir, finalCompiledDir);
|
|
7686
7983
|
const shipVideoSource = createVideoSourceShipper({
|
|
7687
7984
|
planDir,
|
|
@@ -7707,9 +8004,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7707
8004
|
...ext.deferredRange ? { deferredRange: { ...ext.deferredRange, sourcePath: shipVideoSource(ext.srcPath) } } : {}
|
|
7708
8005
|
}))
|
|
7709
8006
|
});
|
|
7710
|
-
mkdirSync8(
|
|
8007
|
+
mkdirSync8(join20(planDir, "meta"), { recursive: true });
|
|
7711
8008
|
writeFileSync5(
|
|
7712
|
-
|
|
8009
|
+
join20(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
7713
8010
|
JSON.stringify(planVideosJson, null, 2),
|
|
7714
8011
|
"utf-8"
|
|
7715
8012
|
);
|
|
@@ -7717,7 +8014,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7717
8014
|
const chunksCarryAudio2 = carriesChunkAudio(config, options);
|
|
7718
8015
|
const audioSeconds = chunksCarryAudio2 ? totalFrames / config.fps : probeResult.duration;
|
|
7719
8016
|
stageStarted = Date.now();
|
|
7720
|
-
const [audioOutcome, overlapOutcome] = await Promise.allSettled([
|
|
8017
|
+
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
7721
8018
|
runAudioStage({
|
|
7722
8019
|
projectDir,
|
|
7723
8020
|
workDir,
|
|
@@ -7740,19 +8037,53 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
7740
8037
|
await options.whileAudioMixes();
|
|
7741
8038
|
logPlanStage(log, "prepublish", prepublishStarted);
|
|
7742
8039
|
}
|
|
7743
|
-
})()
|
|
8040
|
+
})(),
|
|
8041
|
+
measureFrameCost ? measurePlanSecondsPerFrame({
|
|
8042
|
+
planDir,
|
|
8043
|
+
workDir,
|
|
8044
|
+
html: compiled.html,
|
|
8045
|
+
planVideos: planVideosJson,
|
|
8046
|
+
totalFrames,
|
|
8047
|
+
fps: config.fps,
|
|
8048
|
+
width,
|
|
8049
|
+
height,
|
|
8050
|
+
format: config.format,
|
|
8051
|
+
quality: config.quality ?? "standard",
|
|
8052
|
+
deviceScaleFactor,
|
|
8053
|
+
variables: config.variables,
|
|
8054
|
+
cfg: { ...cfg, forceScreenshot, forceScreenshotExplicitlyOptedOut: !forceScreenshot },
|
|
8055
|
+
log
|
|
8056
|
+
}).finally(() => logPlanStage(log, "frame-cost", stageStarted)) : void 0
|
|
7744
8057
|
]);
|
|
7745
8058
|
if (audioOutcome.status === "rejected") throw audioOutcome.reason;
|
|
7746
8059
|
if (overlapOutcome.status === "rejected") throw overlapOutcome.reason;
|
|
8060
|
+
if (frameCostOutcome.status === "rejected") throw frameCostOutcome.reason;
|
|
8061
|
+
if (frameCostOutcome.value !== void 0) {
|
|
8062
|
+
log.info("[plan] measured frame cost", { secondsPerFrame: frameCostOutcome.value });
|
|
8063
|
+
maxParallel = resolveMaxParallelChunks(
|
|
8064
|
+
config,
|
|
8065
|
+
totalFrames,
|
|
8066
|
+
megapixels,
|
|
8067
|
+
log,
|
|
8068
|
+
frameCostOutcome.value
|
|
8069
|
+
);
|
|
8070
|
+
}
|
|
8071
|
+
const { chunkCount, effectiveChunkSize } = resolveChunkPlan(
|
|
8072
|
+
totalFrames,
|
|
8073
|
+
config.chunkSize,
|
|
8074
|
+
maxParallel,
|
|
8075
|
+
config.targetChunkFrames
|
|
8076
|
+
);
|
|
8077
|
+
const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
|
|
7747
8078
|
const audioResult = audioOutcome.value;
|
|
7748
8079
|
if (audioResult.audioError) {
|
|
7749
8080
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
7750
8081
|
}
|
|
7751
|
-
const planAudioPath =
|
|
7752
|
-
if (audioResult.hasAudio &&
|
|
8082
|
+
const planAudioPath = join20(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
8083
|
+
if (audioResult.hasAudio && existsSync14(audioResult.audioOutputPath)) {
|
|
7753
8084
|
renameSync3(audioResult.audioOutputPath, planAudioPath);
|
|
7754
8085
|
}
|
|
7755
|
-
if (chunksCarryAudio2 &&
|
|
8086
|
+
if (chunksCarryAudio2 && existsSync14(planAudioPath)) {
|
|
7756
8087
|
stageStarted = Date.now();
|
|
7757
8088
|
const peak = await constrainAacTruePeak({
|
|
7758
8089
|
audioPath: planAudioPath,
|
|
@@ -7866,26 +8197,26 @@ async function plan(projectDir, config, planDir, options = {}) {
|
|
|
7866
8197
|
// src/services/distributed/planV2.ts
|
|
7867
8198
|
import {
|
|
7868
8199
|
copyFileSync as copyFileSync7,
|
|
7869
|
-
existsSync as
|
|
8200
|
+
existsSync as existsSync16,
|
|
7870
8201
|
lstatSync as lstatSync2,
|
|
7871
8202
|
mkdirSync as mkdirSync10,
|
|
7872
8203
|
mkdtempSync as mkdtempSync4,
|
|
7873
8204
|
readFileSync as readFileSync9,
|
|
7874
|
-
readdirSync as
|
|
8205
|
+
readdirSync as readdirSync7,
|
|
7875
8206
|
renameSync as renameSync5,
|
|
7876
8207
|
rmSync as rmSync9,
|
|
7877
8208
|
statSync as statSync6,
|
|
7878
8209
|
writeFileSync as writeFileSync7
|
|
7879
8210
|
} from "node:fs";
|
|
7880
8211
|
import { tmpdir } from "node:os";
|
|
7881
|
-
import { dirname as dirname10, isAbsolute as isAbsolute4, join as
|
|
8212
|
+
import { dirname as dirname10, isAbsolute as isAbsolute4, join as join23, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
|
|
7882
8213
|
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
7883
|
-
import { createFrameLookupTable as
|
|
8214
|
+
import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
|
|
7884
8215
|
|
|
7885
8216
|
// src/services/distributed/planV2Publisher.ts
|
|
7886
8217
|
import {
|
|
7887
8218
|
copyFileSync as copyFileSync6,
|
|
7888
|
-
existsSync as
|
|
8219
|
+
existsSync as existsSync15,
|
|
7889
8220
|
linkSync as linkSync2,
|
|
7890
8221
|
mkdirSync as mkdirSync9,
|
|
7891
8222
|
mkdtempSync as mkdtempSync3,
|
|
@@ -7894,7 +8225,7 @@ import {
|
|
|
7894
8225
|
statSync as statSync5,
|
|
7895
8226
|
writeFileSync as writeFileSync6
|
|
7896
8227
|
} from "node:fs";
|
|
7897
|
-
import { dirname as dirname9, join as
|
|
8228
|
+
import { dirname as dirname9, join as join22 } from "node:path";
|
|
7898
8229
|
|
|
7899
8230
|
// src/services/distributed/planV2Errors.ts
|
|
7900
8231
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -7909,7 +8240,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
7909
8240
|
};
|
|
7910
8241
|
|
|
7911
8242
|
// src/services/distributed/planV2Layout.ts
|
|
7912
|
-
import { join as
|
|
8243
|
+
import { join as join21 } from "node:path";
|
|
7913
8244
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
7914
8245
|
function assertPlanV2Sha256(value, field) {
|
|
7915
8246
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -7919,7 +8250,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
7919
8250
|
}
|
|
7920
8251
|
function planV2BlobPath(root, sha256) {
|
|
7921
8252
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
7922
|
-
return
|
|
8253
|
+
return join21(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
7923
8254
|
}
|
|
7924
8255
|
|
|
7925
8256
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -7954,13 +8285,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
7954
8285
|
#linkFile;
|
|
7955
8286
|
#committed = false;
|
|
7956
8287
|
constructor(destinationDir, options = {}) {
|
|
7957
|
-
if (
|
|
8288
|
+
if (existsSync15(destinationDir)) {
|
|
7958
8289
|
throw new PlanV2IntegrityError(`output directory already exists: ${destinationDir}`);
|
|
7959
8290
|
}
|
|
7960
8291
|
this.destinationDir = destinationDir;
|
|
7961
8292
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
7962
8293
|
mkdirSync9(dirname9(destinationDir), { recursive: true });
|
|
7963
|
-
this.temporaryDir = mkdtempSync3(
|
|
8294
|
+
this.temporaryDir = mkdtempSync3(join22(dirname9(destinationDir), ".plan-v2-publish-"));
|
|
7964
8295
|
}
|
|
7965
8296
|
async putBlob(blob) {
|
|
7966
8297
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
@@ -7971,10 +8302,10 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
7971
8302
|
);
|
|
7972
8303
|
}
|
|
7973
8304
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
7974
|
-
if (
|
|
8305
|
+
if (existsSync15(destinationPath)) return;
|
|
7975
8306
|
mkdirSync9(dirname9(destinationPath), { recursive: true });
|
|
7976
|
-
const stagingDir = mkdtempSync3(
|
|
7977
|
-
const temporaryPath =
|
|
8307
|
+
const stagingDir = mkdtempSync3(join22(dirname9(destinationPath), ".plan-v2-blob-"));
|
|
8308
|
+
const temporaryPath = join22(stagingDir, "blob");
|
|
7978
8309
|
try {
|
|
7979
8310
|
try {
|
|
7980
8311
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
@@ -7989,13 +8320,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
7989
8320
|
}
|
|
7990
8321
|
async commitManifest(manifestBytes) {
|
|
7991
8322
|
for (const digest of new Set(manifestDigests(manifestBytes))) {
|
|
7992
|
-
if (!
|
|
8323
|
+
if (!existsSync15(planV2BlobPath(this.temporaryDir, digest))) {
|
|
7993
8324
|
throw new PlanV2IntegrityError(
|
|
7994
8325
|
`cannot commit manifest before referenced blob is durable: ${digest}`
|
|
7995
8326
|
);
|
|
7996
8327
|
}
|
|
7997
8328
|
}
|
|
7998
|
-
writeFileSync6(
|
|
8329
|
+
writeFileSync6(join22(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
7999
8330
|
renameSync4(this.temporaryDir, this.destinationDir);
|
|
8000
8331
|
this.#committed = true;
|
|
8001
8332
|
}
|
|
@@ -8039,8 +8370,8 @@ function listFiles(root) {
|
|
|
8039
8370
|
const files = [];
|
|
8040
8371
|
const rootResolved = resolve8(root);
|
|
8041
8372
|
function walk(dir) {
|
|
8042
|
-
for (const entry of
|
|
8043
|
-
const absolutePath =
|
|
8373
|
+
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
8374
|
+
const absolutePath = join23(dir, entry.name);
|
|
8044
8375
|
if (entry.isDirectory()) {
|
|
8045
8376
|
walk(absolutePath);
|
|
8046
8377
|
} else if (entry.isFile()) {
|
|
@@ -8067,13 +8398,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
8067
8398
|
}
|
|
8068
8399
|
}
|
|
8069
8400
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
8070
|
-
const videoRoot =
|
|
8071
|
-
if (!
|
|
8072
|
-
for (const videoEntry of
|
|
8401
|
+
const videoRoot = join23(executionPlanDir, "video-frames");
|
|
8402
|
+
if (!existsSync16(videoRoot)) return;
|
|
8403
|
+
for (const videoEntry of readdirSync7(videoRoot, { withFileTypes: true })) {
|
|
8073
8404
|
if (!videoEntry.isDirectory()) continue;
|
|
8074
|
-
const videoDir =
|
|
8075
|
-
if (
|
|
8076
|
-
const sentinelPath =
|
|
8405
|
+
const videoDir = join23(videoRoot, videoEntry.name);
|
|
8406
|
+
if (readdirSync7(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
8407
|
+
const sentinelPath = join23(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
8077
8408
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
8078
8409
|
}
|
|
8079
8410
|
}
|
|
@@ -8114,15 +8445,15 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
8114
8445
|
if (video.deferredRange) {
|
|
8115
8446
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
8116
8447
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
8117
|
-
framePaths2.set(index,
|
|
8448
|
+
framePaths2.set(index, join23(outputDir, String(index)));
|
|
8118
8449
|
}
|
|
8119
8450
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
8120
8451
|
}
|
|
8121
|
-
const frameNames =
|
|
8452
|
+
const frameNames = readdirSync7(outputDir).sort();
|
|
8122
8453
|
const framePaths = /* @__PURE__ */ new Map();
|
|
8123
8454
|
for (const frameName of frameNames) {
|
|
8124
8455
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
8125
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
8456
|
+
assertValidExtractionCacheCompleteSentinel(join23(outputDir, frameName));
|
|
8126
8457
|
continue;
|
|
8127
8458
|
}
|
|
8128
8459
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -8141,7 +8472,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
8141
8472
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
8142
8473
|
);
|
|
8143
8474
|
}
|
|
8144
|
-
framePaths.set(frameIndex,
|
|
8475
|
+
framePaths.set(frameIndex, join23(outputDir, frameName));
|
|
8145
8476
|
}
|
|
8146
8477
|
return {
|
|
8147
8478
|
...video,
|
|
@@ -8162,8 +8493,8 @@ function parsePlanVideosJson2(value) {
|
|
|
8162
8493
|
}
|
|
8163
8494
|
}
|
|
8164
8495
|
function materializeExtractedVideoDirectories(planDir) {
|
|
8165
|
-
const videosPath =
|
|
8166
|
-
if (!
|
|
8496
|
+
const videosPath = join23(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
8497
|
+
if (!existsSync16(videosPath)) return;
|
|
8167
8498
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
8168
8499
|
for (const video of videos.extracted) {
|
|
8169
8500
|
mkdirSync10(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
|
|
@@ -8191,13 +8522,13 @@ function parseChunkSlices(value) {
|
|
|
8191
8522
|
});
|
|
8192
8523
|
}
|
|
8193
8524
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
8194
|
-
const videoRoot =
|
|
8195
|
-
const hasExtractedFrames =
|
|
8196
|
-
const videosPath =
|
|
8197
|
-
if (!
|
|
8525
|
+
const videoRoot = join23(executionPlanDir, "video-frames");
|
|
8526
|
+
const hasExtractedFrames = existsSync16(videoRoot) && listFiles(videoRoot).length > 0;
|
|
8527
|
+
const videosPath = join23(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
8528
|
+
if (!existsSync16(videosPath)) {
|
|
8198
8529
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
8199
8530
|
}
|
|
8200
|
-
const chunksPath =
|
|
8531
|
+
const chunksPath = join23(executionPlanDir, "meta", "chunks.json");
|
|
8201
8532
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
8202
8533
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
8203
8534
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -8205,7 +8536,7 @@ function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
|
8205
8536
|
const extracted = listVideoFramePaths(executionPlanDir, parsedVideos);
|
|
8206
8537
|
const fpsNum = readPositiveInteger2(dimensions.fpsNum, "dimensions.fpsNum");
|
|
8207
8538
|
const fpsDen = readPositiveInteger2(dimensions.fpsDen, "dimensions.fpsDen");
|
|
8208
|
-
const table =
|
|
8539
|
+
const table = createFrameLookupTable4(
|
|
8209
8540
|
parsedVideos.videos,
|
|
8210
8541
|
extracted,
|
|
8211
8542
|
resolveRenderFpsConfig({ num: fpsNum, den: fpsDen }).value
|
|
@@ -8251,10 +8582,10 @@ function computeManifestHash(manifest) {
|
|
|
8251
8582
|
return sha256Hex(`${PLAN_V2_HASH_PREFIX}${canonicalJsonStringify(manifestPayload(manifest))}`);
|
|
8252
8583
|
}
|
|
8253
8584
|
function writeBlob(sourcePath, destinationPath) {
|
|
8254
|
-
if (
|
|
8585
|
+
if (existsSync16(destinationPath)) return;
|
|
8255
8586
|
mkdirSync10(dirname10(destinationPath), { recursive: true });
|
|
8256
|
-
const temporaryDir = mkdtempSync4(
|
|
8257
|
-
const temporaryPath =
|
|
8587
|
+
const temporaryDir = mkdtempSync4(join23(dirname10(destinationPath), ".plan-v2-blob-"));
|
|
8588
|
+
const temporaryPath = join23(temporaryDir, "blob");
|
|
8258
8589
|
try {
|
|
8259
8590
|
copyFileSync7(sourcePath, temporaryPath);
|
|
8260
8591
|
renameSync5(temporaryPath, destinationPath);
|
|
@@ -8287,8 +8618,8 @@ async function putBlobs(publisher, blobs) {
|
|
|
8287
8618
|
if (failure) throw failure.reason;
|
|
8288
8619
|
}
|
|
8289
8620
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
8290
|
-
const executionPlanPath =
|
|
8291
|
-
if (!
|
|
8621
|
+
const executionPlanPath = join23(executionPlanDir, "plan.json");
|
|
8622
|
+
if (!existsSync16(executionPlanPath)) {
|
|
8292
8623
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
8293
8624
|
}
|
|
8294
8625
|
const executionPlanValue = readJsonFile(executionPlanPath, "execution plan.json");
|
|
@@ -8315,7 +8646,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
8315
8646
|
}
|
|
8316
8647
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
8317
8648
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
8318
|
-
const encoder = readJsonFile(
|
|
8649
|
+
const encoder = readJsonFile(join23(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
8319
8650
|
const chunksCarryAudio2 = isRecord3(encoder) && encoder.chunksCarryAudio === true;
|
|
8320
8651
|
for (const file of listFiles(executionPlanDir)) {
|
|
8321
8652
|
if (isExtractionCacheCompleteSentinelPath(file.path)) continue;
|
|
@@ -8357,18 +8688,18 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
8357
8688
|
};
|
|
8358
8689
|
}
|
|
8359
8690
|
function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
8360
|
-
if (
|
|
8691
|
+
if (existsSync16(planV2Dir)) {
|
|
8361
8692
|
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
8362
8693
|
}
|
|
8363
8694
|
const publication = buildPlanV2Publication(executionPlanDir);
|
|
8364
8695
|
mkdirSync10(dirname10(planV2Dir), { recursive: true });
|
|
8365
|
-
const tempDir = mkdtempSync4(
|
|
8696
|
+
const tempDir = mkdtempSync4(join23(dirname10(planV2Dir), ".plan-v2-build-"));
|
|
8366
8697
|
try {
|
|
8367
8698
|
for (const blob of publication.blobs) {
|
|
8368
8699
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
8369
8700
|
}
|
|
8370
8701
|
writeFileSync7(
|
|
8371
|
-
|
|
8702
|
+
join23(tempDir, "plan.json"),
|
|
8372
8703
|
canonicalJsonStringify(publication.manifest),
|
|
8373
8704
|
"utf-8"
|
|
8374
8705
|
);
|
|
@@ -8409,7 +8740,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
|
8409
8740
|
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
8410
8741
|
}
|
|
8411
8742
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
8412
|
-
const stagingRoot = mkdtempSync4(
|
|
8743
|
+
const stagingRoot = mkdtempSync4(join23(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
|
|
8413
8744
|
const digests = /* @__PURE__ */ new Map();
|
|
8414
8745
|
try {
|
|
8415
8746
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -8419,7 +8750,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
8419
8750
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
8420
8751
|
whileAudioMixes: () => putBlobs(
|
|
8421
8752
|
publisher,
|
|
8422
|
-
listFiles(
|
|
8753
|
+
listFiles(join23(stagingRoot, "compiled")).map(
|
|
8423
8754
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
8424
8755
|
)
|
|
8425
8756
|
),
|
|
@@ -8437,7 +8768,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
8437
8768
|
}
|
|
8438
8769
|
}
|
|
8439
8770
|
async function planV2(projectDir, config, planV2Dir) {
|
|
8440
|
-
if (
|
|
8771
|
+
if (existsSync16(planV2Dir)) {
|
|
8441
8772
|
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
8442
8773
|
}
|
|
8443
8774
|
const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
|
|
@@ -8449,7 +8780,7 @@ async function planV2(projectDir, config, planV2Dir) {
|
|
|
8449
8780
|
function resultFromManifest(planV2Dir, manifest) {
|
|
8450
8781
|
return {
|
|
8451
8782
|
planDir: planV2Dir,
|
|
8452
|
-
manifestPath:
|
|
8783
|
+
manifestPath: join23(planV2Dir, "plan.json"),
|
|
8453
8784
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
8454
8785
|
planHash: manifest.planHash,
|
|
8455
8786
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -8585,8 +8916,8 @@ function parsePlanV2Manifest(value) {
|
|
|
8585
8916
|
return parsed;
|
|
8586
8917
|
}
|
|
8587
8918
|
function readPlanV2Manifest(planV2Dir) {
|
|
8588
|
-
const manifestPath =
|
|
8589
|
-
if (!
|
|
8919
|
+
const manifestPath = join23(planV2Dir, "plan.json");
|
|
8920
|
+
if (!existsSync16(manifestPath)) {
|
|
8590
8921
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
8591
8922
|
}
|
|
8592
8923
|
const value = readJsonFile(manifestPath, "v2 manifest");
|
|
@@ -8605,7 +8936,7 @@ function listPlanV2ArtifactsForTarget(manifest, target) {
|
|
|
8605
8936
|
}
|
|
8606
8937
|
function verifyBlob(planV2Dir, artifact) {
|
|
8607
8938
|
const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
|
|
8608
|
-
if (!
|
|
8939
|
+
if (!existsSync16(sourcePath)) {
|
|
8609
8940
|
throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
|
|
8610
8941
|
}
|
|
8611
8942
|
const stats = statSync6(sourcePath);
|
|
@@ -8618,7 +8949,7 @@ function verifyBlob(planV2Dir, artifact) {
|
|
|
8618
8949
|
return sourcePath;
|
|
8619
8950
|
}
|
|
8620
8951
|
function materializePlanV2Target(planV2Dir, target, destinationDir) {
|
|
8621
|
-
if (
|
|
8952
|
+
if (existsSync16(destinationDir)) {
|
|
8622
8953
|
throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
|
|
8623
8954
|
}
|
|
8624
8955
|
const manifest = readPlanV2Manifest(planV2Dir);
|
|
@@ -8629,16 +8960,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
|
|
|
8629
8960
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
8630
8961
|
}));
|
|
8631
8962
|
mkdirSync10(dirname10(destinationDir), { recursive: true });
|
|
8632
|
-
const tempDir = mkdtempSync4(
|
|
8963
|
+
const tempDir = mkdtempSync4(join23(dirname10(destinationDir), ".plan-v2-materialize-"));
|
|
8633
8964
|
try {
|
|
8634
8965
|
for (const { artifact, sourcePath } of verified) {
|
|
8635
|
-
const destinationPath =
|
|
8966
|
+
const destinationPath = join23(tempDir, ...artifact.path.split("/"));
|
|
8636
8967
|
mkdirSync10(dirname10(destinationPath), { recursive: true });
|
|
8637
8968
|
copyFileSync7(sourcePath, destinationPath);
|
|
8638
8969
|
}
|
|
8639
8970
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
8640
8971
|
writeFileSync7(
|
|
8641
|
-
|
|
8972
|
+
join23(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
8642
8973
|
canonicalJsonStringify({ manifest, target }),
|
|
8643
8974
|
"utf-8"
|
|
8644
8975
|
);
|
|
@@ -8657,12 +8988,12 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
|
|
|
8657
8988
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
8658
8989
|
// before the container change carries the legacy name, and materializing it
|
|
8659
8990
|
// under the new one would point at a file that was never written.
|
|
8660
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
8991
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join23(destinationDir, audioArtifact.path) : null : null
|
|
8661
8992
|
};
|
|
8662
8993
|
}
|
|
8663
8994
|
function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
8664
|
-
const markerPath =
|
|
8665
|
-
if (!
|
|
8995
|
+
const markerPath = join23(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
8996
|
+
if (!existsSync16(markerPath)) return null;
|
|
8666
8997
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
8667
8998
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
8668
8999
|
throw new PlanV2IntegrityError("malformed materialization marker");
|
|
@@ -8676,8 +9007,8 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
8676
9007
|
const manifest = parsePlanV2Manifest(marker.manifest);
|
|
8677
9008
|
const required = listPlanV2ArtifactsForTarget(manifest, expectedTarget);
|
|
8678
9009
|
for (const artifact of required) {
|
|
8679
|
-
const path =
|
|
8680
|
-
if (!
|
|
9010
|
+
const path = join23(planDir, ...artifact.path.split("/"));
|
|
9011
|
+
if (!existsSync16(path) || statSync6(path).size !== artifact.sizeBytes) {
|
|
8681
9012
|
throw new PlanV2IntegrityError(
|
|
8682
9013
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
8683
9014
|
);
|
|
@@ -8692,22 +9023,22 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
8692
9023
|
// src/services/distributed/planV2Execution.ts
|
|
8693
9024
|
import { mkdtempSync as mkdtempSync7, rmSync as rmSync13 } from "node:fs";
|
|
8694
9025
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8695
|
-
import { join as
|
|
9026
|
+
import { join as join27 } from "node:path";
|
|
8696
9027
|
|
|
8697
9028
|
// src/services/distributed/assemble.ts
|
|
8698
9029
|
import {
|
|
8699
9030
|
cpSync as cpSync3,
|
|
8700
|
-
existsSync as
|
|
9031
|
+
existsSync as existsSync17,
|
|
8701
9032
|
mkdirSync as mkdirSync12,
|
|
8702
9033
|
mkdtempSync as mkdtempSync6,
|
|
8703
9034
|
readFileSync as readFileSync10,
|
|
8704
|
-
readdirSync as
|
|
9035
|
+
readdirSync as readdirSync8,
|
|
8705
9036
|
renameSync as renameSync7,
|
|
8706
9037
|
rmSync as rmSync11,
|
|
8707
9038
|
statSync as statSync7,
|
|
8708
9039
|
writeFileSync as writeFileSync8
|
|
8709
9040
|
} from "node:fs";
|
|
8710
|
-
import { dirname as dirname12, join as
|
|
9041
|
+
import { dirname as dirname12, join as join25 } from "node:path";
|
|
8711
9042
|
import {
|
|
8712
9043
|
appendRenderProvenanceArgs,
|
|
8713
9044
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -8729,7 +9060,7 @@ import {
|
|
|
8729
9060
|
rmSync as rmSync10
|
|
8730
9061
|
} from "node:fs";
|
|
8731
9062
|
import { open } from "node:fs/promises";
|
|
8732
|
-
import { dirname as dirname11, join as
|
|
9063
|
+
import { dirname as dirname11, join as join24 } from "node:path";
|
|
8733
9064
|
import { finished, pipeline } from "node:stream/promises";
|
|
8734
9065
|
function boxAt(src, off, at, limit) {
|
|
8735
9066
|
let size = src.readUInt32BE(off);
|
|
@@ -9051,13 +9382,13 @@ async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
|
9051
9382
|
const header = buildStitchedHeader(layouts);
|
|
9052
9383
|
mkdirSync11(dirname11(outputPath), { recursive: true });
|
|
9053
9384
|
const workDir = mkdtempSync5(`${outputPath}.stitch-`);
|
|
9054
|
-
const out = createWriteStream(
|
|
9385
|
+
const out = createWriteStream(join24(workDir, "out.mp4"));
|
|
9055
9386
|
try {
|
|
9056
9387
|
out.write(header);
|
|
9057
9388
|
for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
9058
9389
|
out.end();
|
|
9059
9390
|
await finished(out);
|
|
9060
|
-
renameSync6(
|
|
9391
|
+
renameSync6(join24(workDir, "out.mp4"), outputPath);
|
|
9061
9392
|
} finally {
|
|
9062
9393
|
out.destroy();
|
|
9063
9394
|
rmSync10(workDir, { recursive: true, force: true });
|
|
@@ -9091,7 +9422,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
9091
9422
|
}
|
|
9092
9423
|
const format = plan2.dimensions.format;
|
|
9093
9424
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
9094
|
-
if (!
|
|
9425
|
+
if (!existsSync17(dirname12(outputPath))) {
|
|
9095
9426
|
mkdirSync12(dirname12(outputPath), { recursive: true });
|
|
9096
9427
|
}
|
|
9097
9428
|
const workDir = mkdtempSync6(`${outputPath}.assemble-work-`);
|
|
@@ -9114,8 +9445,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
9114
9445
|
};
|
|
9115
9446
|
try {
|
|
9116
9447
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
9117
|
-
const finalPath =
|
|
9118
|
-
const audio = audioPath !== null &&
|
|
9448
|
+
const finalPath = join25(workDir, `output.${format}`);
|
|
9449
|
+
const audio = audioPath !== null && existsSync17(audioPath) ? audioPath : null;
|
|
9119
9450
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
9120
9451
|
renameSync7(finalPath, outputPath);
|
|
9121
9452
|
log.info("[assemble] stage timings", {
|
|
@@ -9133,7 +9464,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
9133
9464
|
});
|
|
9134
9465
|
}
|
|
9135
9466
|
}
|
|
9136
|
-
const fileSize =
|
|
9467
|
+
const fileSize = existsSync17(outputPath) ? statSync7(outputPath).size : 0;
|
|
9137
9468
|
return {
|
|
9138
9469
|
outputPath,
|
|
9139
9470
|
durationMs: Date.now() - start,
|
|
@@ -9142,15 +9473,15 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
9142
9473
|
};
|
|
9143
9474
|
}
|
|
9144
9475
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
9145
|
-
const planJsonPath =
|
|
9146
|
-
const chunksJsonPath =
|
|
9147
|
-
if (!
|
|
9476
|
+
const planJsonPath = join25(planDir, "plan.json");
|
|
9477
|
+
const chunksJsonPath = join25(planDir, "meta", "chunks.json");
|
|
9478
|
+
if (!existsSync17(planJsonPath)) {
|
|
9148
9479
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
9149
9480
|
}
|
|
9150
9481
|
const plan2 = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
|
|
9151
9482
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
|
|
9152
9483
|
validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
|
|
9153
|
-
if (!
|
|
9484
|
+
if (!existsSync17(chunksJsonPath)) {
|
|
9154
9485
|
throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
|
|
9155
9486
|
}
|
|
9156
9487
|
const chunks = JSON.parse(readFileSync10(chunksJsonPath, "utf-8"));
|
|
@@ -9160,7 +9491,7 @@ function readAssemblePlan(planDir, chunkPaths) {
|
|
|
9160
9491
|
);
|
|
9161
9492
|
}
|
|
9162
9493
|
for (const path of chunkPaths) {
|
|
9163
|
-
if (!
|
|
9494
|
+
if (!existsSync17(path)) {
|
|
9164
9495
|
throw new Error(`[assemble] chunk path does not exist: ${path}`);
|
|
9165
9496
|
}
|
|
9166
9497
|
}
|
|
@@ -9170,7 +9501,7 @@ function videoInputArgs(ctx, videoSources) {
|
|
|
9170
9501
|
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
9171
9502
|
}
|
|
9172
9503
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
9173
|
-
const cfrOutputPath =
|
|
9504
|
+
const cfrOutputPath = join25(ctx.workDir, `cfr.${ctx.format}`);
|
|
9174
9505
|
const cfrArgs = [
|
|
9175
9506
|
...videoInputArgs(ctx, chunkPaths),
|
|
9176
9507
|
"-c:v",
|
|
@@ -9208,7 +9539,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
9208
9539
|
);
|
|
9209
9540
|
return;
|
|
9210
9541
|
}
|
|
9211
|
-
const paddedAudioPath =
|
|
9542
|
+
const paddedAudioPath = join25(ctx.workDir, "audio-padded.m4a");
|
|
9212
9543
|
const padTrimResult = await ctx.timed(
|
|
9213
9544
|
"audioMs",
|
|
9214
9545
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -9248,7 +9579,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
9248
9579
|
}
|
|
9249
9580
|
}
|
|
9250
9581
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
9251
|
-
const listPath =
|
|
9582
|
+
const listPath = join25(workDir, "concat-list.txt");
|
|
9252
9583
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
9253
9584
|
writeFileSync8(listPath, `${body}
|
|
9254
9585
|
`, "utf-8");
|
|
@@ -9281,8 +9612,8 @@ function chunksAreStitchedMp4(planDir, cfr) {
|
|
|
9281
9612
|
return true;
|
|
9282
9613
|
}
|
|
9283
9614
|
function chunksCarryAudio(planDir) {
|
|
9284
|
-
const encoderJsonPath =
|
|
9285
|
-
if (!
|
|
9615
|
+
const encoderJsonPath = join25(planDir, "meta", "encoder.json");
|
|
9616
|
+
if (!existsSync17(encoderJsonPath)) return false;
|
|
9286
9617
|
return JSON.parse(readFileSync10(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
|
|
9287
9618
|
}
|
|
9288
9619
|
function readCfrEncoder(planDir, format) {
|
|
@@ -9291,8 +9622,8 @@ function readCfrEncoder(planDir, format) {
|
|
|
9291
9622
|
`[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.`
|
|
9292
9623
|
);
|
|
9293
9624
|
}
|
|
9294
|
-
const encoderJsonPath =
|
|
9295
|
-
if (!
|
|
9625
|
+
const encoderJsonPath = join25(planDir, "meta", "encoder.json");
|
|
9626
|
+
if (!existsSync17(encoderJsonPath)) {
|
|
9296
9627
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
9297
9628
|
}
|
|
9298
9629
|
const encoderJson = JSON.parse(readFileSync10(encoderJsonPath, "utf-8"));
|
|
@@ -9304,7 +9635,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
9304
9635
|
return encoderJson;
|
|
9305
9636
|
}
|
|
9306
9637
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
9307
|
-
if (
|
|
9638
|
+
if (existsSync17(outputPath)) rmSync11(outputPath, { recursive: true, force: true });
|
|
9308
9639
|
mkdirSync12(outputPath, { recursive: true });
|
|
9309
9640
|
let globalIdx = 0;
|
|
9310
9641
|
for (const chunkDir of chunkPaths) {
|
|
@@ -9313,13 +9644,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
9313
9644
|
`[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
|
|
9314
9645
|
);
|
|
9315
9646
|
}
|
|
9316
|
-
const frames =
|
|
9647
|
+
const frames = readdirSync8(chunkDir).filter((name) => name.endsWith(".png")).sort();
|
|
9317
9648
|
if (frames.length === 0) {
|
|
9318
9649
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
9319
9650
|
}
|
|
9320
9651
|
for (const frame of frames) {
|
|
9321
|
-
const dst =
|
|
9322
|
-
cpSync3(
|
|
9652
|
+
const dst = join25(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
9653
|
+
cpSync3(join25(chunkDir, frame), dst);
|
|
9323
9654
|
globalIdx += 1;
|
|
9324
9655
|
}
|
|
9325
9656
|
}
|
|
@@ -9328,14 +9659,14 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
9328
9659
|
`[assemble] png-sequence frame count mismatch: merged ${globalIdx} frames vs plan.totalFrames=${totalFrames}. Using on-disk count.`
|
|
9329
9660
|
);
|
|
9330
9661
|
}
|
|
9331
|
-
if (audioPath !== null &&
|
|
9332
|
-
const sidecar =
|
|
9662
|
+
if (audioPath !== null && existsSync17(audioPath)) {
|
|
9663
|
+
const sidecar = join25(outputPath, MIXED_AUDIO_FILENAME4);
|
|
9333
9664
|
cpSync3(audioPath, sidecar);
|
|
9334
9665
|
}
|
|
9335
9666
|
let fileSize = 0;
|
|
9336
|
-
for (const name of
|
|
9667
|
+
for (const name of readdirSync8(outputPath)) {
|
|
9337
9668
|
try {
|
|
9338
|
-
fileSize += statSync7(
|
|
9669
|
+
fileSize += statSync7(join25(outputPath, name)).size;
|
|
9339
9670
|
} catch {
|
|
9340
9671
|
}
|
|
9341
9672
|
}
|
|
@@ -9349,22 +9680,19 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
9349
9680
|
|
|
9350
9681
|
// src/services/distributed/renderChunk.ts
|
|
9351
9682
|
import { randomBytes } from "node:crypto";
|
|
9352
|
-
import { existsSync as
|
|
9353
|
-
import {
|
|
9683
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync13, readFileSync as readFileSync11, readdirSync as readdirSync9, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "node:fs";
|
|
9684
|
+
import { join as join26 } from "node:path";
|
|
9354
9685
|
import {
|
|
9355
9686
|
assertSwiftShader,
|
|
9356
|
-
BROWSER_GPU_NOT_SOFTWARE as
|
|
9687
|
+
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
9357
9688
|
calculateOptimalWorkers as calculateOptimalWorkers2,
|
|
9358
9689
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
createFrameLookupTable as createFrameLookupTable3,
|
|
9363
|
-
createVideoFrameInjector as createVideoFrameInjector2,
|
|
9690
|
+
closeCaptureSession as closeCaptureSession10,
|
|
9691
|
+
createCaptureSession as createCaptureSession9,
|
|
9692
|
+
createFrameLookupTable as createFrameLookupTable5,
|
|
9364
9693
|
deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
|
|
9365
|
-
extractVideoFramesRange,
|
|
9366
9694
|
getEncoderPreset as getEncoderPreset6,
|
|
9367
|
-
initializeSession as
|
|
9695
|
+
initializeSession as initializeSession9,
|
|
9368
9696
|
probeBeginFrameLiveness as probeBeginFrameLiveness2,
|
|
9369
9697
|
readWebGlVendorInfoFromCanvas,
|
|
9370
9698
|
resolveConfig as resolveConfig5
|
|
@@ -9468,20 +9796,7 @@ async function muxChunkWithAudio(input) {
|
|
|
9468
9796
|
|
|
9469
9797
|
// src/services/distributed/renderChunk.ts
|
|
9470
9798
|
import { readWebGlVendorInfoFromCanvas as readWebGlVendorInfoFromCanvas2 } from "@hyperframes/engine";
|
|
9471
|
-
var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
|
|
9472
|
-
var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
|
|
9473
|
-
var MISSING_PLAN_ARTIFACT = "MISSING_PLAN_ARTIFACT";
|
|
9474
|
-
var CHUNK_INDEX_OUT_OF_RANGE = "CHUNK_INDEX_OUT_OF_RANGE";
|
|
9475
|
-
var MISSING_RUNTIME_ENV_SNAPSHOT = "MISSING_RUNTIME_ENV_SNAPSHOT";
|
|
9476
9799
|
var LEGACY_DISTRIBUTED_VP9_CPU_USED = 2;
|
|
9477
|
-
var RenderChunkValidationError = class extends Error {
|
|
9478
|
-
code;
|
|
9479
|
-
constructor(code, message) {
|
|
9480
|
-
super(message);
|
|
9481
|
-
this.name = "RenderChunkValidationError";
|
|
9482
|
-
this.code = code;
|
|
9483
|
-
}
|
|
9484
|
-
};
|
|
9485
9800
|
function validatePlanVideosForChunk(value) {
|
|
9486
9801
|
try {
|
|
9487
9802
|
return parsePlanVideosJson(value);
|
|
@@ -9493,10 +9808,10 @@ function validatePlanVideosForChunk(value) {
|
|
|
9493
9808
|
}
|
|
9494
9809
|
}
|
|
9495
9810
|
var distributedCaptureSessionDependencies = {
|
|
9496
|
-
createCaptureSession:
|
|
9811
|
+
createCaptureSession: createCaptureSession9,
|
|
9497
9812
|
assertSwiftShader,
|
|
9498
|
-
initializeSession:
|
|
9499
|
-
closeCaptureSession:
|
|
9813
|
+
initializeSession: initializeSession9,
|
|
9814
|
+
closeCaptureSession: closeCaptureSession10,
|
|
9500
9815
|
readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
|
|
9501
9816
|
};
|
|
9502
9817
|
async function createVerifiedDistributedCaptureSession(serverUrl, framesDir, captureOptions, cfg, dependencies = distributedCaptureSessionDependencies) {
|
|
@@ -9517,9 +9832,6 @@ async function createVerifiedDistributedCaptureSession(serverUrl, framesDir, cap
|
|
|
9517
9832
|
throw error;
|
|
9518
9833
|
}
|
|
9519
9834
|
}
|
|
9520
|
-
function createChunkVideoFrameInjectorFactory(frameLookup) {
|
|
9521
|
-
return () => createVideoFrameInjector2(frameLookup);
|
|
9522
|
-
}
|
|
9523
9835
|
function isCaptureMode(value) {
|
|
9524
9836
|
return value === "beginframe" || value === "screenshot" || value === "drawelement";
|
|
9525
9837
|
}
|
|
@@ -9547,130 +9859,11 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
|
|
|
9547
9859
|
);
|
|
9548
9860
|
return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
|
|
9549
9861
|
}
|
|
9550
|
-
function frameNumberFromFileName(name) {
|
|
9551
|
-
const match = /(\d+)(?=\.[^.]+$)/.exec(name);
|
|
9552
|
-
if (!match) return null;
|
|
9553
|
-
const frameNumber = Number(match[1]);
|
|
9554
|
-
return Number.isSafeInteger(frameNumber) ? frameNumber : null;
|
|
9555
|
-
}
|
|
9556
|
-
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
9557
|
-
const result = [];
|
|
9558
|
-
for (const v of videos) {
|
|
9559
|
-
const outputDir = join24(planDir, "video-frames", v.videoId);
|
|
9560
|
-
if (!existsSync17(outputDir)) {
|
|
9561
|
-
throw new Error(
|
|
9562
|
-
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
9563
|
-
);
|
|
9564
|
-
}
|
|
9565
|
-
const ext = (extname4(v.framePattern) || ".jpg").toLowerCase();
|
|
9566
|
-
const frames = readdirSync8(outputDir).filter((name) => name.toLowerCase().endsWith(ext)).sort((left, right) => {
|
|
9567
|
-
const leftNumber = frameNumberFromFileName(left);
|
|
9568
|
-
const rightNumber = frameNumberFromFileName(right);
|
|
9569
|
-
if (leftNumber === null || rightNumber === null) return left.localeCompare(right);
|
|
9570
|
-
return leftNumber - rightNumber || left.localeCompare(right);
|
|
9571
|
-
});
|
|
9572
|
-
const framePaths = /* @__PURE__ */ new Map();
|
|
9573
|
-
for (let i = 0; i < frames.length; i++) {
|
|
9574
|
-
const frameName = frames[i];
|
|
9575
|
-
if (!frameName) continue;
|
|
9576
|
-
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
9577
|
-
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
9578
|
-
framePaths.set(frameIndex, join24(outputDir, frameName));
|
|
9579
|
-
}
|
|
9580
|
-
result.push({
|
|
9581
|
-
videoId: v.videoId,
|
|
9582
|
-
srcPath: v.srcPath,
|
|
9583
|
-
outputDir,
|
|
9584
|
-
framePattern: v.framePattern,
|
|
9585
|
-
fps: v.fps,
|
|
9586
|
-
totalFrames: v.totalFrames,
|
|
9587
|
-
metadata: v.metadata,
|
|
9588
|
-
framePaths,
|
|
9589
|
-
// The chunk worker doesn't own the planDir's video-frames/ directory
|
|
9590
|
-
// (the controller does — adapters that fan out chunks across machines
|
|
9591
|
-
// share the planDir as read-only). Mark ownership as false so the
|
|
9592
|
-
// injector's eventual cleanup doesn't rm bytes another worker may
|
|
9593
|
-
// still be reading.
|
|
9594
|
-
ownedByLookup: false
|
|
9595
|
-
});
|
|
9596
|
-
}
|
|
9597
|
-
return result;
|
|
9598
|
-
}
|
|
9599
|
-
function contiguousRuns(sortedIndexes) {
|
|
9600
|
-
const runs2 = [];
|
|
9601
|
-
for (const index of sortedIndexes) {
|
|
9602
|
-
const last = runs2[runs2.length - 1];
|
|
9603
|
-
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
9604
|
-
else runs2.push({ firstFrame: index, frames: 1 });
|
|
9605
|
-
}
|
|
9606
|
-
return runs2;
|
|
9607
|
-
}
|
|
9608
|
-
async function extractDeferredVideoFramesForChunk(input) {
|
|
9609
|
-
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
9610
|
-
if (deferred.length === 0) return [];
|
|
9611
|
-
const table = createFrameLookupTable3(
|
|
9612
|
-
input.planVideos.videos,
|
|
9613
|
-
deferred.map((video) => ({
|
|
9614
|
-
...video,
|
|
9615
|
-
outputDir: input.outputRoot,
|
|
9616
|
-
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
9617
|
-
})),
|
|
9618
|
-
resolveRenderFpsConfig(input.fps).value
|
|
9619
|
-
);
|
|
9620
|
-
const shown = /* @__PURE__ */ new Map();
|
|
9621
|
-
for (let frame = input.slice.startFrame; frame < input.slice.endFrame; frame++) {
|
|
9622
|
-
const globalTime = frame * input.fps.den / input.fps.num;
|
|
9623
|
-
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
9624
|
-
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
9625
|
-
indexes.add(payload.frameIndex);
|
|
9626
|
-
shown.set(videoId, indexes);
|
|
9627
|
-
}
|
|
9628
|
-
}
|
|
9629
|
-
return Promise.all(
|
|
9630
|
-
deferred.map(async (video) => {
|
|
9631
|
-
const outputDir = join24(input.outputRoot, video.videoId);
|
|
9632
|
-
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
9633
|
-
if (indexes.length === 0) {
|
|
9634
|
-
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
9635
|
-
}
|
|
9636
|
-
const range = video.deferredRange;
|
|
9637
|
-
const sourcePath = join24(input.planDir, ...range.sourcePath.split("/"));
|
|
9638
|
-
if (!existsSync17(sourcePath)) {
|
|
9639
|
-
throw new RenderChunkValidationError(
|
|
9640
|
-
MISSING_PLAN_ARTIFACT,
|
|
9641
|
-
`[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
|
|
9642
|
-
);
|
|
9643
|
-
}
|
|
9644
|
-
const extracted = await extractVideoFramesRange(
|
|
9645
|
-
sourcePath,
|
|
9646
|
-
video.videoId,
|
|
9647
|
-
range.startTime,
|
|
9648
|
-
range.durationSeconds,
|
|
9649
|
-
{
|
|
9650
|
-
fps: input.fps,
|
|
9651
|
-
outputDir: input.outputRoot,
|
|
9652
|
-
format: range.format,
|
|
9653
|
-
frameRanges: contiguousRuns(indexes)
|
|
9654
|
-
},
|
|
9655
|
-
void 0,
|
|
9656
|
-
input.cfg
|
|
9657
|
-
);
|
|
9658
|
-
const missing = indexes.find((index) => !extracted.framePaths.has(index));
|
|
9659
|
-
if (missing !== void 0) {
|
|
9660
|
-
throw new RenderChunkValidationError(
|
|
9661
|
-
INVALID_VIDEO_METADATA,
|
|
9662
|
-
`[renderChunk] video ${JSON.stringify(video.videoId)} has no frame ${missing}; the plan expected ${video.totalFrames} frames from ${range.sourcePath}.`
|
|
9663
|
-
);
|
|
9664
|
-
}
|
|
9665
|
-
return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
|
|
9666
|
-
})
|
|
9667
|
-
);
|
|
9668
|
-
}
|
|
9669
9862
|
function hashChunkOutput(outputPath, kind) {
|
|
9670
9863
|
if (kind === "file") return sha256Hex(readFileSync11(outputPath));
|
|
9671
|
-
const entries =
|
|
9864
|
+
const entries = readdirSync9(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
9672
9865
|
const lines = entries.map(
|
|
9673
|
-
(name) => `${name}\0${sha256Hex(readFileSync11(
|
|
9866
|
+
(name) => `${name}\0${sha256Hex(readFileSync11(join26(outputPath, name)))}`
|
|
9674
9867
|
);
|
|
9675
9868
|
return sha256Hex(lines.join("\0"));
|
|
9676
9869
|
}
|
|
@@ -9687,10 +9880,10 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
9687
9880
|
async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
9688
9881
|
const start = Date.now();
|
|
9689
9882
|
const log = defaultLogger;
|
|
9690
|
-
const planJsonPath =
|
|
9691
|
-
const encoderJsonPath =
|
|
9692
|
-
const chunksJsonPath =
|
|
9693
|
-
if (!
|
|
9883
|
+
const planJsonPath = join26(planDir, "plan.json");
|
|
9884
|
+
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
9885
|
+
const chunksJsonPath = join26(planDir, "meta", "chunks.json");
|
|
9886
|
+
if (!existsSync18(planJsonPath)) {
|
|
9694
9887
|
throw new RenderChunkValidationError(
|
|
9695
9888
|
MISSING_PLAN_ARTIFACT,
|
|
9696
9889
|
`[renderChunk] planDir is missing required artifact: ${planJsonPath}`
|
|
@@ -9704,7 +9897,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9704
9897
|
chunkIndex
|
|
9705
9898
|
});
|
|
9706
9899
|
for (const required of [encoderJsonPath, chunksJsonPath]) {
|
|
9707
|
-
if (!
|
|
9900
|
+
if (!existsSync18(required)) {
|
|
9708
9901
|
throw new RenderChunkValidationError(
|
|
9709
9902
|
MISSING_PLAN_ARTIFACT,
|
|
9710
9903
|
`[renderChunk] planDir is missing required artifact: ${required}`
|
|
@@ -9713,9 +9906,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9713
9906
|
}
|
|
9714
9907
|
const encoder = JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
9715
9908
|
const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
|
|
9716
|
-
const videosJsonPath =
|
|
9909
|
+
const videosJsonPath = join26(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9717
9910
|
let planVideos = null;
|
|
9718
|
-
if (
|
|
9911
|
+
if (existsSync18(videosJsonPath)) {
|
|
9719
9912
|
try {
|
|
9720
9913
|
planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync11(videosJsonPath, "utf-8")));
|
|
9721
9914
|
} catch (err) {
|
|
@@ -9743,8 +9936,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9743
9936
|
`[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
|
|
9744
9937
|
);
|
|
9745
9938
|
}
|
|
9746
|
-
const compiledDir =
|
|
9747
|
-
if (!
|
|
9939
|
+
const compiledDir = join26(planDir, "compiled");
|
|
9940
|
+
if (!existsSync18(compiledDir)) {
|
|
9748
9941
|
throw new RenderChunkValidationError(
|
|
9749
9942
|
MISSING_PLAN_ARTIFACT,
|
|
9750
9943
|
`[renderChunk] planDir missing compiled/ directory: ${compiledDir}`
|
|
@@ -9759,7 +9952,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9759
9952
|
}
|
|
9760
9953
|
if (encoder.browserGpuMode !== "software") {
|
|
9761
9954
|
throw new RenderChunkValidationError(
|
|
9762
|
-
|
|
9955
|
+
BROWSER_GPU_NOT_SOFTWARE4,
|
|
9763
9956
|
`[renderChunk] planDir requires browserGpuMode=software, got ${JSON.stringify(encoder.browserGpuMode)}.`
|
|
9764
9957
|
);
|
|
9765
9958
|
}
|
|
@@ -9801,10 +9994,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9801
9994
|
};
|
|
9802
9995
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
9803
9996
|
mkdirSync13(workDir, { recursive: true });
|
|
9804
|
-
const framesDir =
|
|
9997
|
+
const framesDir = join26(workDir, "captured-frames");
|
|
9805
9998
|
mkdirSync13(framesDir, { recursive: true });
|
|
9806
9999
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
9807
|
-
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ?
|
|
10000
|
+
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
9808
10001
|
planVideos.videos,
|
|
9809
10002
|
[
|
|
9810
10003
|
...rebuildExtractedFramesFromPlanDir(
|
|
@@ -9815,7 +10008,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9815
10008
|
...await extractDeferredVideoFramesForChunk({
|
|
9816
10009
|
planDir,
|
|
9817
10010
|
planVideos,
|
|
9818
|
-
|
|
10011
|
+
frames: Array.from(
|
|
10012
|
+
{ length: slice.endFrame - slice.startFrame },
|
|
10013
|
+
(_, i) => slice.startFrame + i
|
|
10014
|
+
),
|
|
9819
10015
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
9820
10016
|
outputRoot: videoFramesDir,
|
|
9821
10017
|
cfg
|
|
@@ -9824,9 +10020,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9824
10020
|
resolveRenderFpsConfig(job.config.fps).value
|
|
9825
10021
|
) : null;
|
|
9826
10022
|
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
9827
|
-
const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(
|
|
9828
|
-
planVideos?.videos.length ?? 0
|
|
9829
|
-
);
|
|
9830
10023
|
const fileServer = await createFileServer({
|
|
9831
10024
|
projectDir: compiledDir,
|
|
9832
10025
|
compiledDir,
|
|
@@ -9836,28 +10029,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9836
10029
|
// chunk runtime seek quantization stays on the same fps grid as capture.
|
|
9837
10030
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen }
|
|
9838
10031
|
});
|
|
9839
|
-
const captureOptions = {
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
deviceScaleFactor: encoder.deviceScaleFactor,
|
|
9846
|
-
// Re-inject the controller's snapshotted variables so the chunk's
|
|
9847
|
-
// first capture sees the same `window.__hfVariables` the in-process
|
|
9848
|
-
// renderer would have seen. Optional — compositions that don't
|
|
9849
|
-
// declare `data-composition-variables` leave this undefined and the
|
|
9850
|
-
// engine skips the `evaluateOnNewDocument` injection.
|
|
9851
|
-
variables: encoder.variables,
|
|
9852
|
-
subTimelineWaitMemo: {},
|
|
9853
|
-
...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
|
|
9854
|
-
// lock the BeginFrame warmup loop to a fixed iteration count so
|
|
9855
|
-
// `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
|
|
9856
|
-
lockWarmupTicks: true,
|
|
9857
|
-
requiresWebGpu: compositionRequiresWebGpu3(
|
|
9858
|
-
readFileSync11(join24(compiledDir, "index.html"), "utf-8")
|
|
9859
|
-
)
|
|
9860
|
-
};
|
|
10032
|
+
const captureOptions = buildChunkCaptureOptions({
|
|
10033
|
+
dimensions: plan2.dimensions,
|
|
10034
|
+
encoder,
|
|
10035
|
+
videoCount: planVideos?.videos.length ?? 0,
|
|
10036
|
+
html: readFileSync11(join26(compiledDir, "index.html"), "utf-8")
|
|
10037
|
+
});
|
|
9861
10038
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
9862
10039
|
let session = null;
|
|
9863
10040
|
let outputKind;
|
|
@@ -9888,7 +10065,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9888
10065
|
{ chunkIndex }
|
|
9889
10066
|
);
|
|
9890
10067
|
if (chunkWorkerCount > 1) {
|
|
9891
|
-
await
|
|
10068
|
+
await closeCaptureSession10(session);
|
|
9892
10069
|
session = null;
|
|
9893
10070
|
}
|
|
9894
10071
|
} else if (beginFrameStalled) {
|
|
@@ -9897,7 +10074,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9897
10074
|
"[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
|
|
9898
10075
|
{ chunkIndex }
|
|
9899
10076
|
);
|
|
9900
|
-
await
|
|
10077
|
+
await closeCaptureSession10(session).catch(() => {
|
|
9901
10078
|
});
|
|
9902
10079
|
session = null;
|
|
9903
10080
|
if (chunkWorkerCount === 1) {
|
|
@@ -9916,7 +10093,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
9916
10093
|
sessionBootMs += Date.now() - screenshotBootStarted;
|
|
9917
10094
|
}
|
|
9918
10095
|
} else if (chunkWorkerCount > 1) {
|
|
9919
|
-
await
|
|
10096
|
+
await closeCaptureSession10(session);
|
|
9920
10097
|
session = null;
|
|
9921
10098
|
}
|
|
9922
10099
|
}
|
|
@@ -10018,10 +10195,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
10018
10195
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
10019
10196
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
10020
10197
|
if (isPngSequence) {
|
|
10021
|
-
if (!
|
|
10198
|
+
if (!existsSync18(outputChunkPath)) mkdirSync13(outputChunkPath, { recursive: true });
|
|
10022
10199
|
} else {
|
|
10023
|
-
const outDir =
|
|
10024
|
-
if (!
|
|
10200
|
+
const outDir = join26(outputChunkPath, "..");
|
|
10201
|
+
if (!existsSync18(outDir)) mkdirSync13(outDir, { recursive: true });
|
|
10025
10202
|
}
|
|
10026
10203
|
const encodeStarted = Date.now();
|
|
10027
10204
|
await runEncodeStage({
|
|
@@ -10079,7 +10256,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
10079
10256
|
} finally {
|
|
10080
10257
|
if (session) {
|
|
10081
10258
|
try {
|
|
10082
|
-
await
|
|
10259
|
+
await closeCaptureSession10(session);
|
|
10083
10260
|
} catch (err) {
|
|
10084
10261
|
log.warn("[renderChunk] error closing capture session in finally", {
|
|
10085
10262
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -10144,8 +10321,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
10144
10321
|
|
|
10145
10322
|
// src/services/distributed/planV2Execution.ts
|
|
10146
10323
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
|
|
10147
|
-
const workRoot = mkdtempSync7(
|
|
10148
|
-
const materializedPlanDir =
|
|
10324
|
+
const workRoot = mkdtempSync7(join27(tmpdir2(), "hf-plan-v2-chunk-"));
|
|
10325
|
+
const materializedPlanDir = join27(workRoot, "plan");
|
|
10149
10326
|
try {
|
|
10150
10327
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
10151
10328
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
|
|
@@ -10154,8 +10331,8 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
|
|
|
10154
10331
|
}
|
|
10155
10332
|
}
|
|
10156
10333
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
10157
|
-
const workRoot = mkdtempSync7(
|
|
10158
|
-
const materializedPlanDir =
|
|
10334
|
+
const workRoot = mkdtempSync7(join27(tmpdir2(), "hf-plan-v2-assembler-"));
|
|
10335
|
+
const materializedPlanDir = join27(workRoot, "plan");
|
|
10159
10336
|
try {
|
|
10160
10337
|
const materialized = materializePlanV2Target(
|
|
10161
10338
|
planV2Dir,
|
|
@@ -10417,18 +10594,18 @@ function validateIntDimension(field, value) {
|
|
|
10417
10594
|
}
|
|
10418
10595
|
|
|
10419
10596
|
// src/services/distributed/projectHash.ts
|
|
10420
|
-
import { readdirSync as
|
|
10597
|
+
import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "node:fs";
|
|
10421
10598
|
import { createHash as createHash6 } from "node:crypto";
|
|
10422
|
-
import { join as
|
|
10599
|
+
import { join as join28, relative as relative7 } from "node:path";
|
|
10423
10600
|
function hashProjectDir(projectDir) {
|
|
10424
10601
|
const hash = createHash6("sha256");
|
|
10425
10602
|
const files = [];
|
|
10426
10603
|
function walk(dir, isRoot) {
|
|
10427
|
-
for (const entry of
|
|
10604
|
+
for (const entry of readdirSync10(dir, { withFileTypes: true }).sort(
|
|
10428
10605
|
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
10429
10606
|
)) {
|
|
10430
10607
|
if (isRoot && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(entry.name)) continue;
|
|
10431
|
-
const full =
|
|
10608
|
+
const full = join28(dir, entry.name);
|
|
10432
10609
|
if (entry.isDirectory()) walk(full, false);
|
|
10433
10610
|
else if (entry.isFile()) files.push(full);
|
|
10434
10611
|
}
|