@hyperframes/producer 0.8.177 → 0.8.179

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