@hyperframes/producer 0.8.174 → 0.8.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6923,7 +6923,7 @@ async function embedLocalFontFaces(html, projectDir) {
6923
6923
  embeddedPaths.add(localPath);
6924
6924
  continue;
6925
6925
  }
6926
- dataUri = await toDataUri(font.buffer, ext);
6926
+ dataUri = await toDataUri(font.buffer, ext, { compressToWoff2: false });
6927
6927
  dataUriByAbsolutePath.set(absPath, dataUri);
6928
6928
  defaultLogger.info(
6929
6929
  `[Compiler] Embedded local font file: ${localPath} (${(font.buffer.length / 1024).toFixed(0)} KB \u2192 data URI)`
@@ -13577,688 +13577,914 @@ import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframe
13577
13577
  import {
13578
13578
  copyFileSync as copyFileSync5,
13579
13579
  cpSync as cpSync2,
13580
- existsSync as existsSync19,
13580
+ existsSync as existsSync20,
13581
13581
  linkSync,
13582
13582
  mkdirSync as mkdirSync11,
13583
13583
  renameSync as renameSync5,
13584
13584
  rmSync as rmSync10,
13585
13585
  writeFileSync as writeFileSync7
13586
13586
  } from "node:fs";
13587
- import { extname as extname5, isAbsolute as isAbsolute3, join as join30, relative as relative5, sep as sep2 } from "node:path";
13588
- import { fpsToNumber as fpsToNumber10 } from "@hyperframes/core";
13587
+ import { extname as extname6, isAbsolute as isAbsolute3, join as join32, relative as relative5, sep as sep2 } from "node:path";
13588
+ import {
13589
+ fpsToNumber as fpsToNumber10,
13590
+ HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
13591
+ } from "@hyperframes/core";
13589
13592
  import {
13590
13593
  getEncoderPreset as getEncoderPreset4,
13591
13594
  normalizeVp9CpuUsed,
13592
13595
  resolveConfig as resolveConfig3
13593
13596
  } from "@hyperframes/engine";
13594
13597
 
13595
- // src/services/render/stages/freezePlan.ts
13596
- import { existsSync as existsSync17, mkdirSync as mkdirSync10, readFileSync as readFileSync9, readdirSync as readdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
13597
- import { join as join27, relative as relative3, resolve as resolve8 } from "node:path";
13598
+ // src/services/distributed/planFrameCost.ts
13599
+ import { join as join29 } from "node:path";
13600
+ import {
13601
+ closeCaptureSession as closeCaptureSession9,
13602
+ createCaptureSession as createCaptureSession8,
13603
+ createFrameLookupTable as createFrameLookupTable3,
13604
+ initializeSession as initializeSession8
13605
+ } from "@hyperframes/engine";
13598
13606
 
13599
- // src/services/distributed/planProtocol.ts
13600
- var PLAN_SCHEMA_VERSION = 1;
13601
- var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
13602
- var PLAN_HASH_SCHEMA = "hyperframes-plan-hash-v1";
13603
- var PLAN_V2_SCHEMA_VERSION = 2;
13604
- var PLAN_V2_ARTIFACT_LAYOUT = "content-addressed-plan-v2";
13605
- var PLAN_V2_HASH_SCHEMA = "hyperframes-plan-manifest-hash-v2";
13606
- var PLAN_PROTOCOL_UNSUPPORTED = "PLAN_PROTOCOL_UNSUPPORTED";
13607
- var PLAN_PROTOCOL_V1 = Object.freeze({
13608
- schemaVersion: PLAN_SCHEMA_VERSION,
13609
- artifactLayout: PLAN_ARTIFACT_LAYOUT,
13610
- hashSchema: PLAN_HASH_SCHEMA
13611
- });
13612
- var CURRENT_PLAN_PROTOCOL = PLAN_PROTOCOL_V1;
13613
- var PLAN_PROTOCOL_V2 = Object.freeze({
13614
- schemaVersion: PLAN_V2_SCHEMA_VERSION,
13615
- artifactLayout: PLAN_V2_ARTIFACT_LAYOUT,
13616
- hashSchema: PLAN_V2_HASH_SCHEMA
13617
- });
13618
- var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
13619
- roles: Object.freeze({
13620
- planner: Object.freeze({
13621
- produces: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2])
13622
- }),
13623
- chunk: Object.freeze({
13624
- accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
13625
- acceptsLegacyV1WithoutDescriptor: true
13626
- }),
13627
- assembler: Object.freeze({
13628
- accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
13629
- acceptsLegacyV1WithoutDescriptor: true
13630
- })
13631
- })
13632
- });
13633
- function getDistributedRenderCapabilities() {
13634
- return DISTRIBUTED_RENDER_CAPABILITIES;
13607
+ // src/services/distributed/chunkFrames.ts
13608
+ import { existsSync as existsSync18, readdirSync as readdirSync6 } from "node:fs";
13609
+ import { extname as extname4, join as join28 } from "node:path";
13610
+ import {
13611
+ compositionRequiresWebGpu as compositionRequiresWebGpu3,
13612
+ createFrameLookupTable as createFrameLookupTable2,
13613
+ createVideoFrameInjector as createVideoFrameInjector2,
13614
+ extractVideoFramesRange
13615
+ } from "@hyperframes/engine";
13616
+
13617
+ // src/services/distributed/shared.ts
13618
+ import { execFile as execFileCallback } from "node:child_process";
13619
+ import { dirname as dirname11, join as join27 } from "node:path";
13620
+ import { existsSync as existsSync17, readFileSync as readFileSync9 } from "node:fs";
13621
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
13622
+ import { promisify as promisify2 } from "node:util";
13623
+ import {
13624
+ getFfmpegBinary,
13625
+ MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME3
13626
+ } from "@hyperframes/engine";
13627
+ var PLAN_VIDEOS_META_RELATIVE_PATH = "meta/videos.json";
13628
+ var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
13629
+ var PLAN_AUDIO_LEGACY_RELATIVE_PATH = "audio.aac";
13630
+ function isPlanAudioArtifactPath(path) {
13631
+ return path === PLAN_AUDIO_RELATIVE_PATH || path === PLAN_AUDIO_LEGACY_RELATIVE_PATH;
13635
13632
  }
13636
- var PlanProtocolUnsupportedError = class extends Error {
13637
- // Public adapters inspect this typed code even though OSS producer does not.
13633
+ function resolvePlanAudioPath(planDir) {
13634
+ for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
13635
+ const candidate = join27(planDir, name);
13636
+ if (existsSync17(candidate)) return candidate;
13637
+ }
13638
+ return null;
13639
+ }
13640
+ var INVALID_VIDEO_METADATA = "INVALID_VIDEO_METADATA";
13641
+ var PlanVideosMetadataError = class extends Error {
13642
+ // Read by cloud adapters across the package boundary to classify retries.
13638
13643
  // fallow-ignore-next-line unused-class-member
13639
- code = PLAN_PROTOCOL_UNSUPPORTED;
13640
- constructor(reason) {
13641
- super(`[planProtocol] ${reason} (${PLAN_PROTOCOL_UNSUPPORTED})`);
13642
- this.name = "PlanProtocolUnsupportedError";
13644
+ code = INVALID_VIDEO_METADATA;
13645
+ constructor(message) {
13646
+ super(message);
13647
+ this.name = "PlanVideosMetadataError";
13643
13648
  }
13644
13649
  };
13645
- function isRecord(value) {
13646
- return value !== null && typeof value === "object" && !Array.isArray(value);
13647
- }
13648
- function protocolMatches(descriptor, expected) {
13649
- return descriptor.schemaVersion === expected.schemaVersion && descriptor.artifactLayout === expected.artifactLayout && descriptor.hashSchema === expected.hashSchema;
13650
- }
13651
- function capabilitiesAccept(capabilities, protocol) {
13652
- return capabilities.accepts.some(
13653
- (accepted) => accepted.schemaVersion === protocol.schemaVersion && accepted.artifactLayout === protocol.artifactLayout && accepted.hashSchema === protocol.hashSchema
13654
- );
13650
+ function metadataError(field, expectation) {
13651
+ throw new PlanVideosMetadataError(`${field} ${expectation}`);
13655
13652
  }
13656
- function readPlanProtocol(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
13657
- if (!isRecord(planJson)) {
13658
- throw new PlanProtocolUnsupportedError("plan.json must contain a JSON object");
13659
- }
13660
- if (!Object.prototype.hasOwnProperty.call(planJson, "protocol")) {
13661
- if (!capabilities.acceptsLegacyV1WithoutDescriptor || !capabilitiesAccept(capabilities, PLAN_PROTOCOL_V1)) {
13662
- throw new PlanProtocolUnsupportedError(
13663
- "legacy v1 plan without a protocol descriptor is not accepted by this worker"
13664
- );
13665
- }
13666
- return PLAN_PROTOCOL_V1;
13653
+ function readRecord(value, field) {
13654
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
13655
+ metadataError(field, "must be an object");
13667
13656
  }
13668
- const descriptor = planJson.protocol;
13669
- if (!isRecord(descriptor)) {
13670
- throw new PlanProtocolUnsupportedError("plan.json protocol descriptor must be an object");
13657
+ return value;
13658
+ }
13659
+ function readNonEmptyString(value, field) {
13660
+ if (typeof value !== "string" || value.length === 0) {
13661
+ metadataError(field, "must be a non-empty string");
13671
13662
  }
13672
- for (const field of ["schemaVersion", "artifactLayout", "hashSchema"]) {
13673
- if (!Object.prototype.hasOwnProperty.call(descriptor, field)) {
13674
- throw new PlanProtocolUnsupportedError(`plan.json protocol descriptor is missing ${field}`);
13675
- }
13663
+ return value;
13664
+ }
13665
+ function readString(value, field) {
13666
+ if (typeof value !== "string") {
13667
+ metadataError(field, "must be a string");
13676
13668
  }
13677
- const protocol = protocolMatches(descriptor, PLAN_PROTOCOL_V1) ? PLAN_PROTOCOL_V1 : protocolMatches(descriptor, PLAN_PROTOCOL_V2) ? PLAN_PROTOCOL_V2 : null;
13678
- if (protocol === null || !capabilitiesAccept(capabilities, protocol)) {
13679
- throw new PlanProtocolUnsupportedError("unsupported plan.json protocol descriptor");
13669
+ return value;
13670
+ }
13671
+ function readFiniteNumber(value, field) {
13672
+ if (typeof value !== "number" || !Number.isFinite(value)) {
13673
+ metadataError(field, "must be a finite number");
13680
13674
  }
13681
- return protocol;
13675
+ return value;
13682
13676
  }
13683
- function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
13684
- const protocol = readPlanProtocol(planJson, capabilities);
13685
- if (protocol !== PLAN_PROTOCOL_V1) {
13686
- throw new PlanProtocolUnsupportedError(
13687
- "content-addressed v2 plan must be materialized before v1 layout access"
13688
- );
13677
+ function readBoolean(value, field) {
13678
+ if (typeof value !== "boolean") {
13679
+ metadataError(field, "must be boolean");
13689
13680
  }
13690
- return PLAN_PROTOCOL_V1;
13681
+ return value;
13691
13682
  }
13692
-
13693
- // src/services/render/stages/planHash.ts
13694
- import { createHash as createHash6 } from "node:crypto";
13695
- import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync4, readSync as readSync3 } from "node:fs";
13696
- var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
13697
- var FIELD_DELIMITER = Buffer.from([0]);
13698
- function computePlanHash(input) {
13699
- const hash = createHash6("sha256");
13700
- hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
13701
- hash.update(input.compositionHtml);
13702
- hash.update(FIELD_DELIMITER);
13703
- const sortedAssets = [...input.assets].sort(
13704
- (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
13705
- );
13706
- for (const asset of sortedAssets) {
13707
- hash.update(asset.path, "utf8");
13708
- hash.update(FIELD_DELIMITER);
13709
- hash.update(asset.sha256, "utf8");
13710
- hash.update(FIELD_DELIMITER);
13683
+ function readPositiveInteger(value, field) {
13684
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
13685
+ metadataError(field, "must be a positive integer");
13711
13686
  }
13712
- hash.update(input.fontSnapshotSha, "utf8");
13713
- hash.update(FIELD_DELIMITER);
13714
- hash.update(input.encoderConfigCanonicalJson, "utf8");
13715
- hash.update(FIELD_DELIMITER);
13716
- hash.update(input.producerVersion, "utf8");
13717
- hash.update(FIELD_DELIMITER);
13718
- hash.update(input.ffmpegVersion, "utf8");
13719
- hash.update(FIELD_DELIMITER);
13720
- const d = input.dimensions;
13721
- hash.update(`${d.fpsNum}/${d.fpsDen}x${d.width}x${d.height}x${d.format}`, "utf8");
13722
- return hash.digest("hex");
13687
+ return value;
13723
13688
  }
13724
- function canonicalJsonStringify(value) {
13725
- if (value === null) return "null";
13726
- if (typeof value === "boolean") return value ? "true" : "false";
13727
- if (typeof value === "number") {
13728
- if (!Number.isFinite(value)) {
13729
- throw new TypeError(`canonicalJsonStringify: non-finite number ${value}`);
13730
- }
13731
- return JSON.stringify(value);
13689
+ function readNonNegativeInteger(value, field) {
13690
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
13691
+ metadataError(field, "must be a non-negative integer");
13732
13692
  }
13733
- if (typeof value === "string") return JSON.stringify(value);
13734
- if (Array.isArray(value)) {
13735
- return `[${value.map(canonicalJsonStringify).join(",")}]`;
13693
+ return value;
13694
+ }
13695
+ function readDeferredFrameRange(value, field) {
13696
+ const range = readRecord(value, field);
13697
+ const sourcePath = readNonEmptyString(range.sourcePath, `${field}.sourcePath`);
13698
+ if (sourcePath.startsWith("/") || sourcePath.split(/[\\/]/).some((part) => part === "..")) {
13699
+ metadataError(`${field}.sourcePath`, "must stay inside the plan directory");
13736
13700
  }
13737
- if (typeof value === "object") {
13738
- const obj = value;
13739
- const keys = Object.keys(obj).sort();
13740
- const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJsonStringify(obj[k])}`);
13741
- return `{${parts.join(",")}}`;
13701
+ const durationSeconds = readFiniteNumber(range.durationSeconds, `${field}.durationSeconds`);
13702
+ if (durationSeconds <= 0) metadataError(`${field}.durationSeconds`, "must be positive");
13703
+ if (range.format !== "jpg" && range.format !== "png") {
13704
+ metadataError(`${field}.format`, 'must be "jpg" or "png"');
13742
13705
  }
13743
- throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
13744
- }
13745
- function sha256Hex(bytes) {
13746
- const h = createHash6("sha256");
13747
- h.update(bytes);
13748
- return h.digest("hex");
13706
+ return {
13707
+ sourcePath,
13708
+ startTime: readFiniteNumber(range.startTime, `${field}.startTime`),
13709
+ durationSeconds,
13710
+ format: range.format
13711
+ };
13749
13712
  }
13750
- function sha256File(path) {
13751
- const hash = createHash6("sha256");
13752
- const buffer = Buffer.allocUnsafe(1024 * 1024);
13753
- const fd = openSync4(path, "r");
13754
- try {
13755
- let bytesRead = 0;
13756
- do {
13757
- bytesRead = readSync3(fd, buffer, 0, buffer.byteLength, null);
13758
- if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
13759
- } while (bytesRead > 0);
13760
- } finally {
13761
- closeSync5(fd);
13713
+ function readRateSpec(value, field) {
13714
+ if (value === void 0) return void 0;
13715
+ if (typeof value === "number") return readFiniteNumber(value, field);
13716
+ const lane = readRecord(value, field);
13717
+ if (!Array.isArray(lane.points)) {
13718
+ metadataError(`${field}.points`, "must be an array");
13762
13719
  }
13763
- return hash.digest("hex");
13720
+ return lane;
13764
13721
  }
13765
- async function sha256FileAsync(path) {
13766
- const hash = createHash6("sha256");
13767
- for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
13768
- hash.update(chunk);
13722
+ function readVideoMetadata(value, field) {
13723
+ const record = readRecord(value, field);
13724
+ let colorSpace = null;
13725
+ if (record.colorSpace !== null) {
13726
+ const color = readRecord(record.colorSpace, `${field}.colorSpace`);
13727
+ colorSpace = {
13728
+ colorTransfer: readString(color.colorTransfer, `${field}.colorSpace.colorTransfer`),
13729
+ colorPrimaries: readString(color.colorPrimaries, `${field}.colorSpace.colorPrimaries`),
13730
+ colorSpace: readString(color.colorSpace, `${field}.colorSpace.colorSpace`)
13731
+ };
13769
13732
  }
13770
- return hash.digest("hex");
13733
+ return {
13734
+ durationSeconds: readFiniteNumber(record.durationSeconds, `${field}.durationSeconds`),
13735
+ videoStreamDurationSeconds: readFiniteNumber(
13736
+ record.videoStreamDurationSeconds,
13737
+ `${field}.videoStreamDurationSeconds`
13738
+ ),
13739
+ // Plans written before stream-start metadata existed implicitly used the
13740
+ // overwhelmingly common start-at-zero domain. Preserve that compatibility
13741
+ // while carrying non-zero edit-list/transport timestamps in new plans.
13742
+ videoStreamStartSeconds: record.videoStreamStartSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamStartSeconds, `${field}.videoStreamStartSeconds`),
13743
+ videoStreamLeadSeconds: record.videoStreamLeadSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamLeadSeconds, `${field}.videoStreamLeadSeconds`),
13744
+ width: readPositiveInteger(record.width, `${field}.width`),
13745
+ height: readPositiveInteger(record.height, `${field}.height`),
13746
+ fps: readFiniteNumber(record.fps, `${field}.fps`),
13747
+ videoCodec: readNonEmptyString(record.videoCodec, `${field}.videoCodec`),
13748
+ hasAudio: readBoolean(record.hasAudio, `${field}.hasAudio`),
13749
+ isVFR: readBoolean(record.isVFR, `${field}.isVFR`),
13750
+ hasAlpha: readBoolean(record.hasAlpha, `${field}.hasAlpha`),
13751
+ colorSpace
13752
+ };
13771
13753
  }
13772
-
13773
- // src/services/render/stages/freezePlan.ts
13774
- var COMPILED_INDEX_RELATIVE_PATH = "compiled/index.html";
13775
- var HASH_EXCLUDED_PLAN_FILES = /* @__PURE__ */ new Set([
13776
- "plan.json",
13777
- "meta/encoder.json",
13778
- COMPILED_INDEX_RELATIVE_PATH
13779
- ]);
13780
- function planFileSha256(absolutePath, digests) {
13781
- let sha256 = digests?.get(absolutePath);
13782
- if (sha256 === void 0) {
13783
- sha256 = sha256File(absolutePath);
13784
- digests?.set(absolutePath, sha256);
13754
+ function parsePlanVideosJson(value) {
13755
+ const record = readRecord(value, "meta/videos.json");
13756
+ if (!Array.isArray(record.videos) || !Array.isArray(record.extracted)) {
13757
+ metadataError("meta/videos.json", "must contain videos and extracted arrays");
13785
13758
  }
13786
- return sha256;
13787
- }
13788
- async function prehashPlanFiles(dir, digests) {
13789
- if (!existsSync17(dir)) return;
13790
- for (const { absolutePath } of listPlanFiles(dir)) {
13791
- if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
13759
+ const videos = record.videos.map((value2, index) => {
13760
+ const field = `meta/videos.json.videos[${index}]`;
13761
+ const video = readRecord(value2, field);
13762
+ return {
13763
+ id: readNonEmptyString(video.id, `${field}.id`),
13764
+ src: readNonEmptyString(video.src, `${field}.src`),
13765
+ start: readFiniteNumber(video.start, `${field}.start`),
13766
+ end: readFiniteNumber(video.end, `${field}.end`),
13767
+ mediaStart: readFiniteNumber(video.mediaStart, `${field}.mediaStart`),
13768
+ playbackRate: readRateSpec(video.playbackRate, `${field}.playbackRate`),
13769
+ loop: readBoolean(video.loop, `${field}.loop`),
13770
+ hasAudio: readBoolean(video.hasAudio, `${field}.hasAudio`)
13771
+ };
13772
+ });
13773
+ const extracted = record.extracted.map((value2, index) => {
13774
+ const field = `meta/videos.json.extracted[${index}]`;
13775
+ const entry = readRecord(value2, field);
13776
+ return {
13777
+ videoId: readNonEmptyString(entry.videoId, `${field}.videoId`),
13778
+ srcPath: readNonEmptyString(entry.srcPath, `${field}.srcPath`),
13779
+ framePattern: readNonEmptyString(entry.framePattern, `${field}.framePattern`),
13780
+ fps: readFiniteNumber(entry.fps, `${field}.fps`),
13781
+ totalFrames: readNonNegativeInteger(entry.totalFrames, `${field}.totalFrames`),
13782
+ metadata: readVideoMetadata(entry.metadata, `${field}.metadata`),
13783
+ ...entry.deferredRange === void 0 ? {} : { deferredRange: readDeferredFrameRange(entry.deferredRange, `${field}.deferredRange`) }
13784
+ };
13785
+ });
13786
+ const videoIds = /* @__PURE__ */ new Set();
13787
+ for (const video of videos) {
13788
+ if (videoIds.has(video.id)) {
13789
+ metadataError("meta/videos.json.videos", `contains duplicate id ${JSON.stringify(video.id)}`);
13790
+ }
13791
+ videoIds.add(video.id);
13792
13792
  }
13793
- }
13794
- function stripUndefined(value) {
13795
- if (Array.isArray(value)) return value.map(stripUndefined);
13796
- if (value !== null && typeof value === "object") {
13797
- const obj = value;
13798
- const out = {};
13799
- for (const key of Object.keys(obj)) {
13800
- const v = obj[key];
13801
- if (v === void 0) continue;
13802
- out[key] = stripUndefined(v);
13793
+ const extractedIds = /* @__PURE__ */ new Set();
13794
+ for (const entry of extracted) {
13795
+ if (extractedIds.has(entry.videoId)) {
13796
+ metadataError(
13797
+ "meta/videos.json.extracted",
13798
+ `contains duplicate videoId ${JSON.stringify(entry.videoId)}`
13799
+ );
13800
+ }
13801
+ extractedIds.add(entry.videoId);
13802
+ if (!videoIds.has(entry.videoId)) {
13803
+ metadataError(
13804
+ "meta/videos.json.extracted",
13805
+ `references undeclared video ${JSON.stringify(entry.videoId)}`
13806
+ );
13803
13807
  }
13804
- return out;
13805
13808
  }
13806
- return value;
13807
- }
13808
- function listPlanFiles(planDir) {
13809
- const results = [];
13810
- const rootResolved = resolve8(planDir);
13811
- function walk(dir) {
13812
- const entries = readdirSync6(dir, { withFileTypes: true });
13813
- for (const entry of entries) {
13814
- const full = join27(dir, entry.name);
13815
- if (entry.isDirectory()) {
13816
- walk(full);
13817
- } else if (entry.isFile()) {
13818
- results.push({
13819
- planRelativePath: relative3(rootResolved, full).split(/[\\/]+/).join("/"),
13820
- absolutePath: full
13821
- });
13822
- }
13809
+ for (const video of videos) {
13810
+ if (!extractedIds.has(video.id)) {
13811
+ metadataError(
13812
+ "meta/videos.json.extracted",
13813
+ `is missing declared video ${JSON.stringify(video.id)}`
13814
+ );
13823
13815
  }
13824
13816
  }
13825
- walk(rootResolved);
13826
- results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
13827
- return results;
13817
+ return { videos, extracted };
13828
13818
  }
13829
- function collectPlanAssetShas(planDir, digests) {
13830
- const files = listPlanFiles(planDir);
13831
- let compositionHtml = null;
13832
- const assets = [];
13833
- for (const file of files) {
13834
- if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
13835
- compositionHtml = readFileSync9(file.absolutePath);
13836
- continue;
13819
+ function buildPlanVideosJson(input) {
13820
+ const videos = input.videos.map((video, index) => {
13821
+ if (Number.isFinite(video.end)) return { ...video };
13822
+ if (!Number.isFinite(input.compositionEnd) || input.compositionEnd <= 0 || input.compositionEnd <= video.start) {
13823
+ metadataError(
13824
+ `meta/videos.json.videos[${index}].end`,
13825
+ "cannot be resolved without a finite composition end after its start"
13826
+ );
13837
13827
  }
13838
- if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
13839
- assets.push({
13840
- path: file.planRelativePath,
13841
- sha256: planFileSha256(file.absolutePath, digests)
13842
- });
13843
- }
13844
- if (compositionHtml === null) {
13845
- throw new Error(
13846
- `[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH} (upstream compile stage did not materialize the expected file).`
13847
- );
13848
- }
13849
- return { compositionHtml, assets };
13828
+ return { ...video, end: input.compositionEnd };
13829
+ });
13830
+ return parsePlanVideosJson({ videos, extracted: input.extracted });
13850
13831
  }
13851
- function recomputePlanHashFromPlanDir(planDir, digests) {
13852
- const planJsonPath = join27(planDir, "plan.json");
13853
- const encoderJsonPath = join27(planDir, "meta", "encoder.json");
13854
- if (!existsSync17(planJsonPath)) {
13855
- throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
13832
+ var execFile = promisify2(execFileCallback);
13833
+ var cachedFfmpegVersions = /* @__PURE__ */ new Map();
13834
+ async function readFfmpegVersion() {
13835
+ const binary = getFfmpegBinary();
13836
+ const cached = cachedFfmpegVersions.get(binary);
13837
+ if (cached !== void 0) return cached;
13838
+ let stdout;
13839
+ try {
13840
+ ({ stdout } = await execFile(binary, ["-version"], {
13841
+ maxBuffer: 1024 * 1024,
13842
+ // See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
13843
+ windowsHide: true
13844
+ }));
13845
+ } catch (error) {
13846
+ if (error.code !== "ENOENT") throw error;
13847
+ throw Object.assign(
13848
+ new Error(`ffmpeg not found at "${binary}": install FFmpeg or set HYPERFRAMES_FFMPEG_PATH.`, {
13849
+ cause: error
13850
+ }),
13851
+ { code: "ENOENT" }
13852
+ );
13856
13853
  }
13857
- if (!existsSync17(encoderJsonPath)) {
13858
- throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
13854
+ const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
13855
+ if (!firstLine) {
13856
+ throw new Error("ffmpeg -version returned empty output");
13859
13857
  }
13860
- const planJson = JSON.parse(readFileSync9(planJsonPath, "utf-8"));
13861
- const encoderConfigCanonicalJson = readFileSync9(encoderJsonPath, "utf-8");
13862
- const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
13863
- return computePlanHash({
13864
- compositionHtml,
13865
- assets,
13866
- fontSnapshotSha: planJson.fontSnapshotSha,
13867
- encoderConfigCanonicalJson,
13868
- producerVersion: planJson.producerVersion,
13869
- ffmpegVersion: planJson.ffmpegVersion,
13870
- dimensions: planJson.dimensions
13871
- });
13858
+ cachedFfmpegVersions.set(binary, firstLine);
13859
+ return firstLine;
13872
13860
  }
13873
- async function freezePlan(input) {
13874
- const {
13875
- planDir,
13876
- composition,
13877
- encoder,
13878
- chunks,
13879
- dimensions,
13880
- producerVersion,
13881
- fontSnapshotSha,
13882
- durationSeconds,
13883
- totalFrames,
13884
- hasAudio,
13885
- digests
13886
- } = input;
13887
- if (!existsSync17(planDir)) {
13888
- throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
13889
- }
13890
- const metaDir = join27(planDir, "meta");
13891
- if (!existsSync17(metaDir)) mkdirSync10(metaDir, { recursive: true });
13892
- writeFileSync6(
13893
- join27(metaDir, "composition.json"),
13894
- `${JSON.stringify(composition, null, 2)}
13895
- `,
13896
- "utf-8"
13897
- );
13898
- const encoderForCanonical = stripUndefined(encoder);
13899
- const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
13900
- writeFileSync6(join27(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
13901
- writeFileSync6(join27(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
13902
- `, "utf-8");
13903
- const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
13904
- const planHash = computePlanHash({
13905
- compositionHtml,
13906
- assets,
13907
- fontSnapshotSha,
13908
- encoderConfigCanonicalJson,
13909
- producerVersion,
13910
- ffmpegVersion: encoder.ffmpegVersion,
13911
- dimensions
13912
- });
13913
- const planJson = {
13914
- protocol: PLAN_PROTOCOL_V1,
13915
- planHash,
13916
- producerVersion,
13917
- ffmpegVersion: encoder.ffmpegVersion,
13918
- fontSnapshotSha,
13919
- dimensions,
13920
- chunkCount: chunks.length,
13921
- totalFrames,
13922
- duration: durationSeconds,
13923
- hasAudio
13861
+ function buildSyntheticRenderJob(input) {
13862
+ const renderConfig = {
13863
+ fps: input.fps,
13864
+ quality: input.quality,
13865
+ format: input.format,
13866
+ crf: input.crf,
13867
+ videoBitrate: input.bitrate,
13868
+ videoFrameFormat: input.videoFrameFormat,
13869
+ outputResolution: input.outputResolution,
13870
+ outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
13871
+ // Distributed mode hard-pins to software GPU. The plan-time validator
13872
+ // refuses to fan out otherwise.
13873
+ useGpu: false,
13874
+ debug: false,
13875
+ entryFile: input.entryFile,
13876
+ logger: input.logger ?? defaultLogger,
13877
+ hdrMode: input.hdrMode,
13878
+ strictness: input.strictness,
13879
+ producerConfig: input.producerConfig,
13880
+ variables: input.variables
13924
13881
  };
13925
- const planJsonPath = join27(planDir, "plan.json");
13926
- writeFileSync6(planJsonPath, `${JSON.stringify(planJson, null, 2)}
13927
- `, "utf-8");
13928
- return { planJsonPath, planHash };
13882
+ return createRenderJob(renderConfig);
13883
+ }
13884
+ var cachedProducerVersion = null;
13885
+ function readProducerVersion() {
13886
+ if (cachedProducerVersion !== null) return cachedProducerVersion;
13887
+ const startDir = dirname11(fileURLToPath5(import.meta.url));
13888
+ let current = startDir;
13889
+ for (let i = 0; i < 10; i++) {
13890
+ const candidate = join27(current, "package.json");
13891
+ if (existsSync17(candidate)) {
13892
+ try {
13893
+ const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
13894
+ if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
13895
+ cachedProducerVersion = pkg.version;
13896
+ return pkg.version;
13897
+ }
13898
+ } catch {
13899
+ }
13900
+ }
13901
+ const parent = dirname11(current);
13902
+ if (parent === current) break;
13903
+ current = parent;
13904
+ }
13905
+ cachedProducerVersion = "0.0.0-unknown";
13906
+ return cachedProducerVersion;
13929
13907
  }
13930
13908
 
13931
- // src/services/render/runtimeEnvSnapshot.ts
13932
- var RUNTIME_ENV_PREFIXES = [
13933
- "PRODUCER_RUNTIME_",
13934
- "PRODUCER_RENDER_"
13935
- ];
13936
- function snapshotRuntimeEnv(env = process.env) {
13937
- const snapshot = {};
13938
- for (const key of Object.keys(env)) {
13939
- const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
13940
- if (!matches) continue;
13941
- const value = env[key];
13942
- if (typeof value !== "string") continue;
13943
- snapshot[key] = value;
13909
+ // src/services/distributed/chunkFrames.ts
13910
+ var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
13911
+ var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
13912
+ var MISSING_PLAN_ARTIFACT = "MISSING_PLAN_ARTIFACT";
13913
+ var CHUNK_INDEX_OUT_OF_RANGE = "CHUNK_INDEX_OUT_OF_RANGE";
13914
+ var MISSING_RUNTIME_ENV_SNAPSHOT = "MISSING_RUNTIME_ENV_SNAPSHOT";
13915
+ var RenderChunkValidationError = class extends Error {
13916
+ code;
13917
+ constructor(code, message) {
13918
+ super(message);
13919
+ this.name = "RenderChunkValidationError";
13920
+ this.code = code;
13944
13921
  }
13945
- return snapshot;
13922
+ };
13923
+ function createChunkVideoFrameInjectorFactory(frameLookup) {
13924
+ return () => createVideoFrameInjector2(frameLookup);
13946
13925
  }
13947
- function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
13948
- const previous = {};
13949
- for (const [key, value] of Object.entries(snapshot)) {
13950
- if (typeof value !== "string") continue;
13951
- const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
13952
- if (!matches) continue;
13953
- previous[key] = env[key];
13954
- env[key] = value;
13926
+ function frameNumberFromFileName(name) {
13927
+ const match = /(\d+)(?=\.[^.]+$)/.exec(name);
13928
+ if (!match) return null;
13929
+ const frameNumber = Number(match[1]);
13930
+ return Number.isSafeInteger(frameNumber) ? frameNumber : null;
13931
+ }
13932
+ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
13933
+ const result = [];
13934
+ for (const v of videos) {
13935
+ const outputDir = join28(planDir, "video-frames", v.videoId);
13936
+ if (!existsSync18(outputDir)) {
13937
+ throw new Error(
13938
+ `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
13939
+ );
13940
+ }
13941
+ const ext = (extname4(v.framePattern) || ".jpg").toLowerCase();
13942
+ const frames = readdirSync6(outputDir).filter((name) => name.toLowerCase().endsWith(ext)).sort((left, right) => {
13943
+ const leftNumber = frameNumberFromFileName(left);
13944
+ const rightNumber = frameNumberFromFileName(right);
13945
+ if (leftNumber === null || rightNumber === null) return left.localeCompare(right);
13946
+ return leftNumber - rightNumber || left.localeCompare(right);
13947
+ });
13948
+ const framePaths = /* @__PURE__ */ new Map();
13949
+ for (let i = 0; i < frames.length; i++) {
13950
+ const frameName = frames[i];
13951
+ if (!frameName) continue;
13952
+ const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
13953
+ const frameIndex = frameNumber === null ? i : frameNumber - 1;
13954
+ framePaths.set(frameIndex, join28(outputDir, frameName));
13955
+ }
13956
+ result.push({
13957
+ videoId: v.videoId,
13958
+ srcPath: v.srcPath,
13959
+ outputDir,
13960
+ framePattern: v.framePattern,
13961
+ fps: v.fps,
13962
+ totalFrames: v.totalFrames,
13963
+ metadata: v.metadata,
13964
+ framePaths,
13965
+ // The planDir is shared read-only across workers; cleanup must not delete its frames.
13966
+ ownedByLookup: false
13967
+ });
13955
13968
  }
13969
+ return result;
13970
+ }
13971
+ function contiguousRuns(sortedIndexes) {
13972
+ const runs2 = [];
13973
+ for (const index of sortedIndexes) {
13974
+ const last = runs2[runs2.length - 1];
13975
+ if (last && last.firstFrame + last.frames === index) last.frames++;
13976
+ else runs2.push({ firstFrame: index, frames: 1 });
13977
+ }
13978
+ return runs2;
13979
+ }
13980
+ function buildChunkCaptureOptions(input) {
13981
+ const { dimensions, encoder } = input;
13982
+ const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(input.videoCount);
13956
13983
  return {
13957
- restore: () => {
13958
- for (const [key, value] of Object.entries(previous)) {
13959
- if (value === void 0) delete env[key];
13960
- else env[key] = value;
13984
+ width: dimensions.width,
13985
+ height: dimensions.height,
13986
+ fps: { num: dimensions.fpsNum, den: dimensions.fpsDen },
13987
+ format: dimensions.format === "mp4" ? "jpeg" : "png",
13988
+ quality: dimensions.format === "mp4" ? resolveCaptureJpegQuality(encoder.quality) : void 0,
13989
+ deviceScaleFactor: encoder.deviceScaleFactor,
13990
+ // The controller's snapshot, so the first capture sees the in-process renderer's `__hfVariables`.
13991
+ variables: encoder.variables,
13992
+ subTimelineWaitMemo: {},
13993
+ ...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
13994
+ // A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
13995
+ lockWarmupTicks: true,
13996
+ requiresWebGpu: compositionRequiresWebGpu3(input.html)
13997
+ };
13998
+ }
13999
+ async function extractDeferredVideoFramesForChunk(input) {
14000
+ const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
14001
+ if (deferred.length === 0) return [];
14002
+ const table = createFrameLookupTable2(
14003
+ input.planVideos.videos,
14004
+ deferred.map((video) => ({
14005
+ ...video,
14006
+ outputDir: input.outputRoot,
14007
+ framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
14008
+ })),
14009
+ resolveRenderFpsConfig(input.fps).value
14010
+ );
14011
+ const shown = /* @__PURE__ */ new Map();
14012
+ for (const frame of input.frames) {
14013
+ const globalTime = frame * input.fps.den / input.fps.num;
14014
+ for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
14015
+ const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
14016
+ indexes.add(payload.frameIndex);
14017
+ shown.set(videoId, indexes);
14018
+ }
14019
+ }
14020
+ return Promise.all(
14021
+ deferred.map(async (video) => {
14022
+ const outputDir = join28(input.outputRoot, video.videoId);
14023
+ const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
14024
+ if (indexes.length === 0) {
14025
+ return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
14026
+ }
14027
+ const range = video.deferredRange;
14028
+ const sourcePath = join28(input.planDir, ...range.sourcePath.split("/"));
14029
+ if (!existsSync18(sourcePath)) {
14030
+ throw new RenderChunkValidationError(
14031
+ MISSING_PLAN_ARTIFACT,
14032
+ `[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
14033
+ );
14034
+ }
14035
+ const extracted = await extractVideoFramesRange(
14036
+ sourcePath,
14037
+ video.videoId,
14038
+ range.startTime,
14039
+ range.durationSeconds,
14040
+ {
14041
+ fps: input.fps,
14042
+ outputDir: input.outputRoot,
14043
+ format: range.format,
14044
+ frameRanges: contiguousRuns(indexes)
14045
+ },
14046
+ void 0,
14047
+ input.cfg
14048
+ );
14049
+ const missing = indexes.find((index) => !extracted.framePaths.has(index));
14050
+ if (missing !== void 0) {
14051
+ throw new RenderChunkValidationError(
14052
+ INVALID_VIDEO_METADATA,
14053
+ `[renderChunk] video ${JSON.stringify(video.videoId)} has no frame ${missing}; the plan expected ${video.totalFrames} frames from ${range.sourcePath}.`
14054
+ );
13961
14055
  }
13962
- }
13963
- };
14056
+ return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
14057
+ })
14058
+ );
13964
14059
  }
13965
14060
 
13966
- // src/services/distributed/shared.ts
13967
- import { execFile as execFileCallback } from "node:child_process";
13968
- import { dirname as dirname11, join as join28 } from "node:path";
13969
- import { existsSync as existsSync18, readFileSync as readFileSync10 } from "node:fs";
13970
- import { fileURLToPath as fileURLToPath5 } from "node:url";
13971
- import { promisify as promisify2 } from "node:util";
13972
- import {
13973
- getFfmpegBinary,
13974
- MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME3
13975
- } from "@hyperframes/engine";
13976
- var PLAN_VIDEOS_META_RELATIVE_PATH = "meta/videos.json";
13977
- var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
13978
- var PLAN_AUDIO_LEGACY_RELATIVE_PATH = "audio.aac";
13979
- function isPlanAudioArtifactPath(path) {
13980
- return path === PLAN_AUDIO_RELATIVE_PATH || path === PLAN_AUDIO_LEGACY_RELATIVE_PATH;
13981
- }
13982
- function resolvePlanAudioPath(planDir) {
13983
- for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
13984
- const candidate = join28(planDir, name);
13985
- if (existsSync18(candidate)) return candidate;
14061
+ // src/services/distributed/planFrameCost.ts
14062
+ async function measurePlanSecondsPerFrame(input) {
14063
+ const { planDir, workDir, planVideos, fps, log } = input;
14064
+ const fpsRatio = { num: fps, den: 1 };
14065
+ const sampled = selectCaptureCalibrationFrames(input.totalFrames);
14066
+ const extracted = [
14067
+ ...rebuildExtractedFramesFromPlanDir(
14068
+ planDir,
14069
+ planVideos.extracted.filter((video) => !video.deferredRange)
14070
+ ),
14071
+ ...await extractDeferredVideoFramesForChunk({
14072
+ planDir,
14073
+ planVideos,
14074
+ frames: sampled,
14075
+ fps: fpsRatio,
14076
+ outputRoot: join29(workDir, "frame-cost-video-frames"),
14077
+ cfg: input.cfg
14078
+ })
14079
+ ];
14080
+ const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
14081
+ const compiledDir = join29(planDir, "compiled");
14082
+ const fileServer = await createFileServer({
14083
+ projectDir: compiledDir,
14084
+ compiledDir,
14085
+ port: 0,
14086
+ preHeadScripts: [buildVirtualTimeShim({ seedRandomFromFrame: true })],
14087
+ fps: fpsRatio
14088
+ });
14089
+ let session = null;
14090
+ try {
14091
+ session = await createCaptureSession8(
14092
+ fileServer.url,
14093
+ join29(workDir, "frame-cost"),
14094
+ buildChunkCaptureOptions({
14095
+ dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
14096
+ encoder: input,
14097
+ videoCount: planVideos.videos.length,
14098
+ html: input.html
14099
+ }),
14100
+ createChunkVideoFrameInjectorFactory(lookup)(),
14101
+ createCaptureCalibrationConfig(input.cfg)
14102
+ );
14103
+ await initializeSession8(session);
14104
+ const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
14105
+ return (estimate.p95Ms ?? 0) / 1e3;
14106
+ } finally {
14107
+ if (session) await closeCaptureSession9(session).catch(() => {
14108
+ });
14109
+ closeFileServerSafely(fileServer, "plan frame cost", log);
13986
14110
  }
13987
- return null;
13988
14111
  }
13989
- var INVALID_VIDEO_METADATA = "INVALID_VIDEO_METADATA";
13990
- var PlanVideosMetadataError = class extends Error {
13991
- // Read by cloud adapters across the package boundary to classify retries.
14112
+
14113
+ // src/services/render/stages/freezePlan.ts
14114
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
14115
+ import { join as join30, relative as relative3, resolve as resolve8 } from "node:path";
14116
+
14117
+ // src/services/distributed/planProtocol.ts
14118
+ var PLAN_SCHEMA_VERSION = 1;
14119
+ var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
14120
+ var PLAN_HASH_SCHEMA = "hyperframes-plan-hash-v1";
14121
+ var PLAN_V2_SCHEMA_VERSION = 2;
14122
+ var PLAN_V2_ARTIFACT_LAYOUT = "content-addressed-plan-v2";
14123
+ var PLAN_V2_HASH_SCHEMA = "hyperframes-plan-manifest-hash-v2";
14124
+ var PLAN_PROTOCOL_UNSUPPORTED = "PLAN_PROTOCOL_UNSUPPORTED";
14125
+ var PLAN_PROTOCOL_V1 = Object.freeze({
14126
+ schemaVersion: PLAN_SCHEMA_VERSION,
14127
+ artifactLayout: PLAN_ARTIFACT_LAYOUT,
14128
+ hashSchema: PLAN_HASH_SCHEMA
14129
+ });
14130
+ var CURRENT_PLAN_PROTOCOL = PLAN_PROTOCOL_V1;
14131
+ var PLAN_PROTOCOL_V2 = Object.freeze({
14132
+ schemaVersion: PLAN_V2_SCHEMA_VERSION,
14133
+ artifactLayout: PLAN_V2_ARTIFACT_LAYOUT,
14134
+ hashSchema: PLAN_V2_HASH_SCHEMA
14135
+ });
14136
+ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
14137
+ roles: Object.freeze({
14138
+ planner: Object.freeze({
14139
+ produces: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2])
14140
+ }),
14141
+ chunk: Object.freeze({
14142
+ accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
14143
+ acceptsLegacyV1WithoutDescriptor: true
14144
+ }),
14145
+ assembler: Object.freeze({
14146
+ accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
14147
+ acceptsLegacyV1WithoutDescriptor: true
14148
+ })
14149
+ })
14150
+ });
14151
+ function getDistributedRenderCapabilities() {
14152
+ return DISTRIBUTED_RENDER_CAPABILITIES;
14153
+ }
14154
+ var PlanProtocolUnsupportedError = class extends Error {
14155
+ // Public adapters inspect this typed code even though OSS producer does not.
13992
14156
  // fallow-ignore-next-line unused-class-member
13993
- code = INVALID_VIDEO_METADATA;
13994
- constructor(message) {
13995
- super(message);
13996
- this.name = "PlanVideosMetadataError";
14157
+ code = PLAN_PROTOCOL_UNSUPPORTED;
14158
+ constructor(reason) {
14159
+ super(`[planProtocol] ${reason} (${PLAN_PROTOCOL_UNSUPPORTED})`);
14160
+ this.name = "PlanProtocolUnsupportedError";
13997
14161
  }
13998
14162
  };
13999
- function metadataError(field, expectation) {
14000
- throw new PlanVideosMetadataError(`${field} ${expectation}`);
14163
+ function isRecord(value) {
14164
+ return value !== null && typeof value === "object" && !Array.isArray(value);
14001
14165
  }
14002
- function readRecord(value, field) {
14003
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
14004
- metadataError(field, "must be an object");
14005
- }
14006
- return value;
14166
+ function protocolMatches(descriptor, expected) {
14167
+ return descriptor.schemaVersion === expected.schemaVersion && descriptor.artifactLayout === expected.artifactLayout && descriptor.hashSchema === expected.hashSchema;
14007
14168
  }
14008
- function readNonEmptyString(value, field) {
14009
- if (typeof value !== "string" || value.length === 0) {
14010
- metadataError(field, "must be a non-empty string");
14011
- }
14012
- return value;
14169
+ function capabilitiesAccept(capabilities, protocol) {
14170
+ return capabilities.accepts.some(
14171
+ (accepted) => accepted.schemaVersion === protocol.schemaVersion && accepted.artifactLayout === protocol.artifactLayout && accepted.hashSchema === protocol.hashSchema
14172
+ );
14013
14173
  }
14014
- function readString(value, field) {
14015
- if (typeof value !== "string") {
14016
- metadataError(field, "must be a string");
14174
+ function readPlanProtocol(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
14175
+ if (!isRecord(planJson)) {
14176
+ throw new PlanProtocolUnsupportedError("plan.json must contain a JSON object");
14017
14177
  }
14018
- return value;
14019
- }
14020
- function readFiniteNumber(value, field) {
14021
- if (typeof value !== "number" || !Number.isFinite(value)) {
14022
- metadataError(field, "must be a finite number");
14178
+ if (!Object.prototype.hasOwnProperty.call(planJson, "protocol")) {
14179
+ if (!capabilities.acceptsLegacyV1WithoutDescriptor || !capabilitiesAccept(capabilities, PLAN_PROTOCOL_V1)) {
14180
+ throw new PlanProtocolUnsupportedError(
14181
+ "legacy v1 plan without a protocol descriptor is not accepted by this worker"
14182
+ );
14183
+ }
14184
+ return PLAN_PROTOCOL_V1;
14023
14185
  }
14024
- return value;
14025
- }
14026
- function readBoolean(value, field) {
14027
- if (typeof value !== "boolean") {
14028
- metadataError(field, "must be boolean");
14186
+ const descriptor = planJson.protocol;
14187
+ if (!isRecord(descriptor)) {
14188
+ throw new PlanProtocolUnsupportedError("plan.json protocol descriptor must be an object");
14029
14189
  }
14030
- return value;
14190
+ for (const field of ["schemaVersion", "artifactLayout", "hashSchema"]) {
14191
+ if (!Object.prototype.hasOwnProperty.call(descriptor, field)) {
14192
+ throw new PlanProtocolUnsupportedError(`plan.json protocol descriptor is missing ${field}`);
14193
+ }
14194
+ }
14195
+ const protocol = protocolMatches(descriptor, PLAN_PROTOCOL_V1) ? PLAN_PROTOCOL_V1 : protocolMatches(descriptor, PLAN_PROTOCOL_V2) ? PLAN_PROTOCOL_V2 : null;
14196
+ if (protocol === null || !capabilitiesAccept(capabilities, protocol)) {
14197
+ throw new PlanProtocolUnsupportedError("unsupported plan.json protocol descriptor");
14198
+ }
14199
+ return protocol;
14031
14200
  }
14032
- function readPositiveInteger(value, field) {
14033
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
14034
- metadataError(field, "must be a positive integer");
14201
+ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk) {
14202
+ const protocol = readPlanProtocol(planJson, capabilities);
14203
+ if (protocol !== PLAN_PROTOCOL_V1) {
14204
+ throw new PlanProtocolUnsupportedError(
14205
+ "content-addressed v2 plan must be materialized before v1 layout access"
14206
+ );
14035
14207
  }
14036
- return value;
14208
+ return PLAN_PROTOCOL_V1;
14037
14209
  }
14038
- function readNonNegativeInteger(value, field) {
14039
- if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
14040
- metadataError(field, "must be a non-negative integer");
14210
+
14211
+ // src/services/render/stages/planHash.ts
14212
+ import { createHash as createHash6 } from "node:crypto";
14213
+ import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync4, readSync as readSync3 } from "node:fs";
14214
+ var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
14215
+ var FIELD_DELIMITER = Buffer.from([0]);
14216
+ function computePlanHash(input) {
14217
+ const hash = createHash6("sha256");
14218
+ hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
14219
+ hash.update(input.compositionHtml);
14220
+ hash.update(FIELD_DELIMITER);
14221
+ const sortedAssets = [...input.assets].sort(
14222
+ (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
14223
+ );
14224
+ for (const asset of sortedAssets) {
14225
+ hash.update(asset.path, "utf8");
14226
+ hash.update(FIELD_DELIMITER);
14227
+ hash.update(asset.sha256, "utf8");
14228
+ hash.update(FIELD_DELIMITER);
14041
14229
  }
14042
- return value;
14230
+ hash.update(input.fontSnapshotSha, "utf8");
14231
+ hash.update(FIELD_DELIMITER);
14232
+ hash.update(input.encoderConfigCanonicalJson, "utf8");
14233
+ hash.update(FIELD_DELIMITER);
14234
+ hash.update(input.producerVersion, "utf8");
14235
+ hash.update(FIELD_DELIMITER);
14236
+ hash.update(input.ffmpegVersion, "utf8");
14237
+ hash.update(FIELD_DELIMITER);
14238
+ const d = input.dimensions;
14239
+ hash.update(`${d.fpsNum}/${d.fpsDen}x${d.width}x${d.height}x${d.format}`, "utf8");
14240
+ return hash.digest("hex");
14043
14241
  }
14044
- function readDeferredFrameRange(value, field) {
14045
- const range = readRecord(value, field);
14046
- const sourcePath = readNonEmptyString(range.sourcePath, `${field}.sourcePath`);
14047
- if (sourcePath.startsWith("/") || sourcePath.split(/[\\/]/).some((part) => part === "..")) {
14048
- metadataError(`${field}.sourcePath`, "must stay inside the plan directory");
14242
+ function canonicalJsonStringify(value) {
14243
+ if (value === null) return "null";
14244
+ if (typeof value === "boolean") return value ? "true" : "false";
14245
+ if (typeof value === "number") {
14246
+ if (!Number.isFinite(value)) {
14247
+ throw new TypeError(`canonicalJsonStringify: non-finite number ${value}`);
14248
+ }
14249
+ return JSON.stringify(value);
14049
14250
  }
14050
- const durationSeconds = readFiniteNumber(range.durationSeconds, `${field}.durationSeconds`);
14051
- if (durationSeconds <= 0) metadataError(`${field}.durationSeconds`, "must be positive");
14052
- if (range.format !== "jpg" && range.format !== "png") {
14053
- metadataError(`${field}.format`, 'must be "jpg" or "png"');
14251
+ if (typeof value === "string") return JSON.stringify(value);
14252
+ if (Array.isArray(value)) {
14253
+ return `[${value.map(canonicalJsonStringify).join(",")}]`;
14054
14254
  }
14055
- return {
14056
- sourcePath,
14057
- startTime: readFiniteNumber(range.startTime, `${field}.startTime`),
14058
- durationSeconds,
14059
- format: range.format
14060
- };
14255
+ if (typeof value === "object") {
14256
+ const obj = value;
14257
+ const keys = Object.keys(obj).sort();
14258
+ const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJsonStringify(obj[k])}`);
14259
+ return `{${parts.join(",")}}`;
14260
+ }
14261
+ throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
14061
14262
  }
14062
- function readRateSpec(value, field) {
14063
- if (value === void 0) return void 0;
14064
- if (typeof value === "number") return readFiniteNumber(value, field);
14065
- const lane = readRecord(value, field);
14066
- if (!Array.isArray(lane.points)) {
14067
- metadataError(`${field}.points`, "must be an array");
14263
+ function sha256Hex(bytes) {
14264
+ const h = createHash6("sha256");
14265
+ h.update(bytes);
14266
+ return h.digest("hex");
14267
+ }
14268
+ function sha256File(path) {
14269
+ const hash = createHash6("sha256");
14270
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
14271
+ const fd = openSync4(path, "r");
14272
+ try {
14273
+ let bytesRead = 0;
14274
+ do {
14275
+ bytesRead = readSync3(fd, buffer, 0, buffer.byteLength, null);
14276
+ if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
14277
+ } while (bytesRead > 0);
14278
+ } finally {
14279
+ closeSync5(fd);
14068
14280
  }
14069
- return lane;
14281
+ return hash.digest("hex");
14070
14282
  }
14071
- function readVideoMetadata(value, field) {
14072
- const record = readRecord(value, field);
14073
- let colorSpace = null;
14074
- if (record.colorSpace !== null) {
14075
- const color = readRecord(record.colorSpace, `${field}.colorSpace`);
14076
- colorSpace = {
14077
- colorTransfer: readString(color.colorTransfer, `${field}.colorSpace.colorTransfer`),
14078
- colorPrimaries: readString(color.colorPrimaries, `${field}.colorSpace.colorPrimaries`),
14079
- colorSpace: readString(color.colorSpace, `${field}.colorSpace.colorSpace`)
14080
- };
14283
+ async function sha256FileAsync(path) {
14284
+ const hash = createHash6("sha256");
14285
+ for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
14286
+ hash.update(chunk);
14081
14287
  }
14082
- return {
14083
- durationSeconds: readFiniteNumber(record.durationSeconds, `${field}.durationSeconds`),
14084
- videoStreamDurationSeconds: readFiniteNumber(
14085
- record.videoStreamDurationSeconds,
14086
- `${field}.videoStreamDurationSeconds`
14087
- ),
14088
- // Plans written before stream-start metadata existed implicitly used the
14089
- // overwhelmingly common start-at-zero domain. Preserve that compatibility
14090
- // while carrying non-zero edit-list/transport timestamps in new plans.
14091
- videoStreamStartSeconds: record.videoStreamStartSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamStartSeconds, `${field}.videoStreamStartSeconds`),
14092
- videoStreamLeadSeconds: record.videoStreamLeadSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamLeadSeconds, `${field}.videoStreamLeadSeconds`),
14093
- width: readPositiveInteger(record.width, `${field}.width`),
14094
- height: readPositiveInteger(record.height, `${field}.height`),
14095
- fps: readFiniteNumber(record.fps, `${field}.fps`),
14096
- videoCodec: readNonEmptyString(record.videoCodec, `${field}.videoCodec`),
14097
- hasAudio: readBoolean(record.hasAudio, `${field}.hasAudio`),
14098
- isVFR: readBoolean(record.isVFR, `${field}.isVFR`),
14099
- hasAlpha: readBoolean(record.hasAlpha, `${field}.hasAlpha`),
14100
- colorSpace
14101
- };
14288
+ return hash.digest("hex");
14102
14289
  }
14103
- function parsePlanVideosJson(value) {
14104
- const record = readRecord(value, "meta/videos.json");
14105
- if (!Array.isArray(record.videos) || !Array.isArray(record.extracted)) {
14106
- metadataError("meta/videos.json", "must contain videos and extracted arrays");
14290
+
14291
+ // src/services/render/stages/freezePlan.ts
14292
+ var COMPILED_INDEX_RELATIVE_PATH = "compiled/index.html";
14293
+ var HASH_EXCLUDED_PLAN_FILES = /* @__PURE__ */ new Set([
14294
+ "plan.json",
14295
+ "meta/encoder.json",
14296
+ COMPILED_INDEX_RELATIVE_PATH
14297
+ ]);
14298
+ function planFileSha256(absolutePath, digests) {
14299
+ let sha256 = digests?.get(absolutePath);
14300
+ if (sha256 === void 0) {
14301
+ sha256 = sha256File(absolutePath);
14302
+ digests?.set(absolutePath, sha256);
14107
14303
  }
14108
- const videos = record.videos.map((value2, index) => {
14109
- const field = `meta/videos.json.videos[${index}]`;
14110
- const video = readRecord(value2, field);
14111
- return {
14112
- id: readNonEmptyString(video.id, `${field}.id`),
14113
- src: readNonEmptyString(video.src, `${field}.src`),
14114
- start: readFiniteNumber(video.start, `${field}.start`),
14115
- end: readFiniteNumber(video.end, `${field}.end`),
14116
- mediaStart: readFiniteNumber(video.mediaStart, `${field}.mediaStart`),
14117
- playbackRate: readRateSpec(video.playbackRate, `${field}.playbackRate`),
14118
- loop: readBoolean(video.loop, `${field}.loop`),
14119
- hasAudio: readBoolean(video.hasAudio, `${field}.hasAudio`)
14120
- };
14121
- });
14122
- const extracted = record.extracted.map((value2, index) => {
14123
- const field = `meta/videos.json.extracted[${index}]`;
14124
- const entry = readRecord(value2, field);
14125
- return {
14126
- videoId: readNonEmptyString(entry.videoId, `${field}.videoId`),
14127
- srcPath: readNonEmptyString(entry.srcPath, `${field}.srcPath`),
14128
- framePattern: readNonEmptyString(entry.framePattern, `${field}.framePattern`),
14129
- fps: readFiniteNumber(entry.fps, `${field}.fps`),
14130
- totalFrames: readNonNegativeInteger(entry.totalFrames, `${field}.totalFrames`),
14131
- metadata: readVideoMetadata(entry.metadata, `${field}.metadata`),
14132
- ...entry.deferredRange === void 0 ? {} : { deferredRange: readDeferredFrameRange(entry.deferredRange, `${field}.deferredRange`) }
14133
- };
14134
- });
14135
- const videoIds = /* @__PURE__ */ new Set();
14136
- for (const video of videos) {
14137
- if (videoIds.has(video.id)) {
14138
- metadataError("meta/videos.json.videos", `contains duplicate id ${JSON.stringify(video.id)}`);
14139
- }
14140
- videoIds.add(video.id);
14304
+ return sha256;
14305
+ }
14306
+ async function prehashPlanFiles(dir, digests) {
14307
+ if (!existsSync19(dir)) return;
14308
+ for (const { absolutePath } of listPlanFiles(dir)) {
14309
+ if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
14141
14310
  }
14142
- const extractedIds = /* @__PURE__ */ new Set();
14143
- for (const entry of extracted) {
14144
- if (extractedIds.has(entry.videoId)) {
14145
- metadataError(
14146
- "meta/videos.json.extracted",
14147
- `contains duplicate videoId ${JSON.stringify(entry.videoId)}`
14148
- );
14149
- }
14150
- extractedIds.add(entry.videoId);
14151
- if (!videoIds.has(entry.videoId)) {
14152
- metadataError(
14153
- "meta/videos.json.extracted",
14154
- `references undeclared video ${JSON.stringify(entry.videoId)}`
14155
- );
14311
+ }
14312
+ function stripUndefined(value) {
14313
+ if (Array.isArray(value)) return value.map(stripUndefined);
14314
+ if (value !== null && typeof value === "object") {
14315
+ const obj = value;
14316
+ const out = {};
14317
+ for (const key of Object.keys(obj)) {
14318
+ const v = obj[key];
14319
+ if (v === void 0) continue;
14320
+ out[key] = stripUndefined(v);
14156
14321
  }
14322
+ return out;
14157
14323
  }
14158
- for (const video of videos) {
14159
- if (!extractedIds.has(video.id)) {
14160
- metadataError(
14161
- "meta/videos.json.extracted",
14162
- `is missing declared video ${JSON.stringify(video.id)}`
14163
- );
14324
+ return value;
14325
+ }
14326
+ function listPlanFiles(planDir) {
14327
+ const results = [];
14328
+ const rootResolved = resolve8(planDir);
14329
+ function walk(dir) {
14330
+ const entries = readdirSync7(dir, { withFileTypes: true });
14331
+ for (const entry of entries) {
14332
+ const full = join30(dir, entry.name);
14333
+ if (entry.isDirectory()) {
14334
+ walk(full);
14335
+ } else if (entry.isFile()) {
14336
+ results.push({
14337
+ planRelativePath: relative3(rootResolved, full).split(/[\\/]+/).join("/"),
14338
+ absolutePath: full
14339
+ });
14340
+ }
14164
14341
  }
14165
14342
  }
14166
- return { videos, extracted };
14343
+ walk(rootResolved);
14344
+ results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
14345
+ return results;
14167
14346
  }
14168
- function buildPlanVideosJson(input) {
14169
- const videos = input.videos.map((video, index) => {
14170
- if (Number.isFinite(video.end)) return { ...video };
14171
- if (!Number.isFinite(input.compositionEnd) || input.compositionEnd <= 0 || input.compositionEnd <= video.start) {
14172
- metadataError(
14173
- `meta/videos.json.videos[${index}].end`,
14174
- "cannot be resolved without a finite composition end after its start"
14175
- );
14347
+ function collectPlanAssetShas(planDir, digests) {
14348
+ const files = listPlanFiles(planDir);
14349
+ let compositionHtml = null;
14350
+ const assets = [];
14351
+ for (const file of files) {
14352
+ if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
14353
+ compositionHtml = readFileSync10(file.absolutePath);
14354
+ continue;
14176
14355
  }
14177
- return { ...video, end: input.compositionEnd };
14178
- });
14179
- return parsePlanVideosJson({ videos, extracted: input.extracted });
14180
- }
14181
- var execFile = promisify2(execFileCallback);
14182
- var cachedFfmpegVersions = /* @__PURE__ */ new Map();
14183
- async function readFfmpegVersion() {
14184
- const binary = getFfmpegBinary();
14185
- const cached = cachedFfmpegVersions.get(binary);
14186
- if (cached !== void 0) return cached;
14187
- let stdout;
14188
- try {
14189
- ({ stdout } = await execFile(binary, ["-version"], {
14190
- maxBuffer: 1024 * 1024,
14191
- // See runFfmpeg.ts: keeps a console window off the user's desktop on Windows.
14192
- windowsHide: true
14193
- }));
14194
- } catch (error) {
14195
- if (error.code !== "ENOENT") throw error;
14196
- throw Object.assign(
14197
- new Error(`ffmpeg not found at "${binary}": install FFmpeg or set HYPERFRAMES_FFMPEG_PATH.`, {
14198
- cause: error
14199
- }),
14200
- { code: "ENOENT" }
14356
+ if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
14357
+ assets.push({
14358
+ path: file.planRelativePath,
14359
+ sha256: planFileSha256(file.absolutePath, digests)
14360
+ });
14361
+ }
14362
+ if (compositionHtml === null) {
14363
+ throw new Error(
14364
+ `[freezePlan] compiled HTML missing at ${COMPILED_INDEX_RELATIVE_PATH} (upstream compile stage did not materialize the expected file).`
14201
14365
  );
14202
14366
  }
14203
- const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
14204
- if (!firstLine) {
14205
- throw new Error("ffmpeg -version returned empty output");
14367
+ return { compositionHtml, assets };
14368
+ }
14369
+ function recomputePlanHashFromPlanDir(planDir, digests) {
14370
+ const planJsonPath = join30(planDir, "plan.json");
14371
+ const encoderJsonPath = join30(planDir, "meta", "encoder.json");
14372
+ if (!existsSync19(planJsonPath)) {
14373
+ throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
14206
14374
  }
14207
- cachedFfmpegVersions.set(binary, firstLine);
14208
- return firstLine;
14375
+ if (!existsSync19(encoderJsonPath)) {
14376
+ throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
14377
+ }
14378
+ const planJson = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
14379
+ const encoderConfigCanonicalJson = readFileSync10(encoderJsonPath, "utf-8");
14380
+ const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
14381
+ return computePlanHash({
14382
+ compositionHtml,
14383
+ assets,
14384
+ fontSnapshotSha: planJson.fontSnapshotSha,
14385
+ encoderConfigCanonicalJson,
14386
+ producerVersion: planJson.producerVersion,
14387
+ ffmpegVersion: planJson.ffmpegVersion,
14388
+ dimensions: planJson.dimensions
14389
+ });
14209
14390
  }
14210
- function buildSyntheticRenderJob(input) {
14211
- const renderConfig = {
14212
- fps: input.fps,
14213
- quality: input.quality,
14214
- format: input.format,
14215
- crf: input.crf,
14216
- videoBitrate: input.bitrate,
14217
- videoFrameFormat: input.videoFrameFormat,
14218
- outputResolution: input.outputResolution,
14219
- outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
14220
- // Distributed mode hard-pins to software GPU. The plan-time validator
14221
- // refuses to fan out otherwise.
14222
- useGpu: false,
14223
- debug: false,
14224
- entryFile: input.entryFile,
14225
- logger: input.logger ?? defaultLogger,
14226
- hdrMode: input.hdrMode,
14227
- strictness: input.strictness,
14228
- producerConfig: input.producerConfig,
14229
- variables: input.variables
14391
+ async function freezePlan(input) {
14392
+ const {
14393
+ planDir,
14394
+ composition,
14395
+ encoder,
14396
+ chunks,
14397
+ dimensions,
14398
+ producerVersion,
14399
+ fontSnapshotSha,
14400
+ durationSeconds,
14401
+ totalFrames,
14402
+ hasAudio,
14403
+ digests
14404
+ } = input;
14405
+ if (!existsSync19(planDir)) {
14406
+ throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
14407
+ }
14408
+ const metaDir = join30(planDir, "meta");
14409
+ if (!existsSync19(metaDir)) mkdirSync10(metaDir, { recursive: true });
14410
+ writeFileSync6(
14411
+ join30(metaDir, "composition.json"),
14412
+ `${JSON.stringify(composition, null, 2)}
14413
+ `,
14414
+ "utf-8"
14415
+ );
14416
+ const encoderForCanonical = stripUndefined(encoder);
14417
+ const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
14418
+ writeFileSync6(join30(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
14419
+ writeFileSync6(join30(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
14420
+ `, "utf-8");
14421
+ const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
14422
+ const planHash = computePlanHash({
14423
+ compositionHtml,
14424
+ assets,
14425
+ fontSnapshotSha,
14426
+ encoderConfigCanonicalJson,
14427
+ producerVersion,
14428
+ ffmpegVersion: encoder.ffmpegVersion,
14429
+ dimensions
14430
+ });
14431
+ const planJson = {
14432
+ protocol: PLAN_PROTOCOL_V1,
14433
+ planHash,
14434
+ producerVersion,
14435
+ ffmpegVersion: encoder.ffmpegVersion,
14436
+ fontSnapshotSha,
14437
+ dimensions,
14438
+ chunkCount: chunks.length,
14439
+ totalFrames,
14440
+ duration: durationSeconds,
14441
+ hasAudio
14230
14442
  };
14231
- return createRenderJob(renderConfig);
14443
+ const planJsonPath = join30(planDir, "plan.json");
14444
+ writeFileSync6(planJsonPath, `${JSON.stringify(planJson, null, 2)}
14445
+ `, "utf-8");
14446
+ return { planJsonPath, planHash };
14232
14447
  }
14233
- var cachedProducerVersion = null;
14234
- function readProducerVersion() {
14235
- if (cachedProducerVersion !== null) return cachedProducerVersion;
14236
- const startDir = dirname11(fileURLToPath5(import.meta.url));
14237
- let current = startDir;
14238
- for (let i = 0; i < 10; i++) {
14239
- const candidate = join28(current, "package.json");
14240
- if (existsSync18(candidate)) {
14241
- try {
14242
- const pkg = JSON.parse(readFileSync10(candidate, "utf-8"));
14243
- if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
14244
- cachedProducerVersion = pkg.version;
14245
- return pkg.version;
14246
- }
14247
- } catch {
14448
+
14449
+ // src/services/render/runtimeEnvSnapshot.ts
14450
+ var RUNTIME_ENV_PREFIXES = [
14451
+ "PRODUCER_RUNTIME_",
14452
+ "PRODUCER_RENDER_"
14453
+ ];
14454
+ function snapshotRuntimeEnv(env = process.env) {
14455
+ const snapshot = {};
14456
+ for (const key of Object.keys(env)) {
14457
+ const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
14458
+ if (!matches) continue;
14459
+ const value = env[key];
14460
+ if (typeof value !== "string") continue;
14461
+ snapshot[key] = value;
14462
+ }
14463
+ return snapshot;
14464
+ }
14465
+ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
14466
+ const previous = {};
14467
+ for (const [key, value] of Object.entries(snapshot)) {
14468
+ if (typeof value !== "string") continue;
14469
+ const matches = RUNTIME_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
14470
+ if (!matches) continue;
14471
+ previous[key] = env[key];
14472
+ env[key] = value;
14473
+ }
14474
+ return {
14475
+ restore: () => {
14476
+ for (const [key, value] of Object.entries(previous)) {
14477
+ if (value === void 0) delete env[key];
14478
+ else env[key] = value;
14248
14479
  }
14249
14480
  }
14250
- const parent = dirname11(current);
14251
- if (parent === current) break;
14252
- current = parent;
14253
- }
14254
- cachedProducerVersion = "0.0.0-unknown";
14255
- return cachedProducerVersion;
14481
+ };
14256
14482
  }
14257
14483
 
14258
14484
  // src/services/distributed/planSize.ts
14259
14485
  import { createHash as createHash7 } from "node:crypto";
14260
- import { lstatSync as lstatSync2, readdirSync as readdirSync7 } from "node:fs";
14261
- import { extname as extname4, join as join29, relative as relative4 } from "node:path";
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";
14262
14488
  var MAX_TOP_COMPONENTS = 10;
14263
14489
  var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
14264
14490
  ".aac",
@@ -14318,7 +14544,7 @@ function emptyBreakdown() {
14318
14544
  function addCategoryBytes(result, category, filePath, sizeBytes) {
14319
14545
  if (category === "compiled") {
14320
14546
  result.compiledBytes += sizeBytes;
14321
- if (SOURCE_MEDIA_EXTENSIONS.has(extname4(filePath).toLowerCase())) {
14547
+ if (SOURCE_MEDIA_EXTENSIONS.has(extname5(filePath).toLowerCase())) {
14322
14548
  result.sourceMediaBytes += sizeBytes;
14323
14549
  }
14324
14550
  return;
@@ -14365,12 +14591,12 @@ function recordFile(result, components, rootDir, rootKind, filePath) {
14365
14591
  function walkRegularFiles(dir, visit) {
14366
14592
  let entries;
14367
14593
  try {
14368
- entries = readdirSync7(dir, { withFileTypes: true });
14594
+ entries = readdirSync8(dir, { withFileTypes: true });
14369
14595
  } catch {
14370
14596
  return;
14371
14597
  }
14372
14598
  for (const entry of entries) {
14373
- const full = join29(dir, entry.name);
14599
+ const full = join31(dir, entry.name);
14374
14600
  if (entry.isDirectory()) {
14375
14601
  walkRegularFiles(full, visit);
14376
14602
  } else if (entry.isFile()) {
@@ -14431,8 +14657,17 @@ var MAX_PARALLEL_CHUNKS_CEILING = 256;
14431
14657
  var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
14432
14658
  var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
14433
14659
  var WORKER_TIMEOUT_MARGIN = 0.75;
14434
- function estimateChunkSeconds(frames, megapixels) {
14435
- return ESTIMATED_CHUNK_OVERHEAD_SECONDS + frames * ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
14660
+ function estimateFrameSeconds(megapixels, measuredSecondsPerFrame = 0) {
14661
+ return Math.max(ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels, measuredSecondsPerFrame);
14662
+ }
14663
+ function estimateChunkSeconds(frames, megapixels, measuredSecondsPerFrame) {
14664
+ return ESTIMATED_CHUNK_OVERHEAD_SECONDS + frames * estimateFrameSeconds(megapixels, measuredSecondsPerFrame);
14665
+ }
14666
+ function planNeedsFrameCostMeasurement(html) {
14667
+ return new RegExp(`\\b${HF_COLOR_GRADING_ATTR2}=`, "i").test(html);
14668
+ }
14669
+ function chunkSizingUsesFrameCost(config) {
14670
+ return config.maxParallelChunks === void 0 && config.chunkSize === void 0 && config.targetChunkFrames === void 0 && (config.workerTimeoutSeconds !== void 0 || config.workerConcurrency !== void 0);
14436
14671
  }
14437
14672
  var CHUNK_EXCEEDS_WORKER_TIMEOUT = "CHUNK_EXCEEDS_WORKER_TIMEOUT";
14438
14673
  var ChunkExceedsWorkerTimeoutError = class extends Error {
@@ -14445,18 +14680,15 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
14445
14680
  this.name = "ChunkExceedsWorkerTimeoutError";
14446
14681
  }
14447
14682
  };
14448
- function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger) {
14683
+ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
14449
14684
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
14450
14685
  const { workerTimeoutSeconds, workerConcurrency } = config;
14451
14686
  const wave = Math.min(
14452
14687
  workerConcurrency ?? DEFAULT_MAX_PARALLEL_CHUNKS,
14453
14688
  MAX_PARALLEL_CHUNKS_CEILING
14454
14689
  );
14455
- const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
14456
- if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
14457
- return wave;
14458
- }
14459
- const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
14690
+ if (!chunkSizingUsesFrameCost(config)) return wave;
14691
+ const secondsPerFrame = estimateFrameSeconds(megapixels, measuredSecondsPerFrame);
14460
14692
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
14461
14693
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
14462
14694
  if (workerTimeoutSeconds === void 0) return wide;
@@ -14467,7 +14699,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
14467
14699
  if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
14468
14700
  const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
14469
14701
  throw new ChunkExceedsWorkerTimeoutError(
14470
- Math.ceil(estimateChunkSeconds(framesAtCeiling, megapixels)),
14702
+ Math.ceil(estimateChunkSeconds(framesAtCeiling, megapixels, measuredSecondsPerFrame)),
14471
14703
  workerTimeoutSeconds
14472
14704
  );
14473
14705
  }
@@ -14574,13 +14806,13 @@ function assertPositiveInteger2(name, value) {
14574
14806
  }
14575
14807
  var FREEZE_OWNED_PLAN_FILES = [
14576
14808
  "plan.json",
14577
- join30("meta", "composition.json"),
14578
- join30("meta", "encoder.json"),
14579
- join30("meta", "chunks.json")
14809
+ join32("meta", "composition.json"),
14810
+ join32("meta", "encoder.json"),
14811
+ join32("meta", "chunks.json")
14580
14812
  ];
14581
14813
  function removeFreezeOwnedPlanFiles(planDir) {
14582
14814
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
14583
- rmSync10(join30(planDir, relativePath), { force: true });
14815
+ rmSync10(join32(planDir, relativePath), { force: true });
14584
14816
  }
14585
14817
  }
14586
14818
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -14711,19 +14943,19 @@ function createVideoSourceShipper(input) {
14711
14943
  const known = shipped.get(srcPath);
14712
14944
  if (known) return known;
14713
14945
  const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
14714
- const current = stagedRel === null ? srcPath : join30(input.planDir, "video-frames", stagedRel);
14946
+ const current = stagedRel === null ? srcPath : join32(input.planDir, "video-frames", stagedRel);
14715
14947
  let result = null;
14716
14948
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
14717
14949
  const rel = pathInside(root, srcPath);
14718
- if (rel !== null && existsSync19(join30(input.planDir, "compiled", rel))) {
14950
+ if (rel !== null && existsSync20(join32(input.planDir, "compiled", rel))) {
14719
14951
  result = ["compiled", ...rel.split(sep2)].join("/");
14720
14952
  break;
14721
14953
  }
14722
14954
  }
14723
14955
  if (result === null) {
14724
- result = `video-sources/${shipped.size}${extname5(current)}`;
14725
- const destination = join30(input.planDir, ...result.split("/"));
14726
- mkdirSync11(join30(input.planDir, "video-sources"), { recursive: true });
14956
+ result = `video-sources/${shipped.size}${extname6(current)}`;
14957
+ const destination = join32(input.planDir, ...result.split("/"));
14958
+ mkdirSync11(join32(input.planDir, "video-sources"), { recursive: true });
14727
14959
  try {
14728
14960
  linkSync(current, destination);
14729
14961
  } catch {
@@ -14741,7 +14973,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14741
14973
  useGpu: false,
14742
14974
  browserGpuMode: "software"
14743
14975
  });
14744
- if (!existsSync19(planDir)) mkdirSync11(planDir, { recursive: true });
14976
+ if (!existsSync20(planDir)) mkdirSync11(planDir, { recursive: true });
14745
14977
  const log = config.logger ?? defaultLogger;
14746
14978
  const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
14747
14979
  const abortSignal = config.abortSignal;
@@ -14770,14 +15002,14 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14770
15002
  variables: config.variables
14771
15003
  });
14772
15004
  const entryFile = config.entryFile ?? "index.html";
14773
- const htmlPath = join30(projectDir, entryFile);
14774
- if (!existsSync19(htmlPath)) {
15005
+ const htmlPath = join32(projectDir, entryFile);
15006
+ if (!existsSync20(htmlPath)) {
14775
15007
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
14776
15008
  }
14777
- const workDir = join30(planDir, ".plan-work");
15009
+ const workDir = join32(planDir, ".plan-work");
14778
15010
  rmSync10(workDir, { recursive: true, force: true });
14779
15011
  mkdirSync11(workDir, { recursive: true });
14780
- const compiledDir = join30(workDir, "compiled");
15012
+ const compiledDir = join32(workDir, "compiled");
14781
15013
  mkdirSync11(compiledDir, { recursive: true });
14782
15014
  let stageStarted = Date.now();
14783
15015
  cpSync2(projectDir, compiledDir, {
@@ -14791,7 +15023,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14791
15023
  }
14792
15024
  });
14793
15025
  logPlanStage(log, "copy", stageStarted);
14794
- const finalCompiledDir = join30(planDir, "compiled");
15026
+ const finalCompiledDir = join32(planDir, "compiled");
14795
15027
  const needsAlpha = outputNeedsAlpha(config.format);
14796
15028
  stageStarted = Date.now();
14797
15029
  const compileResult = await runCompileStage({
@@ -14854,27 +15086,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14854
15086
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
14855
15087
  if (probeResult.probeSession) {
14856
15088
  try {
14857
- const { closeCaptureSession: closeCaptureSession11 } = await import("@hyperframes/engine");
14858
- await closeCaptureSession11(probeResult.probeSession);
15089
+ const { closeCaptureSession: closeCaptureSession12 } = await import("@hyperframes/engine");
15090
+ await closeCaptureSession12(probeResult.probeSession);
14859
15091
  } catch (err) {
14860
15092
  log.warn("[plan] probe session close failed", {
14861
15093
  error: err instanceof Error ? err.message : String(err)
14862
15094
  });
14863
15095
  }
14864
15096
  }
14865
- const maxParallel = resolveMaxParallelChunks(
14866
- config,
14867
- totalFrames,
14868
- compileResult.outputWidth * compileResult.outputHeight / 1e6,
14869
- log
14870
- );
14871
- const { chunkCount, effectiveChunkSize } = resolveChunkPlan(
14872
- totalFrames,
14873
- config.chunkSize,
14874
- maxParallel,
14875
- config.targetChunkFrames
14876
- );
14877
- const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
15097
+ const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
15098
+ let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
15099
+ const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
14878
15100
  assertPlanSizeWithinLimit({
14879
15101
  rootDir: compiledDir,
14880
15102
  rootKind: "compiled",
@@ -14899,17 +15121,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14899
15121
  if (extractResult.extractionResult) {
14900
15122
  assertVideoExtractionSucceeded(extractResult.extractionResult);
14901
15123
  }
14902
- const stagedVideoFrames = join30(compiledDir, "__hyperframes_video_frames");
14903
- const videoFramesDst = join30(planDir, "video-frames");
14904
- if (existsSync19(videoFramesDst)) rmSync10(videoFramesDst, { recursive: true, force: true });
14905
- const videoSourcesDir = join30(planDir, "video-sources");
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");
14906
15128
  rmSync10(videoSourcesDir, { recursive: true, force: true });
14907
- if (existsSync19(stagedVideoFrames)) {
15129
+ if (existsSync20(stagedVideoFrames)) {
14908
15130
  renameSync5(stagedVideoFrames, videoFramesDst);
14909
15131
  } else {
14910
15132
  mkdirSync11(videoFramesDst, { recursive: true });
14911
15133
  }
14912
- if (existsSync19(finalCompiledDir)) rmSync10(finalCompiledDir, { recursive: true, force: true });
15134
+ if (existsSync20(finalCompiledDir)) rmSync10(finalCompiledDir, { recursive: true, force: true });
14913
15135
  renameSync5(compiledDir, finalCompiledDir);
14914
15136
  const shipVideoSource = createVideoSourceShipper({
14915
15137
  planDir,
@@ -14935,9 +15157,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14935
15157
  ...ext.deferredRange ? { deferredRange: { ...ext.deferredRange, sourcePath: shipVideoSource(ext.srcPath) } } : {}
14936
15158
  }))
14937
15159
  });
14938
- mkdirSync11(join30(planDir, "meta"), { recursive: true });
15160
+ mkdirSync11(join32(planDir, "meta"), { recursive: true });
14939
15161
  writeFileSync7(
14940
- join30(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
15162
+ join32(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
14941
15163
  JSON.stringify(planVideosJson, null, 2),
14942
15164
  "utf-8"
14943
15165
  );
@@ -14945,7 +15167,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14945
15167
  const chunksCarryAudio2 = carriesChunkAudio(config, options);
14946
15168
  const audioSeconds = chunksCarryAudio2 ? totalFrames / config.fps : probeResult.duration;
14947
15169
  stageStarted = Date.now();
14948
- const [audioOutcome, overlapOutcome] = await Promise.allSettled([
15170
+ const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
14949
15171
  runAudioStage({
14950
15172
  projectDir,
14951
15173
  workDir,
@@ -14968,19 +15190,53 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
14968
15190
  await options.whileAudioMixes();
14969
15191
  logPlanStage(log, "prepublish", prepublishStarted);
14970
15192
  }
14971
- })()
15193
+ })(),
15194
+ measureFrameCost ? measurePlanSecondsPerFrame({
15195
+ planDir,
15196
+ workDir,
15197
+ html: compiled.html,
15198
+ planVideos: planVideosJson,
15199
+ totalFrames,
15200
+ fps: config.fps,
15201
+ width,
15202
+ height,
15203
+ format: config.format,
15204
+ quality: config.quality ?? "standard",
15205
+ deviceScaleFactor,
15206
+ variables: config.variables,
15207
+ cfg: { ...cfg, forceScreenshot, forceScreenshotExplicitlyOptedOut: !forceScreenshot },
15208
+ log
15209
+ }).finally(() => logPlanStage(log, "frame-cost", stageStarted)) : void 0
14972
15210
  ]);
14973
15211
  if (audioOutcome.status === "rejected") throw audioOutcome.reason;
14974
15212
  if (overlapOutcome.status === "rejected") throw overlapOutcome.reason;
15213
+ if (frameCostOutcome.status === "rejected") throw frameCostOutcome.reason;
15214
+ if (frameCostOutcome.value !== void 0) {
15215
+ log.info("[plan] measured frame cost", { secondsPerFrame: frameCostOutcome.value });
15216
+ maxParallel = resolveMaxParallelChunks(
15217
+ config,
15218
+ totalFrames,
15219
+ megapixels,
15220
+ log,
15221
+ frameCostOutcome.value
15222
+ );
15223
+ }
15224
+ const { chunkCount, effectiveChunkSize } = resolveChunkPlan(
15225
+ totalFrames,
15226
+ config.chunkSize,
15227
+ maxParallel,
15228
+ config.targetChunkFrames
15229
+ );
15230
+ const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
14975
15231
  const audioResult = audioOutcome.value;
14976
15232
  if (audioResult.audioError) {
14977
15233
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
14978
15234
  }
14979
- const planAudioPath = join30(planDir, PLAN_AUDIO_RELATIVE_PATH);
14980
- if (audioResult.hasAudio && existsSync19(audioResult.audioOutputPath)) {
15235
+ const planAudioPath = join32(planDir, PLAN_AUDIO_RELATIVE_PATH);
15236
+ if (audioResult.hasAudio && existsSync20(audioResult.audioOutputPath)) {
14981
15237
  renameSync5(audioResult.audioOutputPath, planAudioPath);
14982
15238
  }
14983
- if (chunksCarryAudio2 && existsSync19(planAudioPath)) {
15239
+ if (chunksCarryAudio2 && existsSync20(planAudioPath)) {
14984
15240
  stageStarted = Date.now();
14985
15241
  const peak = await constrainAacTruePeak({
14986
15242
  audioPath: planAudioPath,
@@ -15565,9 +15821,9 @@ function renderRequestFromDistributedConfig(input) {
15565
15821
 
15566
15822
  // src/services/frameCapture.ts
15567
15823
  import {
15568
- createCaptureSession as createCaptureSession8,
15569
- initializeSession as initializeSession8,
15570
- closeCaptureSession as closeCaptureSession9,
15824
+ createCaptureSession as createCaptureSession9,
15825
+ initializeSession as initializeSession9,
15826
+ closeCaptureSession as closeCaptureSession10,
15571
15827
  captureFrame as captureFrame2,
15572
15828
  captureFrameToBuffer as captureFrameToBuffer4,
15573
15829
  getCompositionDuration as getCompositionDuration2,
@@ -15587,7 +15843,7 @@ import {
15587
15843
 
15588
15844
  // src/server.ts
15589
15845
  import {
15590
- existsSync as existsSync22,
15846
+ existsSync as existsSync23,
15591
15847
  mkdirSync as mkdirSync12,
15592
15848
  statSync as statSync8,
15593
15849
  mkdtempSync as mkdtempSync4,
@@ -15595,7 +15851,7 @@ import {
15595
15851
  rmSync as rmSync11,
15596
15852
  createReadStream as createReadStream4
15597
15853
  } from "node:fs";
15598
- import { resolve as resolve10, dirname as dirname13, join as join33 } from "node:path";
15854
+ import { resolve as resolve10, dirname as dirname13, join as join35 } from "node:path";
15599
15855
  import { tmpdir as tmpdir3 } from "node:os";
15600
15856
  import { parseArgs } from "node:util";
15601
15857
  import crypto from "node:crypto";
@@ -15605,7 +15861,7 @@ import { serve as serve2 } from "@hono/node-server";
15605
15861
 
15606
15862
  // src/services/hyperframeLint.ts
15607
15863
  import {
15608
- existsSync as existsSync20,
15864
+ existsSync as existsSync21,
15609
15865
  readFileSync as readFileSync11,
15610
15866
  statSync as statSync7,
15611
15867
  openSync as openSync5,
@@ -15613,7 +15869,7 @@ import {
15613
15869
  closeSync as closeSync6,
15614
15870
  constants as constants3
15615
15871
  } from "node:fs";
15616
- import { resolve as resolve9, join as join31, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
15872
+ import { resolve as resolve9, join as join33, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
15617
15873
  import { lintHyperframeHtml } from "@hyperframes/lint";
15618
15874
  function isStringRecord(value) {
15619
15875
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -15659,7 +15915,7 @@ function readEntryHtml(filePath) {
15659
15915
  }
15660
15916
  function readProjectEntryFile(projectDir, preferredEntryFile) {
15661
15917
  const absProjectDir = resolve9(projectDir);
15662
- if (!existsSync20(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
15918
+ if (!existsSync21(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
15663
15919
  return { error: `Project directory not found: ${absProjectDir}` };
15664
15920
  }
15665
15921
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -15681,7 +15937,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
15681
15937
  }
15682
15938
  }
15683
15939
  return {
15684
- error: `No HTML entry file found in project directory: ${join31(absProjectDir, preferredEntryFile || "index.html")}`
15940
+ error: `No HTML entry file found in project directory: ${join33(absProjectDir, preferredEntryFile || "index.html")}`
15685
15941
  };
15686
15942
  }
15687
15943
  function prepareHyperframeLintBody(body) {
@@ -15727,8 +15983,8 @@ async function runHyperframeLint(prepared) {
15727
15983
  // src/services/healthWorker.ts
15728
15984
  import { Worker as Worker2 } from "node:worker_threads";
15729
15985
  import { fileURLToPath as fileURLToPath6 } from "node:url";
15730
- import { dirname as dirname12, join as join32 } from "node:path";
15731
- import { existsSync as existsSync21 } from "node:fs";
15986
+ import { dirname as dirname12, join as join34 } from "node:path";
15987
+ import { existsSync as existsSync22 } from "node:fs";
15732
15988
  var DEFAULT_HEALTH_PORT = 9848;
15733
15989
  async function startHealthWorker(options = {}) {
15734
15990
  const log = options.logger ?? defaultLogger2();
@@ -15803,9 +16059,9 @@ function defaultLogger2() {
15803
16059
  }
15804
16060
  function resolveWorkerEntry2() {
15805
16061
  const here = dirname12(fileURLToPath6(import.meta.url));
15806
- const candidates = [join32(here, "healthWorkerThread.js"), join32(here, "healthWorkerThread.ts")];
16062
+ const candidates = [join34(here, "healthWorkerThread.js"), join34(here, "healthWorkerThread.ts")];
15807
16063
  for (const candidate of candidates) {
15808
- if (existsSync21(candidate)) return candidate;
16064
+ if (existsSync22(candidate)) return candidate;
15809
16065
  }
15810
16066
  return void 0;
15811
16067
  }
@@ -15950,7 +16206,7 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
15950
16206
  const { input, cleanupProjectDir } = prepared;
15951
16207
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
15952
16208
  const outputDir = dirname13(absoluteOutputPath);
15953
- if (!existsSync22(outputDir)) mkdirSync12(outputDir, { recursive: true });
16209
+ if (!existsSync23(outputDir)) mkdirSync12(outputDir, { recursive: true });
15954
16210
  return { input, cleanupProjectDir, absoluteOutputPath };
15955
16211
  }
15956
16212
  function validateRenderOverrides(body) {
@@ -15990,11 +16246,11 @@ function prepareProjectDirectory(projectDir, options) {
15990
16246
  const candidate = nonEmptyString(projectDir);
15991
16247
  if (!candidate) return null;
15992
16248
  const absProjectDir = resolve10(candidate);
15993
- if (!existsSync22(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
16249
+ if (!existsSync23(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
15994
16250
  return { error: `Project directory not found: ${absProjectDir}` };
15995
16251
  }
15996
16252
  const entry = options.entryFile || "index.html";
15997
- if (!existsSync22(resolve10(absProjectDir, entry))) {
16253
+ if (!existsSync23(resolve10(absProjectDir, entry))) {
15998
16254
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
15999
16255
  }
16000
16256
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -16019,8 +16275,8 @@ async function resolveInlineRenderHtml(body) {
16019
16275
  }
16020
16276
  function materializeInlineProject(html, options) {
16021
16277
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
16022
- const tempProjectDir = mkdtempSync4(join33(tempRoot, "producer-project-"));
16023
- writeFileSync8(join33(tempProjectDir, "index.html"), html, "utf-8");
16278
+ const tempProjectDir = mkdtempSync4(join35(tempRoot, "producer-project-"));
16279
+ writeFileSync8(join35(tempProjectDir, "index.html"), html, "utf-8");
16024
16280
  return {
16025
16281
  prepared: {
16026
16282
  input: { projectDir: tempProjectDir, ...options },
@@ -16086,7 +16342,7 @@ function cleanupTempDir(dir, log) {
16086
16342
  }
16087
16343
  }
16088
16344
  function outputFileSize(path) {
16089
- return existsSync22(path) ? statSync8(path).size : 0;
16345
+ return existsSync23(path) ? statSync8(path).size : 0;
16090
16346
  }
16091
16347
  function createBlockingProgressReporter(log, requestId) {
16092
16348
  let lastLoggedPct = -10;
@@ -16382,7 +16638,7 @@ function createRenderHandlers(options = {}) {
16382
16638
  if (!artifact) {
16383
16639
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
16384
16640
  }
16385
- if (!existsSync22(artifact.path)) {
16641
+ if (!existsSync23(artifact.path)) {
16386
16642
  store.delete(token);
16387
16643
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
16388
16644
  }
@@ -16479,26 +16735,26 @@ import { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes
16479
16735
  // src/services/distributed/planV2.ts
16480
16736
  import {
16481
16737
  copyFileSync as copyFileSync7,
16482
- existsSync as existsSync24,
16738
+ existsSync as existsSync25,
16483
16739
  lstatSync as lstatSync3,
16484
16740
  mkdirSync as mkdirSync14,
16485
16741
  mkdtempSync as mkdtempSync6,
16486
16742
  readFileSync as readFileSync12,
16487
- readdirSync as readdirSync8,
16743
+ readdirSync as readdirSync9,
16488
16744
  renameSync as renameSync7,
16489
16745
  rmSync as rmSync13,
16490
16746
  statSync as statSync10,
16491
16747
  writeFileSync as writeFileSync10
16492
16748
  } from "node:fs";
16493
16749
  import { tmpdir as tmpdir4 } from "node:os";
16494
- import { dirname as dirname15, isAbsolute as isAbsolute5, join as join36, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
16750
+ import { dirname as dirname15, isAbsolute as isAbsolute5, join as join38, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
16495
16751
  import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
16496
- import { createFrameLookupTable as createFrameLookupTable2 } from "@hyperframes/engine";
16752
+ import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
16497
16753
 
16498
16754
  // src/services/distributed/planV2Publisher.ts
16499
16755
  import {
16500
16756
  copyFileSync as copyFileSync6,
16501
- existsSync as existsSync23,
16757
+ existsSync as existsSync24,
16502
16758
  linkSync as linkSync2,
16503
16759
  mkdirSync as mkdirSync13,
16504
16760
  mkdtempSync as mkdtempSync5,
@@ -16507,7 +16763,7 @@ import {
16507
16763
  statSync as statSync9,
16508
16764
  writeFileSync as writeFileSync9
16509
16765
  } from "node:fs";
16510
- import { dirname as dirname14, join as join35 } from "node:path";
16766
+ import { dirname as dirname14, join as join37 } from "node:path";
16511
16767
 
16512
16768
  // src/services/distributed/planV2Errors.ts
16513
16769
  var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
@@ -16522,7 +16778,7 @@ var PlanV2IntegrityError = class extends Error {
16522
16778
  };
16523
16779
 
16524
16780
  // src/services/distributed/planV2Layout.ts
16525
- import { join as join34 } from "node:path";
16781
+ import { join as join36 } from "node:path";
16526
16782
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
16527
16783
  function assertPlanV2Sha256(value, field) {
16528
16784
  if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
@@ -16532,7 +16788,7 @@ function assertPlanV2Sha256(value, field) {
16532
16788
  }
16533
16789
  function planV2BlobPath(root, sha256) {
16534
16790
  const digest = assertPlanV2Sha256(sha256, "artifact sha256");
16535
- return join34(root, "artifacts", "sha256", digest.slice(0, 2), digest);
16791
+ return join36(root, "artifacts", "sha256", digest.slice(0, 2), digest);
16536
16792
  }
16537
16793
 
16538
16794
  // src/services/distributed/planV2Publisher.ts
@@ -16567,13 +16823,13 @@ var LocalPlanV2ArtifactPublisher = class {
16567
16823
  #linkFile;
16568
16824
  #committed = false;
16569
16825
  constructor(destinationDir, options = {}) {
16570
- if (existsSync23(destinationDir)) {
16826
+ if (existsSync24(destinationDir)) {
16571
16827
  throw new PlanV2IntegrityError(`output directory already exists: ${destinationDir}`);
16572
16828
  }
16573
16829
  this.destinationDir = destinationDir;
16574
16830
  this.#linkFile = options.linkFile ?? linkSync2;
16575
16831
  mkdirSync13(dirname14(destinationDir), { recursive: true });
16576
- this.temporaryDir = mkdtempSync5(join35(dirname14(destinationDir), ".plan-v2-publish-"));
16832
+ this.temporaryDir = mkdtempSync5(join37(dirname14(destinationDir), ".plan-v2-publish-"));
16577
16833
  }
16578
16834
  async putBlob(blob) {
16579
16835
  const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
@@ -16584,10 +16840,10 @@ var LocalPlanV2ArtifactPublisher = class {
16584
16840
  );
16585
16841
  }
16586
16842
  const destinationPath = planV2BlobPath(this.temporaryDir, digest);
16587
- if (existsSync23(destinationPath)) return;
16843
+ if (existsSync24(destinationPath)) return;
16588
16844
  mkdirSync13(dirname14(destinationPath), { recursive: true });
16589
- const stagingDir = mkdtempSync5(join35(dirname14(destinationPath), ".plan-v2-blob-"));
16590
- const temporaryPath = join35(stagingDir, "blob");
16845
+ const stagingDir = mkdtempSync5(join37(dirname14(destinationPath), ".plan-v2-blob-"));
16846
+ const temporaryPath = join37(stagingDir, "blob");
16591
16847
  try {
16592
16848
  try {
16593
16849
  this.#linkFile(blob.sourcePath, temporaryPath);
@@ -16602,13 +16858,13 @@ var LocalPlanV2ArtifactPublisher = class {
16602
16858
  }
16603
16859
  async commitManifest(manifestBytes) {
16604
16860
  for (const digest of new Set(manifestDigests(manifestBytes))) {
16605
- if (!existsSync23(planV2BlobPath(this.temporaryDir, digest))) {
16861
+ if (!existsSync24(planV2BlobPath(this.temporaryDir, digest))) {
16606
16862
  throw new PlanV2IntegrityError(
16607
16863
  `cannot commit manifest before referenced blob is durable: ${digest}`
16608
16864
  );
16609
16865
  }
16610
16866
  }
16611
- writeFileSync9(join35(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
16867
+ writeFileSync9(join37(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
16612
16868
  renameSync6(this.temporaryDir, this.destinationDir);
16613
16869
  this.#committed = true;
16614
16870
  }
@@ -16652,8 +16908,8 @@ function listFiles(root) {
16652
16908
  const files = [];
16653
16909
  const rootResolved = resolve11(root);
16654
16910
  function walk(dir) {
16655
- for (const entry of readdirSync8(dir, { withFileTypes: true })) {
16656
- const absolutePath = join36(dir, entry.name);
16911
+ for (const entry of readdirSync9(dir, { withFileTypes: true })) {
16912
+ const absolutePath = join38(dir, entry.name);
16657
16913
  if (entry.isDirectory()) {
16658
16914
  walk(absolutePath);
16659
16915
  } else if (entry.isFile()) {
@@ -16680,13 +16936,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
16680
16936
  }
16681
16937
  }
16682
16938
  function validateExtractionCacheCompleteSentinels(executionPlanDir) {
16683
- const videoRoot = join36(executionPlanDir, "video-frames");
16684
- if (!existsSync24(videoRoot)) return;
16685
- for (const videoEntry of readdirSync8(videoRoot, { withFileTypes: true })) {
16939
+ const videoRoot = join38(executionPlanDir, "video-frames");
16940
+ if (!existsSync25(videoRoot)) return;
16941
+ for (const videoEntry of readdirSync9(videoRoot, { withFileTypes: true })) {
16686
16942
  if (!videoEntry.isDirectory()) continue;
16687
- const videoDir = join36(videoRoot, videoEntry.name);
16688
- if (readdirSync8(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
16689
- const sentinelPath = join36(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
16943
+ const videoDir = join38(videoRoot, videoEntry.name);
16944
+ if (readdirSync9(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
16945
+ const sentinelPath = join38(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
16690
16946
  assertValidExtractionCacheCompleteSentinel(sentinelPath);
16691
16947
  }
16692
16948
  }
@@ -16727,15 +16983,15 @@ function listVideoFramePaths(executionPlanDir, videos) {
16727
16983
  if (video.deferredRange) {
16728
16984
  const framePaths2 = /* @__PURE__ */ new Map();
16729
16985
  for (let index = 0; index < video.totalFrames; index++) {
16730
- framePaths2.set(index, join36(outputDir, String(index)));
16986
+ framePaths2.set(index, join38(outputDir, String(index)));
16731
16987
  }
16732
16988
  return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
16733
16989
  }
16734
- const frameNames = readdirSync8(outputDir).sort();
16990
+ const frameNames = readdirSync9(outputDir).sort();
16735
16991
  const framePaths = /* @__PURE__ */ new Map();
16736
16992
  for (const frameName of frameNames) {
16737
16993
  if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
16738
- assertValidExtractionCacheCompleteSentinel(join36(outputDir, frameName));
16994
+ assertValidExtractionCacheCompleteSentinel(join38(outputDir, frameName));
16739
16995
  continue;
16740
16996
  }
16741
16997
  const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
@@ -16754,7 +17010,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
16754
17010
  `duplicate extracted frame index ${frameIndex} in ${outputDir}`
16755
17011
  );
16756
17012
  }
16757
- framePaths.set(frameIndex, join36(outputDir, frameName));
17013
+ framePaths.set(frameIndex, join38(outputDir, frameName));
16758
17014
  }
16759
17015
  return {
16760
17016
  ...video,
@@ -16775,8 +17031,8 @@ function parsePlanVideosJson2(value) {
16775
17031
  }
16776
17032
  }
16777
17033
  function materializeExtractedVideoDirectories(planDir) {
16778
- const videosPath = join36(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
16779
- if (!existsSync24(videosPath)) return;
17034
+ const videosPath = join38(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
17035
+ if (!existsSync25(videosPath)) return;
16780
17036
  const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
16781
17037
  for (const video of videos.extracted) {
16782
17038
  mkdirSync14(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
@@ -16804,13 +17060,13 @@ function parseChunkSlices(value) {
16804
17060
  });
16805
17061
  }
16806
17062
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
16807
- const videoRoot = join36(executionPlanDir, "video-frames");
16808
- const hasExtractedFrames = existsSync24(videoRoot) && listFiles(videoRoot).length > 0;
16809
- const videosPath = join36(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
16810
- if (!existsSync24(videosPath)) {
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)) {
16811
17067
  return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
16812
17068
  }
16813
- const chunksPath = join36(executionPlanDir, "meta", "chunks.json");
17069
+ const chunksPath = join38(executionPlanDir, "meta", "chunks.json");
16814
17070
  const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
16815
17071
  const chunks = readJsonFile(chunksPath, "meta/chunks.json");
16816
17072
  const parsedVideos = parsePlanVideosJson2(videos);
@@ -16818,7 +17074,7 @@ function buildVideoChunkDependencies(executionPlanDir, dimensions) {
16818
17074
  const extracted = listVideoFramePaths(executionPlanDir, parsedVideos);
16819
17075
  const fpsNum = readPositiveInteger2(dimensions.fpsNum, "dimensions.fpsNum");
16820
17076
  const fpsDen = readPositiveInteger2(dimensions.fpsDen, "dimensions.fpsDen");
16821
- const table = createFrameLookupTable2(
17077
+ const table = createFrameLookupTable4(
16822
17078
  parsedVideos.videos,
16823
17079
  extracted,
16824
17080
  resolveRenderFpsConfig({ num: fpsNum, den: fpsDen }).value
@@ -16864,10 +17120,10 @@ function computeManifestHash(manifest) {
16864
17120
  return sha256Hex(`${PLAN_V2_HASH_PREFIX}${canonicalJsonStringify(manifestPayload(manifest))}`);
16865
17121
  }
16866
17122
  function writeBlob(sourcePath, destinationPath) {
16867
- if (existsSync24(destinationPath)) return;
17123
+ if (existsSync25(destinationPath)) return;
16868
17124
  mkdirSync14(dirname15(destinationPath), { recursive: true });
16869
- const temporaryDir = mkdtempSync6(join36(dirname15(destinationPath), ".plan-v2-blob-"));
16870
- const temporaryPath = join36(temporaryDir, "blob");
17125
+ const temporaryDir = mkdtempSync6(join38(dirname15(destinationPath), ".plan-v2-blob-"));
17126
+ const temporaryPath = join38(temporaryDir, "blob");
16871
17127
  try {
16872
17128
  copyFileSync7(sourcePath, temporaryPath);
16873
17129
  renameSync7(temporaryPath, destinationPath);
@@ -16900,8 +17156,8 @@ async function putBlobs(publisher, blobs) {
16900
17156
  if (failure) throw failure.reason;
16901
17157
  }
16902
17158
  function buildPlanV2Publication(executionPlanDir, digests) {
16903
- const executionPlanPath = join36(executionPlanDir, "plan.json");
16904
- if (!existsSync24(executionPlanPath)) {
17159
+ const executionPlanPath = join38(executionPlanDir, "plan.json");
17160
+ if (!existsSync25(executionPlanPath)) {
16905
17161
  throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
16906
17162
  }
16907
17163
  const executionPlanValue = readJsonFile(executionPlanPath, "execution plan.json");
@@ -16928,7 +17184,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
16928
17184
  }
16929
17185
  validateExtractionCacheCompleteSentinels(executionPlanDir);
16930
17186
  const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
16931
- const encoder = readJsonFile(join36(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
17187
+ const encoder = readJsonFile(join38(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
16932
17188
  const chunksCarryAudio2 = isRecord3(encoder) && encoder.chunksCarryAudio === true;
16933
17189
  for (const file of listFiles(executionPlanDir)) {
16934
17190
  if (isExtractionCacheCompleteSentinelPath(file.path)) continue;
@@ -16970,18 +17226,18 @@ function buildPlanV2Publication(executionPlanDir, digests) {
16970
17226
  };
16971
17227
  }
16972
17228
  function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
16973
- if (existsSync24(planV2Dir)) {
17229
+ if (existsSync25(planV2Dir)) {
16974
17230
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
16975
17231
  }
16976
17232
  const publication = buildPlanV2Publication(executionPlanDir);
16977
17233
  mkdirSync14(dirname15(planV2Dir), { recursive: true });
16978
- const tempDir = mkdtempSync6(join36(dirname15(planV2Dir), ".plan-v2-build-"));
17234
+ const tempDir = mkdtempSync6(join38(dirname15(planV2Dir), ".plan-v2-build-"));
16979
17235
  try {
16980
17236
  for (const blob of publication.blobs) {
16981
17237
  writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
16982
17238
  }
16983
17239
  writeFileSync10(
16984
- join36(tempDir, "plan.json"),
17240
+ join38(tempDir, "plan.json"),
16985
17241
  canonicalJsonStringify(publication.manifest),
16986
17242
  "utf-8"
16987
17243
  );
@@ -17022,7 +17278,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
17022
17278
  return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
17023
17279
  }
17024
17280
  async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
17025
- const stagingRoot = mkdtempSync6(join36(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
17281
+ const stagingRoot = mkdtempSync6(join38(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
17026
17282
  const digests = /* @__PURE__ */ new Map();
17027
17283
  try {
17028
17284
  await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
@@ -17032,7 +17288,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
17032
17288
  // artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
17033
17289
  whileAudioMixes: () => putBlobs(
17034
17290
  publisher,
17035
- listFiles(join36(stagingRoot, "compiled")).map(
17291
+ listFiles(join38(stagingRoot, "compiled")).map(
17036
17292
  (file) => publishBlobFor(file.absolutePath, digests)
17037
17293
  )
17038
17294
  ),
@@ -17050,7 +17306,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
17050
17306
  }
17051
17307
  }
17052
17308
  async function planV2(projectDir, config, planV2Dir) {
17053
- if (existsSync24(planV2Dir)) {
17309
+ if (existsSync25(planV2Dir)) {
17054
17310
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
17055
17311
  }
17056
17312
  const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
@@ -17062,7 +17318,7 @@ async function planV2(projectDir, config, planV2Dir) {
17062
17318
  function resultFromManifest(planV2Dir, manifest) {
17063
17319
  return {
17064
17320
  planDir: planV2Dir,
17065
- manifestPath: join36(planV2Dir, "plan.json"),
17321
+ manifestPath: join38(planV2Dir, "plan.json"),
17066
17322
  planProtocol: PLAN_PROTOCOL_V2,
17067
17323
  planHash: manifest.planHash,
17068
17324
  sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
@@ -17198,8 +17454,8 @@ function parsePlanV2Manifest(value) {
17198
17454
  return parsed;
17199
17455
  }
17200
17456
  function readPlanV2Manifest(planV2Dir) {
17201
- const manifestPath = join36(planV2Dir, "plan.json");
17202
- if (!existsSync24(manifestPath)) {
17457
+ const manifestPath = join38(planV2Dir, "plan.json");
17458
+ if (!existsSync25(manifestPath)) {
17203
17459
  throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
17204
17460
  }
17205
17461
  const value = readJsonFile(manifestPath, "v2 manifest");
@@ -17218,7 +17474,7 @@ function listPlanV2ArtifactsForTarget(manifest, target) {
17218
17474
  }
17219
17475
  function verifyBlob(planV2Dir, artifact) {
17220
17476
  const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
17221
- if (!existsSync24(sourcePath)) {
17477
+ if (!existsSync25(sourcePath)) {
17222
17478
  throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
17223
17479
  }
17224
17480
  const stats = statSync10(sourcePath);
@@ -17231,7 +17487,7 @@ function verifyBlob(planV2Dir, artifact) {
17231
17487
  return sourcePath;
17232
17488
  }
17233
17489
  function materializePlanV2Target(planV2Dir, target, destinationDir) {
17234
- if (existsSync24(destinationDir)) {
17490
+ if (existsSync25(destinationDir)) {
17235
17491
  throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
17236
17492
  }
17237
17493
  const manifest = readPlanV2Manifest(planV2Dir);
@@ -17242,16 +17498,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
17242
17498
  sourcePath: verifyBlob(planV2Dir, artifact)
17243
17499
  }));
17244
17500
  mkdirSync14(dirname15(destinationDir), { recursive: true });
17245
- const tempDir = mkdtempSync6(join36(dirname15(destinationDir), ".plan-v2-materialize-"));
17501
+ const tempDir = mkdtempSync6(join38(dirname15(destinationDir), ".plan-v2-materialize-"));
17246
17502
  try {
17247
17503
  for (const { artifact, sourcePath } of verified) {
17248
- const destinationPath = join36(tempDir, ...artifact.path.split("/"));
17504
+ const destinationPath = join38(tempDir, ...artifact.path.split("/"));
17249
17505
  mkdirSync14(dirname15(destinationPath), { recursive: true });
17250
17506
  copyFileSync7(sourcePath, destinationPath);
17251
17507
  }
17252
17508
  if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
17253
17509
  writeFileSync10(
17254
- join36(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
17510
+ join38(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
17255
17511
  canonicalJsonStringify({ manifest, target }),
17256
17512
  "utf-8"
17257
17513
  );
@@ -17270,12 +17526,12 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
17270
17526
  // Join the artifact's OWN name, not the current constant: a plan written
17271
17527
  // before the container change carries the legacy name, and materializing it
17272
17528
  // under the new one would point at a file that was never written.
17273
- audioPath: target.role === "assembler" ? audioArtifact ? join36(destinationDir, audioArtifact.path) : null : null
17529
+ audioPath: target.role === "assembler" ? audioArtifact ? join38(destinationDir, audioArtifact.path) : null : null
17274
17530
  };
17275
17531
  }
17276
17532
  function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
17277
- const markerPath = join36(planDir, PLAN_V2_MATERIALIZATION_MARKER);
17278
- if (!existsSync24(markerPath)) return null;
17533
+ const markerPath = join38(planDir, PLAN_V2_MATERIALIZATION_MARKER);
17534
+ if (!existsSync25(markerPath)) return null;
17279
17535
  const marker = readJsonFile(markerPath, "materialization marker");
17280
17536
  if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
17281
17537
  throw new PlanV2IntegrityError("malformed materialization marker");
@@ -17289,8 +17545,8 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
17289
17545
  const manifest = parsePlanV2Manifest(marker.manifest);
17290
17546
  const required = listPlanV2ArtifactsForTarget(manifest, expectedTarget);
17291
17547
  for (const artifact of required) {
17292
- const path = join36(planDir, ...artifact.path.split("/"));
17293
- if (!existsSync24(path) || statSync10(path).size !== artifact.sizeBytes) {
17548
+ const path = join38(planDir, ...artifact.path.split("/"));
17549
+ if (!existsSync25(path) || statSync10(path).size !== artifact.sizeBytes) {
17294
17550
  throw new PlanV2IntegrityError(
17295
17551
  `materialized artifact missing or truncated: ${artifact.path}`
17296
17552
  );
@@ -17305,22 +17561,22 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
17305
17561
  // src/services/distributed/planV2Execution.ts
17306
17562
  import { mkdtempSync as mkdtempSync9, rmSync as rmSync17 } from "node:fs";
17307
17563
  import { tmpdir as tmpdir5 } from "node:os";
17308
- import { join as join40 } from "node:path";
17564
+ import { join as join42 } from "node:path";
17309
17565
 
17310
17566
  // src/services/distributed/assemble.ts
17311
17567
  import {
17312
17568
  cpSync as cpSync3,
17313
- existsSync as existsSync25,
17569
+ existsSync as existsSync26,
17314
17570
  mkdirSync as mkdirSync16,
17315
17571
  mkdtempSync as mkdtempSync8,
17316
17572
  readFileSync as readFileSync13,
17317
- readdirSync as readdirSync9,
17573
+ readdirSync as readdirSync10,
17318
17574
  renameSync as renameSync9,
17319
17575
  rmSync as rmSync15,
17320
17576
  statSync as statSync11,
17321
17577
  writeFileSync as writeFileSync11
17322
17578
  } from "node:fs";
17323
- import { dirname as dirname17, join as join38 } from "node:path";
17579
+ import { dirname as dirname17, join as join40 } from "node:path";
17324
17580
  import {
17325
17581
  appendRenderProvenanceArgs,
17326
17582
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -17342,7 +17598,7 @@ import {
17342
17598
  rmSync as rmSync14
17343
17599
  } from "node:fs";
17344
17600
  import { open } from "node:fs/promises";
17345
- import { dirname as dirname16, join as join37 } from "node:path";
17601
+ import { dirname as dirname16, join as join39 } from "node:path";
17346
17602
  import { finished, pipeline } from "node:stream/promises";
17347
17603
  function boxAt(src, off, at, limit) {
17348
17604
  let size = src.readUInt32BE(off);
@@ -17664,13 +17920,13 @@ async function writeStitchedMp4(chunkPaths, outputPath) {
17664
17920
  const header = buildStitchedHeader(layouts);
17665
17921
  mkdirSync15(dirname16(outputPath), { recursive: true });
17666
17922
  const workDir = mkdtempSync7(`${outputPath}.stitch-`);
17667
- const out = createWriteStream(join37(workDir, "out.mp4"));
17923
+ const out = createWriteStream(join39(workDir, "out.mp4"));
17668
17924
  try {
17669
17925
  out.write(header);
17670
17926
  for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
17671
17927
  out.end();
17672
17928
  await finished(out);
17673
- renameSync8(join37(workDir, "out.mp4"), outputPath);
17929
+ renameSync8(join39(workDir, "out.mp4"), outputPath);
17674
17930
  } finally {
17675
17931
  out.destroy();
17676
17932
  rmSync14(workDir, { recursive: true, force: true });
@@ -17704,7 +17960,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17704
17960
  }
17705
17961
  const format = plan2.dimensions.format;
17706
17962
  const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
17707
- if (!existsSync25(dirname17(outputPath))) {
17963
+ if (!existsSync26(dirname17(outputPath))) {
17708
17964
  mkdirSync16(dirname17(outputPath), { recursive: true });
17709
17965
  }
17710
17966
  const workDir = mkdtempSync8(`${outputPath}.assemble-work-`);
@@ -17727,8 +17983,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17727
17983
  };
17728
17984
  try {
17729
17985
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
17730
- const finalPath = join38(workDir, `output.${format}`);
17731
- const audio = audioPath !== null && existsSync25(audioPath) ? audioPath : null;
17986
+ const finalPath = join40(workDir, `output.${format}`);
17987
+ const audio = audioPath !== null && existsSync26(audioPath) ? audioPath : null;
17732
17988
  await writeJoined(ctx, videoSources, audio, finalPath);
17733
17989
  renameSync9(finalPath, outputPath);
17734
17990
  log.info("[assemble] stage timings", {
@@ -17746,7 +18002,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17746
18002
  });
17747
18003
  }
17748
18004
  }
17749
- const fileSize = existsSync25(outputPath) ? statSync11(outputPath).size : 0;
18005
+ const fileSize = existsSync26(outputPath) ? statSync11(outputPath).size : 0;
17750
18006
  return {
17751
18007
  outputPath,
17752
18008
  durationMs: Date.now() - start,
@@ -17755,15 +18011,15 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17755
18011
  };
17756
18012
  }
17757
18013
  function readAssemblePlan(planDir, chunkPaths) {
17758
- const planJsonPath = join38(planDir, "plan.json");
17759
- const chunksJsonPath = join38(planDir, "meta", "chunks.json");
17760
- if (!existsSync25(planJsonPath)) {
18014
+ const planJsonPath = join40(planDir, "plan.json");
18015
+ const chunksJsonPath = join40(planDir, "meta", "chunks.json");
18016
+ if (!existsSync26(planJsonPath)) {
17761
18017
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
17762
18018
  }
17763
18019
  const plan2 = JSON.parse(readFileSync13(planJsonPath, "utf-8"));
17764
18020
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
17765
18021
  validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
17766
- if (!existsSync25(chunksJsonPath)) {
18022
+ if (!existsSync26(chunksJsonPath)) {
17767
18023
  throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
17768
18024
  }
17769
18025
  const chunks = JSON.parse(readFileSync13(chunksJsonPath, "utf-8"));
@@ -17773,7 +18029,7 @@ function readAssemblePlan(planDir, chunkPaths) {
17773
18029
  );
17774
18030
  }
17775
18031
  for (const path of chunkPaths) {
17776
- if (!existsSync25(path)) {
18032
+ if (!existsSync26(path)) {
17777
18033
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
17778
18034
  }
17779
18035
  }
@@ -17783,7 +18039,7 @@ function videoInputArgs(ctx, videoSources) {
17783
18039
  return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
17784
18040
  }
17785
18041
  async function reencodeCfr(ctx, chunkPaths, encoder) {
17786
- const cfrOutputPath = join38(ctx.workDir, `cfr.${ctx.format}`);
18042
+ const cfrOutputPath = join40(ctx.workDir, `cfr.${ctx.format}`);
17787
18043
  const cfrArgs = [
17788
18044
  ...videoInputArgs(ctx, chunkPaths),
17789
18045
  "-c:v",
@@ -17821,7 +18077,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
17821
18077
  );
17822
18078
  return;
17823
18079
  }
17824
- const paddedAudioPath = join38(ctx.workDir, "audio-padded.m4a");
18080
+ const paddedAudioPath = join40(ctx.workDir, "audio-padded.m4a");
17825
18081
  const padTrimResult = await ctx.timed(
17826
18082
  "audioMs",
17827
18083
  () => padOrTrimAudioToVideoFrameCount({
@@ -17861,7 +18117,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
17861
18117
  }
17862
18118
  }
17863
18119
  function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
17864
- const listPath = join38(workDir, "concat-list.txt");
18120
+ const listPath = join40(workDir, "concat-list.txt");
17865
18121
  const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
17866
18122
  writeFileSync11(listPath, `${body}
17867
18123
  `, "utf-8");
@@ -17894,8 +18150,8 @@ function chunksAreStitchedMp4(planDir, cfr) {
17894
18150
  return true;
17895
18151
  }
17896
18152
  function chunksCarryAudio(planDir) {
17897
- const encoderJsonPath = join38(planDir, "meta", "encoder.json");
17898
- if (!existsSync25(encoderJsonPath)) return false;
18153
+ const encoderJsonPath = join40(planDir, "meta", "encoder.json");
18154
+ if (!existsSync26(encoderJsonPath)) return false;
17899
18155
  return JSON.parse(readFileSync13(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
17900
18156
  }
17901
18157
  function readCfrEncoder(planDir, format) {
@@ -17904,8 +18160,8 @@ function readCfrEncoder(planDir, format) {
17904
18160
  `[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.`
17905
18161
  );
17906
18162
  }
17907
- const encoderJsonPath = join38(planDir, "meta", "encoder.json");
17908
- if (!existsSync25(encoderJsonPath)) {
18163
+ const encoderJsonPath = join40(planDir, "meta", "encoder.json");
18164
+ if (!existsSync26(encoderJsonPath)) {
17909
18165
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
17910
18166
  }
17911
18167
  const encoderJson = JSON.parse(readFileSync13(encoderJsonPath, "utf-8"));
@@ -17917,7 +18173,7 @@ function readCfrEncoder(planDir, format) {
17917
18173
  return encoderJson;
17918
18174
  }
17919
18175
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
17920
- if (existsSync25(outputPath)) rmSync15(outputPath, { recursive: true, force: true });
18176
+ if (existsSync26(outputPath)) rmSync15(outputPath, { recursive: true, force: true });
17921
18177
  mkdirSync16(outputPath, { recursive: true });
17922
18178
  let globalIdx = 0;
17923
18179
  for (const chunkDir of chunkPaths) {
@@ -17926,13 +18182,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
17926
18182
  `[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
17927
18183
  );
17928
18184
  }
17929
- const frames = readdirSync9(chunkDir).filter((name) => name.endsWith(".png")).sort();
18185
+ const frames = readdirSync10(chunkDir).filter((name) => name.endsWith(".png")).sort();
17930
18186
  if (frames.length === 0) {
17931
18187
  throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
17932
18188
  }
17933
18189
  for (const frame of frames) {
17934
- const dst = join38(outputPath, formatExportFrameName(globalIdx, "png"));
17935
- cpSync3(join38(chunkDir, frame), dst);
18190
+ const dst = join40(outputPath, formatExportFrameName(globalIdx, "png"));
18191
+ cpSync3(join40(chunkDir, frame), dst);
17936
18192
  globalIdx += 1;
17937
18193
  }
17938
18194
  }
@@ -17941,14 +18197,14 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
17941
18197
  `[assemble] png-sequence frame count mismatch: merged ${globalIdx} frames vs plan.totalFrames=${totalFrames}. Using on-disk count.`
17942
18198
  );
17943
18199
  }
17944
- if (audioPath !== null && existsSync25(audioPath)) {
17945
- const sidecar = join38(outputPath, MIXED_AUDIO_FILENAME4);
18200
+ if (audioPath !== null && existsSync26(audioPath)) {
18201
+ const sidecar = join40(outputPath, MIXED_AUDIO_FILENAME4);
17946
18202
  cpSync3(audioPath, sidecar);
17947
18203
  }
17948
18204
  let fileSize = 0;
17949
- for (const name of readdirSync9(outputPath)) {
18205
+ for (const name of readdirSync10(outputPath)) {
17950
18206
  try {
17951
- fileSize += statSync11(join38(outputPath, name)).size;
18207
+ fileSize += statSync11(join40(outputPath, name)).size;
17952
18208
  } catch {
17953
18209
  }
17954
18210
  }
@@ -17962,22 +18218,19 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
17962
18218
 
17963
18219
  // src/services/distributed/renderChunk.ts
17964
18220
  import { randomBytes } from "node:crypto";
17965
- import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync14, readdirSync as readdirSync10, rmSync as rmSync16, writeFileSync as writeFileSync12 } from "node:fs";
17966
- import { extname as extname6, join as join39 } from "node:path";
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";
17967
18223
  import {
17968
18224
  assertSwiftShader,
17969
- BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE3,
18225
+ BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
17970
18226
  calculateOptimalWorkers as calculateOptimalWorkers2,
17971
18227
  classifyCaptureFailure as classifyCaptureFailure4,
17972
- compositionRequiresWebGpu as compositionRequiresWebGpu3,
17973
- closeCaptureSession as closeCaptureSession10,
17974
- createCaptureSession as createCaptureSession9,
17975
- createFrameLookupTable as createFrameLookupTable3,
17976
- createVideoFrameInjector as createVideoFrameInjector2,
18228
+ closeCaptureSession as closeCaptureSession11,
18229
+ createCaptureSession as createCaptureSession10,
18230
+ createFrameLookupTable as createFrameLookupTable5,
17977
18231
  deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
17978
- extractVideoFramesRange,
17979
18232
  getEncoderPreset as getEncoderPreset6,
17980
- initializeSession as initializeSession9,
18233
+ initializeSession as initializeSession10,
17981
18234
  probeBeginFrameLiveness as probeBeginFrameLiveness2,
17982
18235
  readWebGlVendorInfoFromCanvas,
17983
18236
  resolveConfig as resolveConfig7
@@ -18081,20 +18334,7 @@ async function muxChunkWithAudio(input) {
18081
18334
 
18082
18335
  // src/services/distributed/renderChunk.ts
18083
18336
  import { readWebGlVendorInfoFromCanvas as readWebGlVendorInfoFromCanvas2 } from "@hyperframes/engine";
18084
- var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
18085
- var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
18086
- var MISSING_PLAN_ARTIFACT = "MISSING_PLAN_ARTIFACT";
18087
- var CHUNK_INDEX_OUT_OF_RANGE = "CHUNK_INDEX_OUT_OF_RANGE";
18088
- var MISSING_RUNTIME_ENV_SNAPSHOT = "MISSING_RUNTIME_ENV_SNAPSHOT";
18089
18337
  var LEGACY_DISTRIBUTED_VP9_CPU_USED = 2;
18090
- var RenderChunkValidationError = class extends Error {
18091
- code;
18092
- constructor(code, message) {
18093
- super(message);
18094
- this.name = "RenderChunkValidationError";
18095
- this.code = code;
18096
- }
18097
- };
18098
18338
  function validatePlanVideosForChunk(value) {
18099
18339
  try {
18100
18340
  return parsePlanVideosJson(value);
@@ -18106,10 +18346,10 @@ function validatePlanVideosForChunk(value) {
18106
18346
  }
18107
18347
  }
18108
18348
  var distributedCaptureSessionDependencies = {
18109
- createCaptureSession: createCaptureSession9,
18349
+ createCaptureSession: createCaptureSession10,
18110
18350
  assertSwiftShader,
18111
- initializeSession: initializeSession9,
18112
- closeCaptureSession: closeCaptureSession10,
18351
+ initializeSession: initializeSession10,
18352
+ closeCaptureSession: closeCaptureSession11,
18113
18353
  readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
18114
18354
  };
18115
18355
  async function createVerifiedDistributedCaptureSession(serverUrl, framesDir, captureOptions, cfg, dependencies = distributedCaptureSessionDependencies) {
@@ -18130,9 +18370,6 @@ async function createVerifiedDistributedCaptureSession(serverUrl, framesDir, cap
18130
18370
  throw error;
18131
18371
  }
18132
18372
  }
18133
- function createChunkVideoFrameInjectorFactory(frameLookup) {
18134
- return () => createVideoFrameInjector2(frameLookup);
18135
- }
18136
18373
  function isCaptureMode(value) {
18137
18374
  return value === "beginframe" || value === "screenshot" || value === "drawelement";
18138
18375
  }
@@ -18160,130 +18397,11 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
18160
18397
  );
18161
18398
  return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
18162
18399
  }
18163
- function frameNumberFromFileName(name) {
18164
- const match = /(\d+)(?=\.[^.]+$)/.exec(name);
18165
- if (!match) return null;
18166
- const frameNumber = Number(match[1]);
18167
- return Number.isSafeInteger(frameNumber) ? frameNumber : null;
18168
- }
18169
- function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
18170
- const result = [];
18171
- for (const v of videos) {
18172
- const outputDir = join39(planDir, "video-frames", v.videoId);
18173
- if (!existsSync26(outputDir)) {
18174
- throw new Error(
18175
- `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
18176
- );
18177
- }
18178
- const ext = (extname6(v.framePattern) || ".jpg").toLowerCase();
18179
- const frames = readdirSync10(outputDir).filter((name) => name.toLowerCase().endsWith(ext)).sort((left, right) => {
18180
- const leftNumber = frameNumberFromFileName(left);
18181
- const rightNumber = frameNumberFromFileName(right);
18182
- if (leftNumber === null || rightNumber === null) return left.localeCompare(right);
18183
- return leftNumber - rightNumber || left.localeCompare(right);
18184
- });
18185
- const framePaths = /* @__PURE__ */ new Map();
18186
- for (let i = 0; i < frames.length; i++) {
18187
- const frameName = frames[i];
18188
- if (!frameName) continue;
18189
- const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
18190
- const frameIndex = frameNumber === null ? i : frameNumber - 1;
18191
- framePaths.set(frameIndex, join39(outputDir, frameName));
18192
- }
18193
- result.push({
18194
- videoId: v.videoId,
18195
- srcPath: v.srcPath,
18196
- outputDir,
18197
- framePattern: v.framePattern,
18198
- fps: v.fps,
18199
- totalFrames: v.totalFrames,
18200
- metadata: v.metadata,
18201
- framePaths,
18202
- // The chunk worker doesn't own the planDir's video-frames/ directory
18203
- // (the controller does — adapters that fan out chunks across machines
18204
- // share the planDir as read-only). Mark ownership as false so the
18205
- // injector's eventual cleanup doesn't rm bytes another worker may
18206
- // still be reading.
18207
- ownedByLookup: false
18208
- });
18209
- }
18210
- return result;
18211
- }
18212
- function contiguousRuns(sortedIndexes) {
18213
- const runs2 = [];
18214
- for (const index of sortedIndexes) {
18215
- const last = runs2[runs2.length - 1];
18216
- if (last && last.firstFrame + last.frames === index) last.frames++;
18217
- else runs2.push({ firstFrame: index, frames: 1 });
18218
- }
18219
- return runs2;
18220
- }
18221
- async function extractDeferredVideoFramesForChunk(input) {
18222
- const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
18223
- if (deferred.length === 0) return [];
18224
- const table = createFrameLookupTable3(
18225
- input.planVideos.videos,
18226
- deferred.map((video) => ({
18227
- ...video,
18228
- outputDir: input.outputRoot,
18229
- framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
18230
- })),
18231
- resolveRenderFpsConfig(input.fps).value
18232
- );
18233
- const shown = /* @__PURE__ */ new Map();
18234
- for (let frame = input.slice.startFrame; frame < input.slice.endFrame; frame++) {
18235
- const globalTime = frame * input.fps.den / input.fps.num;
18236
- for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
18237
- const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
18238
- indexes.add(payload.frameIndex);
18239
- shown.set(videoId, indexes);
18240
- }
18241
- }
18242
- return Promise.all(
18243
- deferred.map(async (video) => {
18244
- const outputDir = join39(input.outputRoot, video.videoId);
18245
- const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
18246
- if (indexes.length === 0) {
18247
- return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
18248
- }
18249
- const range = video.deferredRange;
18250
- const sourcePath = join39(input.planDir, ...range.sourcePath.split("/"));
18251
- if (!existsSync26(sourcePath)) {
18252
- throw new RenderChunkValidationError(
18253
- MISSING_PLAN_ARTIFACT,
18254
- `[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
18255
- );
18256
- }
18257
- const extracted = await extractVideoFramesRange(
18258
- sourcePath,
18259
- video.videoId,
18260
- range.startTime,
18261
- range.durationSeconds,
18262
- {
18263
- fps: input.fps,
18264
- outputDir: input.outputRoot,
18265
- format: range.format,
18266
- frameRanges: contiguousRuns(indexes)
18267
- },
18268
- void 0,
18269
- input.cfg
18270
- );
18271
- const missing = indexes.find((index) => !extracted.framePaths.has(index));
18272
- if (missing !== void 0) {
18273
- throw new RenderChunkValidationError(
18274
- INVALID_VIDEO_METADATA,
18275
- `[renderChunk] video ${JSON.stringify(video.videoId)} has no frame ${missing}; the plan expected ${video.totalFrames} frames from ${range.sourcePath}.`
18276
- );
18277
- }
18278
- return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
18279
- })
18280
- );
18281
- }
18282
18400
  function hashChunkOutput(outputPath, kind) {
18283
18401
  if (kind === "file") return sha256Hex(readFileSync14(outputPath));
18284
- const entries = readdirSync10(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
18402
+ const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
18285
18403
  const lines = entries.map(
18286
- (name) => `${name}\0${sha256Hex(readFileSync14(join39(outputPath, name)))}`
18404
+ (name) => `${name}\0${sha256Hex(readFileSync14(join41(outputPath, name)))}`
18287
18405
  );
18288
18406
  return sha256Hex(lines.join("\0"));
18289
18407
  }
@@ -18300,10 +18418,10 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
18300
18418
  async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18301
18419
  const start = Date.now();
18302
18420
  const log = defaultLogger;
18303
- const planJsonPath = join39(planDir, "plan.json");
18304
- const encoderJsonPath = join39(planDir, "meta", "encoder.json");
18305
- const chunksJsonPath = join39(planDir, "meta", "chunks.json");
18306
- if (!existsSync26(planJsonPath)) {
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)) {
18307
18425
  throw new RenderChunkValidationError(
18308
18426
  MISSING_PLAN_ARTIFACT,
18309
18427
  `[renderChunk] planDir is missing required artifact: ${planJsonPath}`
@@ -18317,7 +18435,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18317
18435
  chunkIndex
18318
18436
  });
18319
18437
  for (const required of [encoderJsonPath, chunksJsonPath]) {
18320
- if (!existsSync26(required)) {
18438
+ if (!existsSync27(required)) {
18321
18439
  throw new RenderChunkValidationError(
18322
18440
  MISSING_PLAN_ARTIFACT,
18323
18441
  `[renderChunk] planDir is missing required artifact: ${required}`
@@ -18326,9 +18444,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18326
18444
  }
18327
18445
  const encoder = JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
18328
18446
  const chunks = JSON.parse(readFileSync14(chunksJsonPath, "utf-8"));
18329
- const videosJsonPath = join39(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18447
+ const videosJsonPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18330
18448
  let planVideos = null;
18331
- if (existsSync26(videosJsonPath)) {
18449
+ if (existsSync27(videosJsonPath)) {
18332
18450
  try {
18333
18451
  planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync14(videosJsonPath, "utf-8")));
18334
18452
  } catch (err) {
@@ -18356,8 +18474,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18356
18474
  `[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
18357
18475
  );
18358
18476
  }
18359
- const compiledDir = join39(planDir, "compiled");
18360
- if (!existsSync26(compiledDir)) {
18477
+ const compiledDir = join41(planDir, "compiled");
18478
+ if (!existsSync27(compiledDir)) {
18361
18479
  throw new RenderChunkValidationError(
18362
18480
  MISSING_PLAN_ARTIFACT,
18363
18481
  `[renderChunk] planDir missing compiled/ directory: ${compiledDir}`
@@ -18372,7 +18490,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18372
18490
  }
18373
18491
  if (encoder.browserGpuMode !== "software") {
18374
18492
  throw new RenderChunkValidationError(
18375
- BROWSER_GPU_NOT_SOFTWARE3,
18493
+ BROWSER_GPU_NOT_SOFTWARE4,
18376
18494
  `[renderChunk] planDir requires browserGpuMode=software, got ${JSON.stringify(encoder.browserGpuMode)}.`
18377
18495
  );
18378
18496
  }
@@ -18414,10 +18532,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18414
18532
  };
18415
18533
  const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
18416
18534
  mkdirSync17(workDir, { recursive: true });
18417
- const framesDir = join39(workDir, "captured-frames");
18535
+ const framesDir = join41(workDir, "captured-frames");
18418
18536
  mkdirSync17(framesDir, { recursive: true });
18419
18537
  const videoFramesDir = `${workDir}.video-frames`;
18420
- const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable3(
18538
+ const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
18421
18539
  planVideos.videos,
18422
18540
  [
18423
18541
  ...rebuildExtractedFramesFromPlanDir(
@@ -18428,7 +18546,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18428
18546
  ...await extractDeferredVideoFramesForChunk({
18429
18547
  planDir,
18430
18548
  planVideos,
18431
- slice,
18549
+ frames: Array.from(
18550
+ { length: slice.endFrame - slice.startFrame },
18551
+ (_, i) => slice.startFrame + i
18552
+ ),
18432
18553
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18433
18554
  outputRoot: videoFramesDir,
18434
18555
  cfg
@@ -18437,9 +18558,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18437
18558
  resolveRenderFpsConfig(job.config.fps).value
18438
18559
  ) : null;
18439
18560
  const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
18440
- const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(
18441
- planVideos?.videos.length ?? 0
18442
- );
18443
18561
  const fileServer = await createFileServer({
18444
18562
  projectDir: compiledDir,
18445
18563
  compiledDir,
@@ -18449,28 +18567,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18449
18567
  // chunk runtime seek quantization stays on the same fps grid as capture.
18450
18568
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen }
18451
18569
  });
18452
- const captureOptions = {
18453
- width: plan2.dimensions.width,
18454
- height: plan2.dimensions.height,
18455
- fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
18456
- format: plan2.dimensions.format === "mp4" ? "jpeg" : "png",
18457
- quality: plan2.dimensions.format === "mp4" ? resolveCaptureJpegQuality(job.config.quality) : void 0,
18458
- deviceScaleFactor: encoder.deviceScaleFactor,
18459
- // Re-inject the controller's snapshotted variables so the chunk's
18460
- // first capture sees the same `window.__hfVariables` the in-process
18461
- // renderer would have seen. Optional — compositions that don't
18462
- // declare `data-composition-variables` leave this undefined and the
18463
- // engine skips the `evaluateOnNewDocument` injection.
18464
- variables: encoder.variables,
18465
- subTimelineWaitMemo: {},
18466
- ...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
18467
- // lock the BeginFrame warmup loop to a fixed iteration count so
18468
- // `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
18469
- lockWarmupTicks: true,
18470
- requiresWebGpu: compositionRequiresWebGpu3(
18471
- readFileSync14(join39(compiledDir, "index.html"), "utf-8")
18472
- )
18473
- };
18570
+ const captureOptions = buildChunkCaptureOptions({
18571
+ dimensions: plan2.dimensions,
18572
+ encoder,
18573
+ videoCount: planVideos?.videos.length ?? 0,
18574
+ html: readFileSync14(join41(compiledDir, "index.html"), "utf-8")
18575
+ });
18474
18576
  const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
18475
18577
  let session = null;
18476
18578
  let outputKind;
@@ -18501,7 +18603,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18501
18603
  { chunkIndex }
18502
18604
  );
18503
18605
  if (chunkWorkerCount > 1) {
18504
- await closeCaptureSession10(session);
18606
+ await closeCaptureSession11(session);
18505
18607
  session = null;
18506
18608
  }
18507
18609
  } else if (beginFrameStalled) {
@@ -18510,7 +18612,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18510
18612
  "[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
18511
18613
  { chunkIndex }
18512
18614
  );
18513
- await closeCaptureSession10(session).catch(() => {
18615
+ await closeCaptureSession11(session).catch(() => {
18514
18616
  });
18515
18617
  session = null;
18516
18618
  if (chunkWorkerCount === 1) {
@@ -18529,7 +18631,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18529
18631
  sessionBootMs += Date.now() - screenshotBootStarted;
18530
18632
  }
18531
18633
  } else if (chunkWorkerCount > 1) {
18532
- await closeCaptureSession10(session);
18634
+ await closeCaptureSession11(session);
18533
18635
  session = null;
18534
18636
  }
18535
18637
  }
@@ -18631,10 +18733,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18631
18733
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
18632
18734
  const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
18633
18735
  if (isPngSequence) {
18634
- if (!existsSync26(outputChunkPath)) mkdirSync17(outputChunkPath, { recursive: true });
18736
+ if (!existsSync27(outputChunkPath)) mkdirSync17(outputChunkPath, { recursive: true });
18635
18737
  } else {
18636
- const outDir = join39(outputChunkPath, "..");
18637
- if (!existsSync26(outDir)) mkdirSync17(outDir, { recursive: true });
18738
+ const outDir = join41(outputChunkPath, "..");
18739
+ if (!existsSync27(outDir)) mkdirSync17(outDir, { recursive: true });
18638
18740
  }
18639
18741
  const encodeStarted = Date.now();
18640
18742
  await runEncodeStage({
@@ -18692,7 +18794,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18692
18794
  } finally {
18693
18795
  if (session) {
18694
18796
  try {
18695
- await closeCaptureSession10(session);
18797
+ await closeCaptureSession11(session);
18696
18798
  } catch (err) {
18697
18799
  log.warn("[renderChunk] error closing capture session in finally", {
18698
18800
  error: err instanceof Error ? err.message : String(err)
@@ -18757,8 +18859,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
18757
18859
 
18758
18860
  // src/services/distributed/planV2Execution.ts
18759
18861
  async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
18760
- const workRoot = mkdtempSync9(join40(tmpdir5(), "hf-plan-v2-chunk-"));
18761
- const materializedPlanDir = join40(workRoot, "plan");
18862
+ const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-chunk-"));
18863
+ const materializedPlanDir = join42(workRoot, "plan");
18762
18864
  try {
18763
18865
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
18764
18866
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
@@ -18767,8 +18869,8 @@ async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
18767
18869
  }
18768
18870
  }
18769
18871
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
18770
- const workRoot = mkdtempSync9(join40(tmpdir5(), "hf-plan-v2-assembler-"));
18771
- const materializedPlanDir = join40(workRoot, "plan");
18872
+ const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-assembler-"));
18873
+ const materializedPlanDir = join42(workRoot, "plan");
18772
18874
  try {
18773
18875
  const materialized = materializePlanV2Target(
18774
18876
  planV2Dir,
@@ -18816,9 +18918,9 @@ export {
18816
18918
  assembleV2,
18817
18919
  captureFrame2 as captureFrame,
18818
18920
  captureFrameToBuffer4 as captureFrameToBuffer,
18819
- closeCaptureSession9 as closeCaptureSession,
18921
+ closeCaptureSession10 as closeCaptureSession,
18820
18922
  collectRenderMedia,
18821
- createCaptureSession8 as createCaptureSession,
18923
+ createCaptureSession9 as createCaptureSession,
18822
18924
  createConsoleLogger,
18823
18925
  createFileServer,
18824
18926
  createOwnedRenderDir,
@@ -18836,7 +18938,7 @@ export {
18836
18938
  getCompositionDuration2 as getCompositionDuration,
18837
18939
  getDistributedRenderCapabilities,
18838
18940
  getPlanV2ExecutionPlanHash,
18839
- initializeSession8 as initializeSession,
18941
+ initializeSession9 as initializeSession,
18840
18942
  injectDeterministicFontFaces2 as injectDeterministicFontFaces,
18841
18943
  isTransientBrowserError3 as isTransientBrowserError,
18842
18944
  listAbandonedRenderDirs,