@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.
@@ -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 existsSync13,
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 extname3, isAbsolute as isAbsolute3, join as join18, relative as relative5, sep as sep2 } from "node:path";
14
- import { fpsToNumber as fpsToNumber10 } from "@hyperframes/core";
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/render/stages/freezePlan.ts
6364
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
6365
- import { join as join15, relative as relative3, resolve as resolve7 } from "node:path";
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/planProtocol.ts
6368
- var PLAN_SCHEMA_VERSION = 1;
6369
- var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
6370
- var PLAN_HASH_SCHEMA = "hyperframes-plan-hash-v1";
6371
- var PLAN_V2_SCHEMA_VERSION = 2;
6372
- var PLAN_V2_ARTIFACT_LAYOUT = "content-addressed-plan-v2";
6373
- var PLAN_V2_HASH_SCHEMA = "hyperframes-plan-manifest-hash-v2";
6374
- var PLAN_PROTOCOL_UNSUPPORTED = "PLAN_PROTOCOL_UNSUPPORTED";
6375
- var PLAN_PROTOCOL_V1 = Object.freeze({
6376
- schemaVersion: PLAN_SCHEMA_VERSION,
6377
- artifactLayout: PLAN_ARTIFACT_LAYOUT,
6378
- hashSchema: PLAN_HASH_SCHEMA
6379
- });
6380
- var CURRENT_PLAN_PROTOCOL = PLAN_PROTOCOL_V1;
6381
- var PLAN_PROTOCOL_V2 = Object.freeze({
6382
- schemaVersion: PLAN_V2_SCHEMA_VERSION,
6383
- artifactLayout: PLAN_V2_ARTIFACT_LAYOUT,
6384
- hashSchema: PLAN_V2_HASH_SCHEMA
6385
- });
6386
- var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
6387
- roles: Object.freeze({
6388
- planner: Object.freeze({
6389
- produces: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2])
6390
- }),
6391
- chunk: Object.freeze({
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
- var PlanProtocolUnsupportedError = class extends Error {
6405
- // Public adapters inspect this typed code even though OSS producer does not.
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 = PLAN_PROTOCOL_UNSUPPORTED;
6408
- constructor(reason) {
6409
- super(`[planProtocol] ${reason} (${PLAN_PROTOCOL_UNSUPPORTED})`);
6410
- this.name = "PlanProtocolUnsupportedError";
6487
+ code = INVALID_VIDEO_METADATA;
6488
+ constructor(message) {
6489
+ super(message);
6490
+ this.name = "PlanVideosMetadataError";
6411
6491
  }
6412
6492
  };
6413
- function isRecord(value) {
6414
- return value !== null && typeof value === "object" && !Array.isArray(value);
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 readPlanProtocol(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
6425
- if (!isRecord(planJson)) {
6426
- throw new PlanProtocolUnsupportedError("plan.json must contain a JSON object");
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
- const descriptor = planJson.protocol;
6437
- if (!isRecord(descriptor)) {
6438
- throw new PlanProtocolUnsupportedError("plan.json protocol descriptor must be an object");
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
- for (const field of ["schemaVersion", "artifactLayout", "hashSchema"]) {
6441
- if (!Object.prototype.hasOwnProperty.call(descriptor, field)) {
6442
- throw new PlanProtocolUnsupportedError(`plan.json protocol descriptor is missing ${field}`);
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
- const protocol = protocolMatches(descriptor, PLAN_PROTOCOL_V1) ? PLAN_PROTOCOL_V1 : protocolMatches(descriptor, PLAN_PROTOCOL_V2) ? PLAN_PROTOCOL_V2 : null;
6446
- if (protocol === null || !capabilitiesAccept(capabilities, protocol)) {
6447
- throw new PlanProtocolUnsupportedError("unsupported plan.json protocol descriptor");
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 protocol;
6518
+ return value;
6450
6519
  }
6451
- function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
6452
- const protocol = readPlanProtocol(planJson, capabilities);
6453
- if (protocol !== PLAN_PROTOCOL_V1) {
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 PLAN_PROTOCOL_V1;
6524
+ return value;
6459
6525
  }
6460
-
6461
- // src/services/render/stages/planHash.ts
6462
- import { createHash as createHash4 } from "node:crypto";
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
- hash.update(input.fontSnapshotSha, "utf8");
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 canonicalJsonStringify(value) {
6493
- if (value === null) return "null";
6494
- if (typeof value === "boolean") return value ? "true" : "false";
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
- if (typeof value === "string") return JSON.stringify(value);
6502
- if (Array.isArray(value)) {
6503
- return `[${value.map(canonicalJsonStringify).join(",")}]`;
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
- if (typeof value === "object") {
6506
- const obj = value;
6507
- const keys = Object.keys(obj).sort();
6508
- const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJsonStringify(obj[k])}`);
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
- throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
6549
+ return {
6550
+ sourcePath,
6551
+ startTime: readFiniteNumber(range.startTime, `${field}.startTime`),
6552
+ durationSeconds,
6553
+ format: range.format
6554
+ };
6512
6555
  }
6513
- function sha256Hex(bytes) {
6514
- const h = createHash4("sha256");
6515
- h.update(bytes);
6516
- return h.digest("hex");
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 sha256File(path) {
6519
- const hash = createHash4("sha256");
6520
- const buffer = Buffer.allocUnsafe(1024 * 1024);
6521
- const fd = openSync3(path, "r");
6522
- try {
6523
- let bytesRead = 0;
6524
- do {
6525
- bytesRead = readSync(fd, buffer, 0, buffer.byteLength, null);
6526
- if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
6527
- } while (bytesRead > 0);
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 hash.digest("hex");
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
- async function sha256FileAsync(path) {
6534
- const hash = createHash4("sha256");
6535
- for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
6536
- hash.update(chunk);
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
- return hash.digest("hex");
6539
- }
6540
-
6541
- // src/services/render/stages/freezePlan.ts
6542
- var COMPILED_INDEX_RELATIVE_PATH = "compiled/index.html";
6543
- var HASH_EXCLUDED_PLAN_FILES = /* @__PURE__ */ new Set([
6544
- "plan.json",
6545
- "meta/encoder.json",
6546
- COMPILED_INDEX_RELATIVE_PATH
6547
- ]);
6548
- function planFileSha256(absolutePath, digests) {
6549
- let sha256 = digests?.get(absolutePath);
6550
- if (sha256 === void 0) {
6551
- sha256 = sha256File(absolutePath);
6552
- digests?.set(absolutePath, sha256);
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
- return sha256;
6555
- }
6556
- async function prehashPlanFiles(dir, digests) {
6557
- if (!existsSync11(dir)) return;
6558
- for (const { absolutePath } of listPlanFiles(dir)) {
6559
- if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
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 stripUndefined(value) {
6563
- if (Array.isArray(value)) return value.map(stripUndefined);
6564
- if (value !== null && typeof value === "object") {
6565
- const obj = value;
6566
- const out = {};
6567
- for (const key of Object.keys(obj)) {
6568
- const v = obj[key];
6569
- if (v === void 0) continue;
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 out;
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
- return value;
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 listPlanFiles(planDir) {
6577
- const results = [];
6578
- const rootResolved = resolve7(planDir);
6579
- function walk(dir) {
6580
- const entries = readdirSync4(dir, { withFileTypes: true });
6581
- for (const entry of entries) {
6582
- const full = join15(dir, entry.name);
6583
- if (entry.isDirectory()) {
6584
- walk(full);
6585
- } else if (entry.isFile()) {
6586
- results.push({
6587
- planRelativePath: relative3(rootResolved, full).split(/[\\/]+/).join("/"),
6588
- absolutePath: full
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
- walk(rootResolved);
6594
- results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
6595
- return results;
6748
+ cachedProducerVersion = "0.0.0-unknown";
6749
+ return cachedProducerVersion;
6596
6750
  }
6597
- function collectPlanAssetShas(planDir, digests) {
6598
- const files = listPlanFiles(planDir);
6599
- let compositionHtml = null;
6600
- const assets = [];
6601
- for (const file of files) {
6602
- if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
6603
- compositionHtml = readFileSync7(file.absolutePath);
6604
- continue;
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
- if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
6607
- assets.push({
6608
- path: file.planRelativePath,
6609
- sha256: planFileSha256(file.absolutePath, digests)
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
- if (compositionHtml === null) {
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 recomputePlanHashFromPlanDir(planDir, digests) {
6620
- const planJsonPath = join15(planDir, "plan.json");
6621
- const encoderJsonPath = join15(planDir, "meta", "encoder.json");
6622
- if (!existsSync11(planJsonPath)) {
6623
- throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
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
- const planJson = JSON.parse(readFileSync7(planJsonPath, "utf-8"));
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
- async function freezePlan(input) {
6642
- const {
6643
- planDir,
6644
- composition,
6645
- encoder,
6646
- chunks,
6647
- dimensions,
6648
- producerVersion,
6649
- fontSnapshotSha,
6650
- durationSeconds,
6651
- totalFrames,
6652
- hasAudio,
6653
- digests
6654
- } = input;
6655
- if (!existsSync11(planDir)) {
6656
- throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
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
- // src/services/render/runtimeEnvSnapshot.ts
6700
- var RUNTIME_ENV_PREFIXES = [
6701
- "PRODUCER_RUNTIME_",
6702
- "PRODUCER_RENDER_"
6703
- ];
6704
- function snapshotRuntimeEnv(env = process.env) {
6705
- const snapshot = {};
6706
- for (const key of Object.keys(env)) {
6707
- const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
6708
- if (!matches) continue;
6709
- const value = env[key];
6710
- if (typeof value !== "string") continue;
6711
- snapshot[key] = value;
6712
- }
6713
- return snapshot;
6714
- }
6715
- function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
6716
- const previous = {};
6717
- for (const [key, value] of Object.entries(snapshot)) {
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
- restore: () => {
6726
- for (const [key, value] of Object.entries(previous)) {
6727
- if (value === void 0) delete env[key];
6728
- else env[key] = value;
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/shared.ts
6735
- import { execFile as execFileCallback } from "node:child_process";
6736
- import { dirname as dirname8, join as join16 } from "node:path";
6737
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "node:fs";
6738
- import { fileURLToPath as fileURLToPath3 } from "node:url";
6739
- import { promisify as promisify2 } from "node:util";
6740
- import {
6741
- getFfmpegBinary,
6742
- MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME3
6743
- } from "@hyperframes/engine";
6744
- var PLAN_VIDEOS_META_RELATIVE_PATH = "meta/videos.json";
6745
- var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
6746
- var PLAN_AUDIO_LEGACY_RELATIVE_PATH = "audio.aac";
6747
- function isPlanAudioArtifactPath(path) {
6748
- return path === PLAN_AUDIO_RELATIVE_PATH || path === PLAN_AUDIO_LEGACY_RELATIVE_PATH;
6749
- }
6750
- function resolvePlanAudioPath(planDir) {
6751
- for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
6752
- const candidate = join16(planDir, name);
6753
- if (existsSync12(candidate)) return candidate;
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
- var INVALID_VIDEO_METADATA = "INVALID_VIDEO_METADATA";
6758
- var PlanVideosMetadataError = class extends Error {
6759
- // Read by cloud adapters across the package boundary to classify retries.
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 = INVALID_VIDEO_METADATA;
6762
- constructor(message) {
6763
- super(message);
6764
- this.name = "PlanVideosMetadataError";
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 metadataError(field, expectation) {
6768
- throw new PlanVideosMetadataError(`${field} ${expectation}`);
7006
+ function isRecord(value) {
7007
+ return value !== null && typeof value === "object" && !Array.isArray(value);
6769
7008
  }
6770
- function readRecord(value, field) {
6771
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
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 readNonEmptyString(value, field) {
6777
- if (typeof value !== "string" || value.length === 0) {
6778
- metadataError(field, "must be a non-empty string");
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 readString(value, field) {
6783
- if (typeof value !== "string") {
6784
- metadataError(field, "must be a string");
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
- return value;
6787
- }
6788
- function readFiniteNumber(value, field) {
6789
- if (typeof value !== "number" || !Number.isFinite(value)) {
6790
- metadataError(field, "must be a finite number");
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
- return value;
6793
- }
6794
- function readBoolean(value, field) {
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
- return value;
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 readPositiveInteger(value, field) {
6801
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
6802
- metadataError(field, "must be a positive integer");
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 value;
7051
+ return PLAN_PROTOCOL_V1;
6805
7052
  }
6806
- function readNonNegativeInteger(value, field) {
6807
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
6808
- metadataError(field, "must be a non-negative integer");
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
- return value;
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 readDeferredFrameRange(value, field) {
6813
- const range = readRecord(value, field);
6814
- const sourcePath = readNonEmptyString(range.sourcePath, `${field}.sourcePath`);
6815
- if (sourcePath.startsWith("/") || sourcePath.split(/[\\/]/).some((part) => part === "..")) {
6816
- metadataError(`${field}.sourcePath`, "must stay inside the plan directory");
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
- const durationSeconds = readFiniteNumber(range.durationSeconds, `${field}.durationSeconds`);
6819
- if (durationSeconds <= 0) metadataError(`${field}.durationSeconds`, "must be positive");
6820
- if (range.format !== "jpg" && range.format !== "png") {
6821
- metadataError(`${field}.format`, 'must be "jpg" or "png"');
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 readRateSpec(value, field) {
6831
- if (value === void 0) return void 0;
6832
- if (typeof value === "number") return readFiniteNumber(value, field);
6833
- const lane = readRecord(value, field);
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 lane;
7131
+ return hash.digest("hex");
6838
7132
  }
6839
- function readVideoMetadata(value, field) {
6840
- const record = readRecord(value, field);
6841
- let colorSpace = null;
6842
- if (record.colorSpace !== null) {
6843
- const color = readRecord(record.colorSpace, `${field}.colorSpace`);
6844
- colorSpace = {
6845
- colorTransfer: readString(color.colorTransfer, `${field}.colorSpace.colorTransfer`),
6846
- colorPrimaries: readString(color.colorPrimaries, `${field}.colorSpace.colorPrimaries`),
6847
- colorSpace: readString(color.colorSpace, `${field}.colorSpace.colorSpace`)
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 parsePlanVideosJson(value) {
6872
- const record = readRecord(value, "meta/videos.json");
6873
- if (!Array.isArray(record.videos) || !Array.isArray(record.extracted)) {
6874
- metadataError("meta/videos.json", "must contain videos and extracted arrays");
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
- const extractedIds = /* @__PURE__ */ new Set();
6911
- for (const entry of extracted) {
6912
- if (extractedIds.has(entry.videoId)) {
6913
- metadataError(
6914
- "meta/videos.json.extracted",
6915
- `contains duplicate videoId ${JSON.stringify(entry.videoId)}`
6916
- );
6917
- }
6918
- extractedIds.add(entry.videoId);
6919
- if (!videoIds.has(entry.videoId)) {
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
- for (const video of videos) {
6927
- if (!extractedIds.has(video.id)) {
6928
- metadataError(
6929
- "meta/videos.json.extracted",
6930
- `is missing declared video ${JSON.stringify(video.id)}`
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
- return { videos, extracted };
7186
+ walk(rootResolved);
7187
+ results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
7188
+ return results;
6935
7189
  }
6936
- function buildPlanVideosJson(input) {
6937
- const videos = input.videos.map((video, index) => {
6938
- if (Number.isFinite(video.end)) return { ...video };
6939
- if (!Number.isFinite(input.compositionEnd) || input.compositionEnd <= 0 || input.compositionEnd <= video.start) {
6940
- metadataError(
6941
- `meta/videos.json.videos[${index}].end`,
6942
- "cannot be resolved without a finite composition end after its start"
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
- return { ...video, end: input.compositionEnd };
6946
- });
6947
- return parsePlanVideosJson({ videos, extracted: input.extracted });
6948
- }
6949
- var execFile = promisify2(execFileCallback);
6950
- var cachedFfmpegVersions = /* @__PURE__ */ new Map();
6951
- async function readFfmpegVersion() {
6952
- const binary = getFfmpegBinary();
6953
- const cached = cachedFfmpegVersions.get(binary);
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
- const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
6972
- if (!firstLine) {
6973
- throw new Error("ffmpeg -version returned empty output");
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
- cachedFfmpegVersions.set(binary, firstLine);
6976
- return firstLine;
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 buildSyntheticRenderJob(input) {
6979
- const renderConfig = {
6980
- fps: input.fps,
6981
- quality: input.quality,
6982
- format: input.format,
6983
- crf: input.crf,
6984
- videoBitrate: input.bitrate,
6985
- videoFrameFormat: input.videoFrameFormat,
6986
- outputResolution: input.outputResolution,
6987
- outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
6988
- // Distributed mode hard-pins to software GPU. The plan-time validator
6989
- // refuses to fan out otherwise.
6990
- useGpu: false,
6991
- debug: false,
6992
- entryFile: input.entryFile,
6993
- logger: input.logger ?? defaultLogger,
6994
- hdrMode: input.hdrMode,
6995
- strictness: input.strictness,
6996
- producerConfig: input.producerConfig,
6997
- variables: input.variables
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
- return createRenderJob(renderConfig);
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
- var cachedProducerVersion = null;
7002
- function readProducerVersion() {
7003
- if (cachedProducerVersion !== null) return cachedProducerVersion;
7004
- const startDir = dirname8(fileURLToPath3(import.meta.url));
7005
- let current = startDir;
7006
- for (let i = 0; i < 10; i++) {
7007
- const candidate = join16(current, "package.json");
7008
- if (existsSync12(candidate)) {
7009
- try {
7010
- const pkg = JSON.parse(readFileSync8(candidate, "utf-8"));
7011
- if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
7012
- cachedProducerVersion = pkg.version;
7013
- return pkg.version;
7014
- }
7015
- } catch {
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
- const parent = dirname8(current);
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 readdirSync5 } from "node:fs";
7029
- import { extname as extname2, join as join17, relative as relative4 } from "node:path";
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(extname2(filePath).toLowerCase())) {
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 = readdirSync5(dir, { withFileTypes: true });
7437
+ entries = readdirSync6(dir, { withFileTypes: true });
7137
7438
  } catch {
7138
7439
  return;
7139
7440
  }
7140
7441
  for (const entry of entries) {
7141
- const full = join17(dir, entry.name);
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 estimateChunkSeconds(frames, megapixels) {
7204
- return ESTIMATED_CHUNK_OVERHEAD_SECONDS + frames * ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
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
- const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
7225
- if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
7226
- return wave;
7227
- }
7228
- const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
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
- join18("meta", "composition.json"),
7350
- join18("meta", "encoder.json"),
7351
- join18("meta", "chunks.json")
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(join18(planDir, relativePath), { force: true });
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 : join18(input.planDir, "video-frames", stagedRel);
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 && existsSync13(join18(input.planDir, "compiled", rel))) {
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}${extname3(current)}`;
7497
- const destination = join18(input.planDir, ...result.split("/"));
7498
- mkdirSync8(join18(input.planDir, "video-sources"), { recursive: true });
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 (!existsSync13(planDir)) mkdirSync8(planDir, { recursive: true });
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 = join18(projectDir, entryFile);
7546
- if (!existsSync13(htmlPath)) {
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 = join18(planDir, ".plan-work");
7856
+ const workDir = join20(planDir, ".plan-work");
7550
7857
  rmSync7(workDir, { recursive: true, force: true });
7551
7858
  mkdirSync8(workDir, { recursive: true });
7552
- const compiledDir = join18(workDir, "compiled");
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 = join18(planDir, "compiled");
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: closeCaptureSession10 } = await import("@hyperframes/engine");
7630
- await closeCaptureSession10(probeResult.probeSession);
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 maxParallel = resolveMaxParallelChunks(
7638
- config,
7639
- totalFrames,
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 = join18(compiledDir, "__hyperframes_video_frames");
7675
- const videoFramesDst = join18(planDir, "video-frames");
7676
- if (existsSync13(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
7677
- const videoSourcesDir = join18(planDir, "video-sources");
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 (existsSync13(stagedVideoFrames)) {
7976
+ if (existsSync14(stagedVideoFrames)) {
7680
7977
  renameSync3(stagedVideoFrames, videoFramesDst);
7681
7978
  } else {
7682
7979
  mkdirSync8(videoFramesDst, { recursive: true });
7683
7980
  }
7684
- if (existsSync13(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
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(join18(planDir, "meta"), { recursive: true });
8007
+ mkdirSync8(join20(planDir, "meta"), { recursive: true });
7711
8008
  writeFileSync5(
7712
- join18(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
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 = join18(planDir, PLAN_AUDIO_RELATIVE_PATH);
7752
- if (audioResult.hasAudio && existsSync13(audioResult.audioOutputPath)) {
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 && existsSync13(planAudioPath)) {
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 existsSync15,
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 readdirSync6,
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 join21, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
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 createFrameLookupTable2 } from "@hyperframes/engine";
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 existsSync14,
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 join20 } from "node:path";
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 join19 } from "node:path";
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 join19(root, "artifacts", "sha256", digest.slice(0, 2), digest);
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 (existsSync14(destinationDir)) {
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(join20(dirname9(destinationDir), ".plan-v2-publish-"));
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 (existsSync14(destinationPath)) return;
8305
+ if (existsSync15(destinationPath)) return;
7975
8306
  mkdirSync9(dirname9(destinationPath), { recursive: true });
7976
- const stagingDir = mkdtempSync3(join20(dirname9(destinationPath), ".plan-v2-blob-"));
7977
- const temporaryPath = join20(stagingDir, "blob");
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 (!existsSync14(planV2BlobPath(this.temporaryDir, digest))) {
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(join20(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
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 readdirSync6(dir, { withFileTypes: true })) {
8043
- const absolutePath = join21(dir, entry.name);
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 = join21(executionPlanDir, "video-frames");
8071
- if (!existsSync15(videoRoot)) return;
8072
- for (const videoEntry of readdirSync6(videoRoot, { withFileTypes: true })) {
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 = join21(videoRoot, videoEntry.name);
8075
- if (readdirSync6(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
8076
- const sentinelPath = join21(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
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, join21(outputDir, String(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 = readdirSync6(outputDir).sort();
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(join21(outputDir, frameName));
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, join21(outputDir, frameName));
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 = join21(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
8166
- if (!existsSync15(videosPath)) return;
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 = join21(executionPlanDir, "video-frames");
8195
- const hasExtractedFrames = existsSync15(videoRoot) && listFiles(videoRoot).length > 0;
8196
- const videosPath = join21(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
8197
- if (!existsSync15(videosPath)) {
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 = join21(executionPlanDir, "meta", "chunks.json");
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 = createFrameLookupTable2(
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 (existsSync15(destinationPath)) return;
8585
+ if (existsSync16(destinationPath)) return;
8255
8586
  mkdirSync10(dirname10(destinationPath), { recursive: true });
8256
- const temporaryDir = mkdtempSync4(join21(dirname10(destinationPath), ".plan-v2-blob-"));
8257
- const temporaryPath = join21(temporaryDir, "blob");
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 = join21(executionPlanDir, "plan.json");
8291
- if (!existsSync15(executionPlanPath)) {
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(join21(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
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 (existsSync15(planV2Dir)) {
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(join21(dirname10(planV2Dir), ".plan-v2-build-"));
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
- join21(tempDir, "plan.json"),
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(join21(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
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(join21(stagingRoot, "compiled")).map(
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 (existsSync15(planV2Dir)) {
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: join21(planV2Dir, "plan.json"),
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 = join21(planV2Dir, "plan.json");
8589
- if (!existsSync15(manifestPath)) {
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 (!existsSync15(sourcePath)) {
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 (existsSync15(destinationDir)) {
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(join21(dirname10(destinationDir), ".plan-v2-materialize-"));
8963
+ const tempDir = mkdtempSync4(join23(dirname10(destinationDir), ".plan-v2-materialize-"));
8633
8964
  try {
8634
8965
  for (const { artifact, sourcePath } of verified) {
8635
- const destinationPath = join21(tempDir, ...artifact.path.split("/"));
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
- join21(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
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 ? join21(destinationDir, audioArtifact.path) : null : null
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 = join21(planDir, PLAN_V2_MATERIALIZATION_MARKER);
8665
- if (!existsSync15(markerPath)) return null;
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 = join21(planDir, ...artifact.path.split("/"));
8680
- if (!existsSync15(path) || statSync6(path).size !== artifact.sizeBytes) {
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 join25 } from "node:path";
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 existsSync16,
9031
+ existsSync as existsSync17,
8701
9032
  mkdirSync as mkdirSync12,
8702
9033
  mkdtempSync as mkdtempSync6,
8703
9034
  readFileSync as readFileSync10,
8704
- readdirSync as readdirSync7,
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 join23 } from "node:path";
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 join22 } from "node:path";
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(join22(workDir, "out.mp4"));
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(join22(workDir, "out.mp4"), outputPath);
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 (!existsSync16(dirname12(outputPath))) {
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 = join23(workDir, `output.${format}`);
9118
- const audio = audioPath !== null && existsSync16(audioPath) ? 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 = existsSync16(outputPath) ? statSync7(outputPath).size : 0;
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 = join23(planDir, "plan.json");
9146
- const chunksJsonPath = join23(planDir, "meta", "chunks.json");
9147
- if (!existsSync16(planJsonPath)) {
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 (!existsSync16(chunksJsonPath)) {
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 (!existsSync16(path)) {
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 = join23(ctx.workDir, `cfr.${ctx.format}`);
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 = join23(ctx.workDir, "audio-padded.m4a");
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 = join23(workDir, "concat-list.txt");
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 = join23(planDir, "meta", "encoder.json");
9285
- if (!existsSync16(encoderJsonPath)) return false;
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 = join23(planDir, "meta", "encoder.json");
9295
- if (!existsSync16(encoderJsonPath)) {
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 (existsSync16(outputPath)) rmSync11(outputPath, { recursive: true, force: true });
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 = readdirSync7(chunkDir).filter((name) => name.endsWith(".png")).sort();
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 = join23(outputPath, formatExportFrameName(globalIdx, "png"));
9322
- cpSync3(join23(chunkDir, frame), dst);
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 && existsSync16(audioPath)) {
9332
- const sidecar = join23(outputPath, MIXED_AUDIO_FILENAME4);
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 readdirSync7(outputPath)) {
9667
+ for (const name of readdirSync8(outputPath)) {
9337
9668
  try {
9338
- fileSize += statSync7(join23(outputPath, name)).size;
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 existsSync17, mkdirSync as mkdirSync13, readFileSync as readFileSync11, readdirSync as readdirSync8, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "node:fs";
9353
- import { extname as extname4, join as join24 } from "node:path";
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 BROWSER_GPU_NOT_SOFTWARE3,
9687
+ BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
9357
9688
  calculateOptimalWorkers as calculateOptimalWorkers2,
9358
9689
  classifyCaptureFailure as classifyCaptureFailure4,
9359
- compositionRequiresWebGpu as compositionRequiresWebGpu3,
9360
- closeCaptureSession as closeCaptureSession9,
9361
- createCaptureSession as createCaptureSession8,
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 initializeSession8,
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: createCaptureSession8,
9811
+ createCaptureSession: createCaptureSession9,
9497
9812
  assertSwiftShader,
9498
- initializeSession: initializeSession8,
9499
- closeCaptureSession: closeCaptureSession9,
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 = readdirSync8(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
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(join24(outputPath, name)))}`
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 = join24(planDir, "plan.json");
9691
- const encoderJsonPath = join24(planDir, "meta", "encoder.json");
9692
- const chunksJsonPath = join24(planDir, "meta", "chunks.json");
9693
- if (!existsSync17(planJsonPath)) {
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 (!existsSync17(required)) {
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 = join24(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
9909
+ const videosJsonPath = join26(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
9717
9910
  let planVideos = null;
9718
- if (existsSync17(videosJsonPath)) {
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 = join24(planDir, "compiled");
9747
- if (!existsSync17(compiledDir)) {
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
- BROWSER_GPU_NOT_SOFTWARE3,
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 = join24(workDir, "captured-frames");
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 ? createFrameLookupTable3(
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
- slice,
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
- width: plan2.dimensions.width,
9841
- height: plan2.dimensions.height,
9842
- fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
9843
- format: plan2.dimensions.format === "mp4" ? "jpeg" : "png",
9844
- quality: plan2.dimensions.format === "mp4" ? resolveCaptureJpegQuality(job.config.quality) : void 0,
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 closeCaptureSession9(session);
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 closeCaptureSession9(session).catch(() => {
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 closeCaptureSession9(session);
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 (!existsSync17(outputChunkPath)) mkdirSync13(outputChunkPath, { recursive: true });
10198
+ if (!existsSync18(outputChunkPath)) mkdirSync13(outputChunkPath, { recursive: true });
10022
10199
  } else {
10023
- const outDir = join24(outputChunkPath, "..");
10024
- if (!existsSync17(outDir)) mkdirSync13(outDir, { recursive: true });
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 closeCaptureSession9(session);
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(join25(tmpdir2(), "hf-plan-v2-chunk-"));
10148
- const materializedPlanDir = join25(workRoot, "plan");
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(join25(tmpdir2(), "hf-plan-v2-assembler-"));
10158
- const materializedPlanDir = join25(workRoot, "plan");
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 readdirSync9, readFileSync as readFileSync12 } from "node:fs";
10597
+ import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "node:fs";
10421
10598
  import { createHash as createHash6 } from "node:crypto";
10422
- import { join as join26, relative as relative7 } from "node:path";
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 readdirSync9(dir, { withFileTypes: true }).sort(
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 = join26(dir, entry.name);
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
  }