@hyperframes/producer 0.8.177 → 0.8.178

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.
@@ -29,8 +29,8 @@
29
29
  * No networking, no AWS SDK, no Temporal SDK — those live in adapter
30
30
  * packages. This module is library code only.
31
31
  */
32
- export { buildChunkSlices, measurePlanDirBytes, plan, rejectUnsupportedDistributedFormat, resolveChunkPlan, type DistributedRenderConfig, type PlanResult, DEFAULT_CHUNK_SIZE, DEFAULT_MAX_PARALLEL_CHUNKS, MIN_CHUNK_SIZE, PLAN_DIR_SIZE_LIMIT_BYTES, PLAN_PROJECT_DIR_SKIP_SEGMENTS, CHUNK_EXCEEDS_WORKER_TIMEOUT, ChunkExceedsWorkerTimeoutError, FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED, FormatNotSupportedInDistributedError, PLAN_TOO_LARGE, PlanTooLargeError, } from "./services/distributed/plan.js";
33
- export { createPlanV2FromExecutionPlan, createPlanV2FromV1, getPlanV2ExecutionPlanHash, listPlanV2ArtifactsForTarget, materializePlanV2Target, planV2, planV2WithPublisher, publishPlanV2FromExecutionPlan, publishPlanV2FromV1, readPlanV2Manifest, validatePlanV2MaterializedTarget, PLAN_V2_INTEGRITY_UNRECOVERABLE, PLAN_V2_MATERIALIZATION_MARKER, PlanV2IntegrityError, type PlanV2Artifact, type PlanV2Limitations, type PlanV2Manifest, type PlanV2MaterializationResult, type PlanV2MaterializationTarget, type PlanV2Result, type PlanV2WithPublisherOptions, } from "./services/distributed/planV2.js";
32
+ export { buildChunkSlices, measurePlanDirBytes, plan, rejectUnsupportedDistributedFormat, resolveChunkPlan, type DistributedRenderConfig, type PlanResult, type RemoteProjectFile, DEFAULT_CHUNK_SIZE, DEFAULT_MAX_PARALLEL_CHUNKS, MIN_CHUNK_SIZE, PLAN_DIR_SIZE_LIMIT_BYTES, PLAN_PROJECT_DIR_SKIP_SEGMENTS, CHUNK_EXCEEDS_WORKER_TIMEOUT, ChunkExceedsWorkerTimeoutError, FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED, FormatNotSupportedInDistributedError, PLAN_TOO_LARGE, PlanTooLargeError, } from "./services/distributed/plan.js";
33
+ export { createPlanV2FromExecutionPlan, createPlanV2FromV1, getPlanV2ExecutionPlanHash, isPlanV2DeferredSourceArtifact, listPlanV2ArtifactsForTarget, materializePlanV2Target, planV2, planV2WithPublisher, publishPlanV2FromExecutionPlan, publishPlanV2FromV1, readPlanV2Manifest, validatePlanV2MaterializedTarget, PLAN_V2_INTEGRITY_UNRECOVERABLE, PLAN_V2_MATERIALIZATION_MARKER, PlanV2IntegrityError, type PlanV2Artifact, type PlanV2Limitations, type PlanV2Manifest, type PlanV2MaterializeOptions, type PlanV2MaterializationResult, type PlanV2MaterializationTarget, type PlanV2RemoteSources, type PlanV2Result, type PlanV2WithPublisherOptions, } from "./services/distributed/planV2.js";
34
34
  export { LocalPlanV2ArtifactPublisher, type LocalPlanV2ArtifactPublisherOptions, type PlanV2ArtifactPublisher, type PlanV2PublishBlob, } from "./services/distributed/planV2Publisher.js";
35
35
  export { assembleV2, renderChunkV2 } from "./services/distributed/planV2Execution.js";
36
36
  export { applyRuntimeEnvSnapshot, readWebGlVendorInfoFromCanvas, renderChunk, type ChunkRenderer, type ChunkResult, type EffectiveChunkResult, FFMPEG_VERSION_MISMATCH, INVALID_VIDEO_METADATA, PLAN_HASH_MISMATCH, RenderChunkValidationError, } from "./services/distributed/renderChunk.js";
@@ -38,7 +38,7 @@ export { assemble, chunksAreStitchedMp4, type AssembleResult, } from "./services
38
38
  export { muxChunkWithAudio } from "./services/distributed/chunkAudio.js";
39
39
  export { buildStitchedHeader, readMp4Layout, type Mp4Layout, type ReadRange, } from "./services/distributed/mp4Stitch.js";
40
40
  export { InvalidConfigError, type SerializableDistributedRenderConfig, validateDistributedRenderConfig, validateVariablesPayload, } from "./services/distributed/renderConfigValidation.js";
41
- export { hashProjectDir } from "./services/distributed/projectHash.js";
41
+ export { hashProjectDir, listSiteFiles } from "./services/distributed/projectHash.js";
42
42
  export { CURRENT_PLAN_PROTOCOL, DISTRIBUTED_RENDER_CAPABILITIES, getDistributedRenderCapabilities, PLAN_ARTIFACT_LAYOUT, PLAN_HASH_SCHEMA, PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2, PLAN_PROTOCOL_UNSUPPORTED, PLAN_SCHEMA_VERSION, PLAN_V2_ARTIFACT_LAYOUT, PLAN_V2_HASH_SCHEMA, PLAN_V2_SCHEMA_VERSION, PlanProtocolUnsupportedError, readPlanProtocol, readPlanProtocolV1, type DistributedRenderCapabilities, type PlanProtocolConsumerCapabilities, type PlanProtocolDescriptor, type PlanProtocolV1Descriptor, type PlanProtocolV2Descriptor, type SupportedPlanProtocolDescriptor, } from "./services/distributed/planProtocol.js";
43
43
  export { PlanVideosMetadataError, type DistributedFormat } from "./services/distributed/shared.js";
44
44
  export { isPlanAudioArtifactPath, PLAN_AUDIO_LEGACY_RELATIVE_PATH, PLAN_AUDIO_RELATIVE_PATH, resolvePlanAudioPath, } from "./services/distributed/shared.js";
@@ -1148,6 +1148,9 @@ import {
1148
1148
  extractAudioMetadata
1149
1149
  } from "@hyperframes/engine";
1150
1150
 
1151
+ // src/services/render/renderDirOwner.ts
1152
+ import { RENDER_TEMP_DIR } from "@hyperframes/core/project-folders";
1153
+
1151
1154
  // src/services/render/capturePlan.ts
1152
1155
  function assertWorkerCount(workerCount) {
1153
1156
  if (!Number.isInteger(workerCount) || workerCount < 1) {
@@ -2337,7 +2340,7 @@ function detectShaderTransitionUsage(html) {
2337
2340
  }
2338
2341
  return false;
2339
2342
  }
2340
- async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log) {
2343
+ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
2341
2344
  let filePath = src;
2342
2345
  if (isHttpUrl(src)) {
2343
2346
  if (!existsSync6(downloadDir)) mkdirSync4(downloadDir, { recursive: true });
@@ -2351,8 +2354,9 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
2351
2354
  } else {
2352
2355
  filePath = decodedUrlPath(src);
2353
2356
  if (!filePath.startsWith("/")) filePath = join5(baseDir, filePath);
2357
+ filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
2354
2358
  }
2355
- if (!existsSync6(filePath)) {
2359
+ if (!isHttpUrl(filePath) && !existsSync6(filePath)) {
2356
2360
  return { duration: null, resolvedPath: filePath };
2357
2361
  }
2358
2362
  const withSrcContext = (error) => {
@@ -2415,7 +2419,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
2415
2419
  }
2416
2420
  return changed ? document2.toString() : html;
2417
2421
  }
2418
- async function compileHtmlFile(html, baseDir, downloadDir, log) {
2422
+ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
2419
2423
  const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
2420
2424
  const mediaUnresolved = unresolved.filter(
2421
2425
  (el) => (el.tagName === "video" || el.tagName === "audio") && el.src
@@ -2431,7 +2435,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log) {
2431
2435
  downloadDir,
2432
2436
  el.tagName,
2433
2437
  el.id,
2434
- log
2438
+ log,
2439
+ urlByAbsentLocalPath
2435
2440
  ).then(({ duration }) => ({ id: el.id, duration }))
2436
2441
  )
2437
2442
  );
@@ -2473,7 +2478,7 @@ async function compileHtmlFile(html, baseDir, downloadDir, log) {
2473
2478
  compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
2474
2479
  return { html: compiledHtml, unresolvedCompositions };
2475
2480
  }
2476
- async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set()) {
2481
+ async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
2477
2482
  const subCompositions = /* @__PURE__ */ new Map();
2478
2483
  const { document: document2 } = parseHTML4(html);
2479
2484
  const compEls = document2.querySelectorAll("[data-composition-src]");
@@ -2499,13 +2504,16 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
2499
2504
  const { html: compiledSub } = await compileHtmlFile(
2500
2505
  item.rawSubHtml,
2501
2506
  dirname4(item.filePath),
2502
- downloadDir
2507
+ downloadDir,
2508
+ void 0,
2509
+ urlByAbsentLocalPath
2503
2510
  );
2504
2511
  const nested = await parseSubCompositions(
2505
2512
  compiledSub,
2506
2513
  projectDir,
2507
2514
  downloadDir,
2508
- item.nestedVisited
2515
+ item.nestedVisited,
2516
+ urlByAbsentLocalPath
2509
2517
  );
2510
2518
  return {
2511
2519
  srcPath: item.srcPath,
@@ -3263,9 +3271,16 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3263
3271
  rawHtml,
3264
3272
  projectDir,
3265
3273
  downloadDir,
3266
- options.log
3274
+ options.log,
3275
+ options.urlByAbsentLocalPath
3276
+ );
3277
+ const { subCompositions } = await parseSubCompositions(
3278
+ compiledHtml,
3279
+ projectDir,
3280
+ downloadDir,
3281
+ void 0,
3282
+ options.urlByAbsentLocalPath
3267
3283
  );
3268
- const { subCompositions } = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
3269
3284
  const fullHtml = ensureFullDocument(compiledHtml);
3270
3285
  const inlinedHtml = inlineSubCompositions(
3271
3286
  fullHtml,
@@ -3761,6 +3776,7 @@ async function runCompileStage(input) {
3761
3776
  allowSystemFontCapture,
3762
3777
  abortSignal,
3763
3778
  variables: input.variables,
3779
+ urlByAbsentLocalPath: input.urlByAbsentLocalPath,
3764
3780
  animatedGifCacheDir: cfg.extractCacheDir ? join6(cfg.extractCacheDir, "animated-gif") : void 0,
3765
3781
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
3766
3782
  });
@@ -4759,7 +4775,8 @@ async function runExtractVideosStage(input) {
4759
4775
  abortSignal,
4760
4776
  assertNotAborted,
4761
4777
  materializeSymlinks,
4762
- deferRangeExtraction
4778
+ deferRangeExtraction,
4779
+ urlByAbsentLocalPath
4763
4780
  } = input;
4764
4781
  const stage2Start = Date.now();
4765
4782
  const extractionPolicy = resolveVideoExtractionPolicy();
@@ -4853,7 +4870,8 @@ async function runExtractVideosStage(input) {
4853
4870
  maxTransientRetries: extractionPolicy.maxTransientRetries,
4854
4871
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
4855
4872
  deferRangeExtraction,
4856
- contentKeyedDirs: [join8(compiledDir, REMOTE_MEDIA_SUBDIR)]
4873
+ contentKeyedDirs: [join8(compiledDir, REMOTE_MEDIA_SUBDIR)],
4874
+ urlByAbsentLocalPath
4857
4875
  },
4858
4876
  abortSignal,
4859
4877
  cfg,
@@ -4936,6 +4954,7 @@ async function runAudioStage(input) {
4936
4954
  audioGain,
4937
4955
  audios,
4938
4956
  masterFxChain,
4957
+ urlByAbsentLocalPath,
4939
4958
  abortSignal,
4940
4959
  assertNotAborted,
4941
4960
  log
@@ -4955,7 +4974,7 @@ async function runAudioStage(input) {
4955
4974
  audioOutputPath,
4956
4975
  duration,
4957
4976
  abortSignal,
4958
- { ffmpegProcessTimeout, audioGain, masterFxChain },
4977
+ { ffmpegProcessTimeout, audioGain, masterFxChain, urlByAbsentLocalPath },
4959
4978
  compiledDir
4960
4979
  );
4961
4980
  } catch (err) {
@@ -6833,6 +6852,8 @@ function buildChunkCaptureOptions(input) {
6833
6852
  // The controller's snapshot, so the first capture sees the in-process renderer's `__hfVariables`.
6834
6853
  variables: encoder.variables,
6835
6854
  subTimelineWaitMemo: {},
6855
+ skipReadinessVideoIds: input.skipReadinessVideoIds,
6856
+ videoMetadataHints: input.videoMetadataHints,
6836
6857
  ...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
6837
6858
  // A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
6838
6859
  lockWarmupTicks: true,
@@ -6841,6 +6862,19 @@ function buildChunkCaptureOptions(input) {
6841
6862
  }
6842
6863
  async function extractDeferredVideoFramesForChunk(input) {
6843
6864
  const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
6865
+ const remoteSources = input.remoteSources ?? {};
6866
+ for (const path of Object.keys(remoteSources)) {
6867
+ if (!deferred.some((video) => video.deferredRange.sourcePath === path)) {
6868
+ throw new RenderChunkValidationError(
6869
+ MISSING_PLAN_ARTIFACT,
6870
+ `[renderChunk] ${path} is read by URL but no deferred video uses it`
6871
+ );
6872
+ }
6873
+ defaultLogger.info("[renderChunk] reading video by URL", {
6874
+ path,
6875
+ url: remoteSources[path].split("?")[0]
6876
+ });
6877
+ }
6844
6878
  if (deferred.length === 0) return [];
6845
6879
  const table = createFrameLookupTable2(
6846
6880
  input.planVideos.videos,
@@ -6868,8 +6902,9 @@ async function extractDeferredVideoFramesForChunk(input) {
6868
6902
  return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
6869
6903
  }
6870
6904
  const range = video.deferredRange;
6871
- const sourcePath = join16(input.planDir, ...range.sourcePath.split("/"));
6872
- if (!existsSync12(sourcePath)) {
6905
+ const remoteUrl = remoteSources[range.sourcePath];
6906
+ const sourcePath = remoteUrl ?? join16(input.planDir, ...range.sourcePath.split("/"));
6907
+ if (remoteUrl === void 0 && !existsSync12(sourcePath)) {
6873
6908
  throw new RenderChunkValidationError(
6874
6909
  MISSING_PLAN_ARTIFACT,
6875
6910
  `[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
@@ -7209,6 +7244,16 @@ function collectPlanAssetShas(planDir, digests) {
7209
7244
  }
7210
7245
  return { compositionHtml, assets };
7211
7246
  }
7247
+ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
7248
+ function writePlanRemoteFiles(planDir, files) {
7249
+ if (files.length === 0) return;
7250
+ const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
7251
+ writeFileSync4(join18(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
7252
+ }
7253
+ function readPlanRemoteFiles(planDir) {
7254
+ const path = join18(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
7255
+ return existsSync13(path) ? JSON.parse(readFileSync8(path, "utf-8")) : [];
7256
+ }
7212
7257
  function recomputePlanHashFromPlanDir(planDir, digests) {
7213
7258
  const planJsonPath = join18(planDir, "plan.json");
7214
7259
  const encoderJsonPath = join18(planDir, "meta", "encoder.json");
@@ -7794,7 +7839,7 @@ function createVideoSourceShipper(input) {
7794
7839
  let result = null;
7795
7840
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
7796
7841
  const rel = pathInside(root, srcPath);
7797
- if (rel !== null && existsSync14(join20(input.planDir, "compiled", rel))) {
7842
+ if (rel !== null && (existsSync14(join20(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
7798
7843
  result = ["compiled", ...rel.split(sep2)].join("/");
7799
7844
  break;
7800
7845
  }
@@ -7872,8 +7917,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7872
7917
  logPlanStage(log, "copy", stageStarted);
7873
7918
  const finalCompiledDir = join20(planDir, "compiled");
7874
7919
  const needsAlpha = outputNeedsAlpha(config.format);
7920
+ const remoteFiles = Object.entries(options.remoteFiles ?? {});
7921
+ const remoteLocalPath = (rel) => join20(projectDir, ...rel.split("/"));
7922
+ const urlByAbsentLocalPath = new Map(
7923
+ remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
7924
+ );
7925
+ const localPathByUrl = new Map(
7926
+ remoteFiles.map(([rel, file]) => [file.url, remoteLocalPath(rel)])
7927
+ );
7875
7928
  stageStarted = Date.now();
7876
7929
  const compileResult = await runCompileStage({
7930
+ urlByAbsentLocalPath,
7877
7931
  projectDir,
7878
7932
  workDir,
7879
7933
  htmlPath,
@@ -7961,7 +8015,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7961
8015
  abortSignal,
7962
8016
  assertNotAborted,
7963
8017
  materializeSymlinks: true,
7964
- deferRangeExtraction: options.deferVideoExtraction
8018
+ deferRangeExtraction: options.deferVideoExtraction,
8019
+ urlByAbsentLocalPath
7965
8020
  });
7966
8021
  logPlanStage(log, "extract", stageStarted);
7967
8022
  if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
@@ -7984,7 +8039,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7984
8039
  planDir,
7985
8040
  projectDir,
7986
8041
  workCompiledDir: compiledDir,
7987
- workVideoFramesDir: stagedVideoFrames
8042
+ workVideoFramesDir: stagedVideoFrames,
8043
+ isRemote: (srcPath) => urlByAbsentLocalPath.has(srcPath)
7988
8044
  });
7989
8045
  const sourceRoots = [
7990
8046
  [stagedVideoFrames, "video-frames"],
@@ -7994,17 +8050,28 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7994
8050
  const planVideosJson = buildPlanVideosJson({
7995
8051
  videos: composition.videos,
7996
8052
  compositionEnd: job.duration ?? Number.NaN,
7997
- extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) => ({
7998
- videoId: ext.videoId,
7999
- srcPath: stableSourcePath(sourceRoots, ext.srcPath),
8000
- framePattern: ext.framePattern,
8001
- fps: ext.fps,
8002
- totalFrames: ext.totalFrames,
8003
- metadata: ext.metadata,
8004
- ...ext.deferredRange ? { deferredRange: { ...ext.deferredRange, sourcePath: shipVideoSource(ext.srcPath) } } : {}
8005
- }))
8053
+ extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) => {
8054
+ const sourcePath = localPathByUrl.get(ext.srcPath) ?? ext.srcPath;
8055
+ return {
8056
+ videoId: ext.videoId,
8057
+ srcPath: stableSourcePath(sourceRoots, sourcePath),
8058
+ framePattern: ext.framePattern,
8059
+ fps: ext.fps,
8060
+ totalFrames: ext.totalFrames,
8061
+ metadata: ext.metadata,
8062
+ ...ext.deferredRange ? { deferredRange: { ...ext.deferredRange, sourcePath: shipVideoSource(sourcePath) } } : {}
8063
+ };
8064
+ })
8006
8065
  });
8007
8066
  mkdirSync8(join20(planDir, "meta"), { recursive: true });
8067
+ writePlanRemoteFiles(
8068
+ planDir,
8069
+ remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
8070
+ path: `compiled/${rel}`,
8071
+ sha256,
8072
+ sizeBytes
8073
+ }))
8074
+ );
8008
8075
  writeFileSync5(
8009
8076
  join20(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
8010
8077
  JSON.stringify(planVideosJson, null, 2),
@@ -8019,6 +8086,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
8019
8086
  projectDir,
8020
8087
  workDir,
8021
8088
  compiledDir: finalCompiledDir,
8089
+ urlByAbsentLocalPath,
8022
8090
  duration: audioSeconds,
8023
8091
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
8024
8092
  audioGain: cfg.audioGain,
@@ -8428,17 +8496,20 @@ function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
8428
8496
  if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
8429
8497
  return { chunks: "all", assembler: true };
8430
8498
  }
8431
- if (path === "meta/composition.json" || path === "meta/videos.json" || path.startsWith("compiled/")) {
8432
- return { chunks: "all", assembler: false };
8433
- }
8434
- if (path.startsWith("video-frames/") || path.startsWith("video-sources/")) {
8499
+ if (path.startsWith("video-frames/") || path.startsWith("video-sources/") || path.startsWith("compiled/") && videoDependencies?.has(path)) {
8435
8500
  return {
8436
8501
  chunks: videoDependencies === null ? "all" : videoDependencies.get(path) ?? [],
8437
8502
  assembler: false
8438
8503
  };
8439
8504
  }
8505
+ if (path === "meta/composition.json" || path === "meta/videos.json" || path.startsWith("compiled/")) {
8506
+ return { chunks: "all", assembler: false };
8507
+ }
8440
8508
  return { chunks: "all", assembler: true };
8441
8509
  }
8510
+ function isPlanV2DeferredSourceArtifact(artifact) {
8511
+ return artifact.path.startsWith("compiled/") && artifact.chunks !== "all";
8512
+ }
8442
8513
  function listVideoFramePaths(executionPlanDir, videos) {
8443
8514
  return videos.extracted.map((video) => {
8444
8515
  const outputDir = resolveExtractedVideoOutputDir(executionPlanDir, video.videoId);
@@ -8601,14 +8672,17 @@ function publishBlobFor(absolutePath, digests) {
8601
8672
  };
8602
8673
  }
8603
8674
  async function putBlobs(publisher, blobs) {
8675
+ await putTasks(blobs.map((blob) => () => publisher.putBlob(blob)));
8676
+ }
8677
+ async function putTasks(tasks) {
8604
8678
  const uploadSlots = 32;
8605
8679
  let next = 0;
8606
8680
  let failure;
8607
8681
  const upload = async () => {
8608
- while (!failure && next < blobs.length) {
8609
- const blob = blobs[next++];
8682
+ while (!failure && next < tasks.length) {
8683
+ const task = tasks[next++];
8610
8684
  try {
8611
- await publisher.putBlob(blob);
8685
+ await task();
8612
8686
  } catch (reason) {
8613
8687
  failure ??= { reason };
8614
8688
  }
@@ -8663,6 +8737,15 @@ function buildPlanV2Publication(executionPlanDir, digests) {
8663
8737
  ...targets
8664
8738
  });
8665
8739
  }
8740
+ const storedBlobs = /* @__PURE__ */ new Map();
8741
+ for (const remote of readPlanRemoteFiles(executionPlanDir)) {
8742
+ const { sha256, sizeBytes } = remote;
8743
+ if (!blobs.has(sha256)) storedBlobs.set(sha256, { sha256, sizeBytes });
8744
+ artifacts.push({
8745
+ ...remote,
8746
+ ...artifactTargets(remote.path, videoDependencyPlan.dependencies, chunksCarryAudio2)
8747
+ });
8748
+ }
8666
8749
  const base = {
8667
8750
  protocol: PLAN_PROTOCOL_V2,
8668
8751
  // Retain the established manifest key byte-for-byte. It now acts as the
@@ -8684,7 +8767,8 @@ function buildPlanV2Publication(executionPlanDir, digests) {
8684
8767
  ...base,
8685
8768
  planHash: computeManifestHash(base)
8686
8769
  },
8687
- blobs: [...blobs.values()]
8770
+ blobs: [...blobs.values()],
8771
+ storedBlobs: [...storedBlobs.values()]
8688
8772
  };
8689
8773
  }
8690
8774
  function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
@@ -8692,6 +8776,9 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
8692
8776
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
8693
8777
  }
8694
8778
  const publication = buildPlanV2Publication(executionPlanDir);
8779
+ if (publication.storedBlobs.length > 0) {
8780
+ throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
8781
+ }
8695
8782
  mkdirSync10(dirname10(planV2Dir), { recursive: true });
8696
8783
  const tempDir = mkdtempSync4(join23(dirname10(planV2Dir), ".plan-v2-build-"));
8697
8784
  try {
@@ -8721,8 +8808,14 @@ async function publishExecutionPlan(executionPlanDir, publisher, { digests, log
8721
8808
  let stageStarted = Date.now();
8722
8809
  const publication = buildPlanV2Publication(executionPlanDir, digests);
8723
8810
  logPlanStage(log, "publication", stageStarted);
8811
+ if (publication.storedBlobs.length > 0 && !publisher.referenceStoredBlob) {
8812
+ throw new PlanV2IntegrityError("this publisher cannot reference remote files");
8813
+ }
8724
8814
  stageStarted = Date.now();
8725
- await putBlobs(publisher, publication.blobs);
8815
+ await putTasks([
8816
+ ...publication.blobs.map((blob) => () => publisher.putBlob(blob)),
8817
+ ...publication.storedBlobs.map((blob) => () => publisher.referenceStoredBlob(blob))
8818
+ ]);
8726
8819
  logPlanStage(log, "upload", stageStarted);
8727
8820
  stageStarted = Date.now();
8728
8821
  await publisher.commitManifest(canonicalJsonStringify(publication.manifest));
@@ -8746,6 +8839,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
8746
8839
  await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
8747
8840
  executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER,
8748
8841
  deferVideoExtraction: true,
8842
+ remoteFiles: options.remoteFiles,
8749
8843
  digests,
8750
8844
  // artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
8751
8845
  whileAudioMixes: () => putBlobs(
@@ -8948,14 +9042,16 @@ function verifyBlob(planV2Dir, artifact) {
8948
9042
  }
8949
9043
  return sourcePath;
8950
9044
  }
8951
- function materializePlanV2Target(planV2Dir, target, destinationDir) {
9045
+ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}) {
8952
9046
  if (existsSync16(destinationDir)) {
8953
9047
  throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
8954
9048
  }
8955
9049
  const manifest = readPlanV2Manifest(planV2Dir);
8956
9050
  const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
9051
+ const remoteSources = options.remoteSources ?? {};
9052
+ assertRemoteSources(remoteSources, artifacts);
8957
9053
  const audioArtifact = artifacts.find((artifact) => isPlanAudioArtifactPath(artifact.path));
8958
- const verified = artifacts.map((artifact) => ({
9054
+ const verified = artifacts.filter((artifact) => !Object.hasOwn(remoteSources, artifact.path)).map((artifact) => ({
8959
9055
  artifact,
8960
9056
  sourcePath: verifyBlob(planV2Dir, artifact)
8961
9057
  }));
@@ -8970,7 +9066,11 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
8970
9066
  if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
8971
9067
  writeFileSync7(
8972
9068
  join23(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
8973
- canonicalJsonStringify({ manifest, target }),
9069
+ canonicalJsonStringify({
9070
+ manifest,
9071
+ target,
9072
+ ...Object.keys(remoteSources).length > 0 ? { remoteSources } : {}
9073
+ }),
8974
9074
  "utf-8"
8975
9075
  );
8976
9076
  renameSync5(tempDir, destinationDir);
@@ -8991,7 +9091,22 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
8991
9091
  audioPath: target.role === "assembler" ? audioArtifact ? join23(destinationDir, audioArtifact.path) : null : null
8992
9092
  };
8993
9093
  }
9094
+ function assertRemoteSources(remoteSources, artifacts) {
9095
+ if (!isRecord3(remoteSources)) throw new PlanV2IntegrityError("remoteSources must be an object");
9096
+ for (const [path, url] of Object.entries(remoteSources)) {
9097
+ const artifact = artifacts.find((candidate) => candidate.path === path);
9098
+ if (!artifact || !isPlanV2DeferredSourceArtifact(artifact)) {
9099
+ throw new PlanV2IntegrityError(`remote source is not a selected deferred video: ${path}`);
9100
+ }
9101
+ if (typeof url !== "string" || !/^https?:\/\//.test(url)) {
9102
+ throw new PlanV2IntegrityError(`remote source for ${path} must be an http(s) URL`);
9103
+ }
9104
+ }
9105
+ }
8994
9106
  function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
9107
+ return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
9108
+ }
9109
+ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
8995
9110
  const markerPath = join23(planDir, PLAN_V2_MATERIALIZATION_MARKER);
8996
9111
  if (!existsSync16(markerPath)) return null;
8997
9112
  const marker = readJsonFile(markerPath, "materialization marker");
@@ -9006,7 +9121,10 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
9006
9121
  }
9007
9122
  const manifest = parsePlanV2Manifest(marker.manifest);
9008
9123
  const required = listPlanV2ArtifactsForTarget(manifest, expectedTarget);
9124
+ const remoteSources = marker.remoteSources ?? {};
9125
+ assertRemoteSources(remoteSources, required);
9009
9126
  for (const artifact of required) {
9127
+ if (Object.hasOwn(remoteSources, artifact.path)) continue;
9010
9128
  const path = join23(planDir, ...artifact.path.split("/"));
9011
9129
  if (!existsSync16(path) || statSync6(path).size !== artifact.sizeBytes) {
9012
9130
  throw new PlanV2IntegrityError(
@@ -9017,7 +9135,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
9017
9135
  throw new PlanV2IntegrityError(`materialized artifact hash mismatch: ${artifact.path}`);
9018
9136
  }
9019
9137
  }
9020
- return manifest;
9138
+ return { manifest, remoteSources };
9021
9139
  }
9022
9140
 
9023
9141
  // src/services/distributed/planV2Execution.ts
@@ -9892,10 +10010,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9892
10010
  const plan2 = JSON.parse(readFileSync11(planJsonPath, "utf-8"));
9893
10011
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk);
9894
10012
  const planHashStarted = Date.now();
9895
- const v2Manifest = validatePlanV2MaterializedTarget(planDir, {
10013
+ const v2Materialization = readValidatedPlanV2Materialization(planDir, {
9896
10014
  role: "chunk",
9897
10015
  chunkIndex
9898
10016
  });
10017
+ const v2Manifest = v2Materialization?.manifest ?? null;
10018
+ const remoteSources = v2Materialization?.remoteSources ?? {};
9899
10019
  for (const required of [encoderJsonPath, chunksJsonPath]) {
9900
10020
  if (!existsSync18(required)) {
9901
10021
  throw new RenderChunkValidationError(
@@ -9997,6 +10117,24 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9997
10117
  const framesDir = join26(workDir, "captured-frames");
9998
10118
  mkdirSync13(framesDir, { recursive: true });
9999
10119
  const videoFramesDir = `${workDir}.video-frames`;
10120
+ const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
10121
+ planDir,
10122
+ planVideos,
10123
+ frames: Array.from(
10124
+ { length: slice.endFrame - slice.startFrame },
10125
+ (_, i) => slice.startFrame + i
10126
+ ),
10127
+ fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
10128
+ outputRoot: videoFramesDir,
10129
+ cfg,
10130
+ remoteSources
10131
+ }) : [];
10132
+ const remoteVideoIds = new Set(
10133
+ planVideos?.extracted.filter(
10134
+ (video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
10135
+ ).map((video) => video.videoId)
10136
+ );
10137
+ const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
10000
10138
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
10001
10139
  planVideos.videos,
10002
10140
  [
@@ -10005,17 +10143,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
10005
10143
  planVideos.extracted.filter((video) => !video.deferredRange),
10006
10144
  v2Manifest === null ? "dense-v1" : "sparse-v2"
10007
10145
  ),
10008
- ...await extractDeferredVideoFramesForChunk({
10009
- planDir,
10010
- planVideos,
10011
- frames: Array.from(
10012
- { length: slice.endFrame - slice.startFrame },
10013
- (_, i) => slice.startFrame + i
10014
- ),
10015
- fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
10016
- outputRoot: videoFramesDir,
10017
- cfg
10018
- })
10146
+ ...deferredVideoFrames
10019
10147
  ],
10020
10148
  resolveRenderFpsConfig(job.config.fps).value
10021
10149
  ) : null;
@@ -10033,7 +10161,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
10033
10161
  dimensions: plan2.dimensions,
10034
10162
  encoder,
10035
10163
  videoCount: planVideos?.videos.length ?? 0,
10036
- html: readFileSync11(join26(compiledDir, "index.html"), "utf-8")
10164
+ html: readFileSync11(join26(compiledDir, "index.html"), "utf-8"),
10165
+ // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
10166
+ skipReadinessVideoIds: deferredVideoFrames.filter((video) => video.framePaths.size === 0 || remoteVideoIds.has(video.videoId)).map((video) => video.videoId),
10167
+ videoMetadataHints: collectVideoMetadataHints(remoteVideos)
10037
10168
  });
10038
10169
  const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
10039
10170
  let session = null;
@@ -10594,28 +10725,46 @@ function validateIntDimension(field, value) {
10594
10725
  }
10595
10726
 
10596
10727
  // src/services/distributed/projectHash.ts
10597
- import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "node:fs";
10728
+ import { readdirSync as readdirSync10, readFileSync as readFileSync12, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
10598
10729
  import { createHash as createHash6 } from "node:crypto";
10599
- import { join as join28, relative as relative7 } from "node:path";
10600
- function hashProjectDir(projectDir) {
10601
- const hash = createHash6("sha256");
10730
+ import { join as join28 } from "node:path";
10731
+ var SITE_ID_SCHEME = "site-v2\0";
10732
+ function listSiteFiles(projectDir) {
10602
10733
  const files = [];
10603
- function walk(dir, isRoot) {
10604
- for (const entry of readdirSync10(dir, { withFileTypes: true }).sort(
10605
- (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
10606
- )) {
10607
- if (isRoot && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(entry.name)) continue;
10608
- const full = join28(dir, entry.name);
10609
- if (entry.isDirectory()) walk(full, false);
10610
- else if (entry.isFile()) files.push(full);
10734
+ const ancestors = /* @__PURE__ */ new Set([realpathSync2(projectDir)]);
10735
+ function walk(dir, rel) {
10736
+ for (const name of readdirSync10(dir).sort()) {
10737
+ if (rel === "" && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(name)) continue;
10738
+ const full = join28(dir, name);
10739
+ const relPath = rel === "" ? name : `${rel}/${name}`;
10740
+ let stat;
10741
+ try {
10742
+ stat = statSync8(full);
10743
+ } catch (err) {
10744
+ throw new Error(`[listSiteFiles] cannot read ${relPath} (dangling symlink?)`, {
10745
+ cause: err
10746
+ });
10747
+ }
10748
+ if (stat.isFile()) {
10749
+ files.push(relPath);
10750
+ } else if (stat.isDirectory()) {
10751
+ const real = realpathSync2(full);
10752
+ if (ancestors.has(real)) continue;
10753
+ ancestors.add(real);
10754
+ walk(full, relPath);
10755
+ ancestors.delete(real);
10756
+ }
10611
10757
  }
10612
10758
  }
10613
- walk(projectDir, true);
10614
- for (const file of files) {
10615
- const rel = relative7(projectDir, file).replaceAll("\\", "/");
10759
+ walk(projectDir, "");
10760
+ return files;
10761
+ }
10762
+ function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
10763
+ const hash = createHash6("sha256").update(SITE_ID_SCHEME);
10764
+ for (const rel of files) {
10616
10765
  hash.update(rel);
10617
10766
  hash.update("\0");
10618
- hash.update(readFileSync12(file));
10767
+ hash.update(readFileSync12(join28(projectDir, rel)));
10619
10768
  }
10620
10769
  return hash.digest("hex").slice(0, 16);
10621
10770
  }
@@ -10670,7 +10819,9 @@ export {
10670
10819
  getPlanV2ExecutionPlanHash,
10671
10820
  hashProjectDir,
10672
10821
  isPlanAudioArtifactPath,
10822
+ isPlanV2DeferredSourceArtifact,
10673
10823
  listPlanV2ArtifactsForTarget,
10824
+ listSiteFiles,
10674
10825
  materializePlanV2Target,
10675
10826
  measurePlanDirBytes,
10676
10827
  muxChunkWithAudio,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "231daa3aea863ffab8c1794254a07a9cfb36cbd599b0c8c7d4cb05a96818299b",
4
+ "sha256": "5da92c0e03e3d47df672f23707742908357e94fb7b39ed0f518d4c37400f433d",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"