@hyperframes/producer 0.8.175 → 0.8.176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/distributed.js +1109 -932
- package/dist/index.js +1052 -950
- package/dist/public-server.js +30 -11
- package/dist/services/distributed/chunkFrames.d.ts +48 -0
- package/dist/services/distributed/plan.d.ts +8 -2
- package/dist/services/distributed/planFrameCost.d.ts +24 -0
- package/dist/services/distributed/renderChunk.d.ts +4 -69
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -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
|
|
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
|
|
13588
|
-
import {
|
|
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/
|
|
13596
|
-
import {
|
|
13597
|
-
import {
|
|
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/
|
|
13600
|
-
|
|
13601
|
-
|
|
13602
|
-
|
|
13603
|
-
|
|
13604
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
13608
|
-
|
|
13609
|
-
|
|
13610
|
-
|
|
13611
|
-
}
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
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
|
-
|
|
13637
|
-
|
|
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 =
|
|
13640
|
-
constructor(
|
|
13641
|
-
super(
|
|
13642
|
-
this.name = "
|
|
13644
|
+
code = INVALID_VIDEO_METADATA;
|
|
13645
|
+
constructor(message) {
|
|
13646
|
+
super(message);
|
|
13647
|
+
this.name = "PlanVideosMetadataError";
|
|
13643
13648
|
}
|
|
13644
13649
|
};
|
|
13645
|
-
function
|
|
13646
|
-
|
|
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
|
|
13657
|
-
if (
|
|
13658
|
-
|
|
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
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
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
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
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
|
-
|
|
13678
|
-
|
|
13679
|
-
|
|
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
|
|
13675
|
+
return value;
|
|
13682
13676
|
}
|
|
13683
|
-
function
|
|
13684
|
-
|
|
13685
|
-
|
|
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
|
|
13681
|
+
return value;
|
|
13691
13682
|
}
|
|
13692
|
-
|
|
13693
|
-
|
|
13694
|
-
|
|
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
|
-
|
|
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
|
|
13725
|
-
if (value
|
|
13726
|
-
|
|
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
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
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
|
-
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
|
|
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
|
-
|
|
13744
|
-
|
|
13745
|
-
|
|
13746
|
-
|
|
13747
|
-
|
|
13748
|
-
|
|
13706
|
+
return {
|
|
13707
|
+
sourcePath,
|
|
13708
|
+
startTime: readFiniteNumber(range.startTime, `${field}.startTime`),
|
|
13709
|
+
durationSeconds,
|
|
13710
|
+
format: range.format
|
|
13711
|
+
};
|
|
13749
13712
|
}
|
|
13750
|
-
function
|
|
13751
|
-
|
|
13752
|
-
|
|
13753
|
-
const
|
|
13754
|
-
|
|
13755
|
-
|
|
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
|
|
13720
|
+
return lane;
|
|
13764
13721
|
}
|
|
13765
|
-
|
|
13766
|
-
const
|
|
13767
|
-
|
|
13768
|
-
|
|
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
|
|
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
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
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
|
-
|
|
13787
|
-
}
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
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
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
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
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
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
|
-
|
|
13826
|
-
results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
|
|
13827
|
-
return results;
|
|
13817
|
+
return { videos, extracted };
|
|
13828
13818
|
}
|
|
13829
|
-
function
|
|
13830
|
-
const
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
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
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
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
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
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
|
-
|
|
13858
|
-
|
|
13854
|
+
const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
|
|
13855
|
+
if (!firstLine) {
|
|
13856
|
+
throw new Error("ffmpeg -version returned empty output");
|
|
13859
13857
|
}
|
|
13860
|
-
|
|
13861
|
-
|
|
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
|
-
|
|
13874
|
-
const {
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
13878
|
-
|
|
13879
|
-
|
|
13880
|
-
|
|
13881
|
-
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
|
|
13890
|
-
|
|
13891
|
-
|
|
13892
|
-
|
|
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
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
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/
|
|
13932
|
-
var
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
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
|
-
|
|
13922
|
+
};
|
|
13923
|
+
function createChunkVideoFrameInjectorFactory(frameLookup) {
|
|
13924
|
+
return () => createVideoFrameInjector2(frameLookup);
|
|
13946
13925
|
}
|
|
13947
|
-
function
|
|
13948
|
-
const
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
|
|
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
|
-
|
|
13958
|
-
|
|
13959
|
-
|
|
13960
|
-
|
|
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/
|
|
13967
|
-
|
|
13968
|
-
|
|
13969
|
-
|
|
13970
|
-
|
|
13971
|
-
|
|
13972
|
-
|
|
13973
|
-
|
|
13974
|
-
|
|
13975
|
-
|
|
13976
|
-
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
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
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
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 =
|
|
13994
|
-
constructor(
|
|
13995
|
-
super(
|
|
13996
|
-
this.name = "
|
|
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
|
|
14000
|
-
|
|
14163
|
+
function isRecord(value) {
|
|
14164
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14001
14165
|
}
|
|
14002
|
-
function
|
|
14003
|
-
|
|
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
|
|
14009
|
-
|
|
14010
|
-
|
|
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
|
|
14015
|
-
if (
|
|
14016
|
-
|
|
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
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
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
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
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
|
-
|
|
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
|
|
14033
|
-
|
|
14034
|
-
|
|
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
|
|
14208
|
+
return PLAN_PROTOCOL_V1;
|
|
14037
14209
|
}
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
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
|
-
|
|
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
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
if (
|
|
14048
|
-
|
|
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
|
-
|
|
14051
|
-
if (
|
|
14052
|
-
|
|
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
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
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
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
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
|
|
14281
|
+
return hash.digest("hex");
|
|
14070
14282
|
}
|
|
14071
|
-
function
|
|
14072
|
-
const
|
|
14073
|
-
|
|
14074
|
-
|
|
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
|
-
|
|
14104
|
-
|
|
14105
|
-
|
|
14106
|
-
|
|
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
|
-
|
|
14109
|
-
|
|
14110
|
-
|
|
14111
|
-
|
|
14112
|
-
|
|
14113
|
-
|
|
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
|
-
|
|
14143
|
-
|
|
14144
|
-
|
|
14145
|
-
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
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
|
-
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
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
|
-
|
|
14343
|
+
walk(rootResolved);
|
|
14344
|
+
results.sort((a, b) => a.planRelativePath < b.planRelativePath ? -1 : 1);
|
|
14345
|
+
return results;
|
|
14167
14346
|
}
|
|
14168
|
-
function
|
|
14169
|
-
const
|
|
14170
|
-
|
|
14171
|
-
|
|
14172
|
-
|
|
14173
|
-
|
|
14174
|
-
|
|
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
|
-
|
|
14178
|
-
|
|
14179
|
-
|
|
14180
|
-
|
|
14181
|
-
|
|
14182
|
-
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
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
|
-
|
|
14204
|
-
|
|
14205
|
-
|
|
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
|
-
|
|
14208
|
-
|
|
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
|
|
14211
|
-
const
|
|
14212
|
-
|
|
14213
|
-
|
|
14214
|
-
|
|
14215
|
-
|
|
14216
|
-
|
|
14217
|
-
|
|
14218
|
-
|
|
14219
|
-
|
|
14220
|
-
|
|
14221
|
-
|
|
14222
|
-
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
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
|
-
|
|
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
|
|
14261
|
-
import { extname as
|
|
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(
|
|
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 =
|
|
14594
|
+
entries = readdirSync8(dir, { withFileTypes: true });
|
|
14369
14595
|
} catch {
|
|
14370
14596
|
return;
|
|
14371
14597
|
}
|
|
14372
14598
|
for (const entry of entries) {
|
|
14373
|
-
const full =
|
|
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
|
|
14435
|
-
return
|
|
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
|
-
|
|
14456
|
-
|
|
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
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
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(
|
|
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 :
|
|
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 &&
|
|
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}${
|
|
14725
|
-
const destination =
|
|
14726
|
-
mkdirSync11(
|
|
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 (!
|
|
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 =
|
|
14774
|
-
if (!
|
|
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 =
|
|
15009
|
+
const workDir = join32(planDir, ".plan-work");
|
|
14778
15010
|
rmSync10(workDir, { recursive: true, force: true });
|
|
14779
15011
|
mkdirSync11(workDir, { recursive: true });
|
|
14780
|
-
const compiledDir =
|
|
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 =
|
|
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:
|
|
14858
|
-
await
|
|
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
|
|
14866
|
-
|
|
14867
|
-
|
|
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 =
|
|
14903
|
-
const videoFramesDst =
|
|
14904
|
-
if (
|
|
14905
|
-
const videoSourcesDir =
|
|
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 (
|
|
15129
|
+
if (existsSync20(stagedVideoFrames)) {
|
|
14908
15130
|
renameSync5(stagedVideoFrames, videoFramesDst);
|
|
14909
15131
|
} else {
|
|
14910
15132
|
mkdirSync11(videoFramesDst, { recursive: true });
|
|
14911
15133
|
}
|
|
14912
|
-
if (
|
|
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(
|
|
15160
|
+
mkdirSync11(join32(planDir, "meta"), { recursive: true });
|
|
14939
15161
|
writeFileSync7(
|
|
14940
|
-
|
|
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 =
|
|
14980
|
-
if (audioResult.hasAudio &&
|
|
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 &&
|
|
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
|
|
15569
|
-
initializeSession as
|
|
15570
|
-
closeCaptureSession as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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: ${
|
|
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
|
|
15731
|
-
import { existsSync as
|
|
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 = [
|
|
16062
|
+
const candidates = [join34(here, "healthWorkerThread.js"), join34(here, "healthWorkerThread.ts")];
|
|
15807
16063
|
for (const candidate of candidates) {
|
|
15808
|
-
if (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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(
|
|
16023
|
-
writeFileSync8(
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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(
|
|
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 (
|
|
16843
|
+
if (existsSync24(destinationPath)) return;
|
|
16588
16844
|
mkdirSync13(dirname14(destinationPath), { recursive: true });
|
|
16589
|
-
const stagingDir = mkdtempSync5(
|
|
16590
|
-
const temporaryPath =
|
|
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 (!
|
|
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(
|
|
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
|
|
16656
|
-
const absolutePath =
|
|
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 =
|
|
16684
|
-
if (!
|
|
16685
|
-
for (const videoEntry of
|
|
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 =
|
|
16688
|
-
if (
|
|
16689
|
-
const sentinelPath =
|
|
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,
|
|
16986
|
+
framePaths2.set(index, join38(outputDir, String(index)));
|
|
16731
16987
|
}
|
|
16732
16988
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
16733
16989
|
}
|
|
16734
|
-
const frameNames =
|
|
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(
|
|
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,
|
|
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 =
|
|
16779
|
-
if (!
|
|
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 =
|
|
16808
|
-
const hasExtractedFrames =
|
|
16809
|
-
const videosPath =
|
|
16810
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 (
|
|
17123
|
+
if (existsSync25(destinationPath)) return;
|
|
16868
17124
|
mkdirSync14(dirname15(destinationPath), { recursive: true });
|
|
16869
|
-
const temporaryDir = mkdtempSync6(
|
|
16870
|
-
const temporaryPath =
|
|
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 =
|
|
16904
|
-
if (!
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 (
|
|
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:
|
|
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 =
|
|
17202
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
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(
|
|
17501
|
+
const tempDir = mkdtempSync6(join38(dirname15(destinationDir), ".plan-v2-materialize-"));
|
|
17246
17502
|
try {
|
|
17247
17503
|
for (const { artifact, sourcePath } of verified) {
|
|
17248
|
-
const destinationPath =
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
17278
|
-
if (!
|
|
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 =
|
|
17293
|
-
if (!
|
|
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
|
|
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
|
|
17569
|
+
existsSync as existsSync26,
|
|
17314
17570
|
mkdirSync as mkdirSync16,
|
|
17315
17571
|
mkdtempSync as mkdtempSync8,
|
|
17316
17572
|
readFileSync as readFileSync13,
|
|
17317
|
-
readdirSync as
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 (!
|
|
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 =
|
|
17731
|
-
const audio = 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 =
|
|
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 =
|
|
17759
|
-
const chunksJsonPath =
|
|
17760
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
17898
|
-
if (!
|
|
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 =
|
|
17908
|
-
if (!
|
|
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 (
|
|
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 =
|
|
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 =
|
|
17935
|
-
cpSync3(
|
|
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 &&
|
|
17945
|
-
const sidecar =
|
|
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
|
|
18205
|
+
for (const name of readdirSync10(outputPath)) {
|
|
17950
18206
|
try {
|
|
17951
|
-
fileSize += statSync11(
|
|
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
|
|
17966
|
-
import {
|
|
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
|
|
18225
|
+
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
17970
18226
|
calculateOptimalWorkers as calculateOptimalWorkers2,
|
|
17971
18227
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
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
|
|
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:
|
|
18349
|
+
createCaptureSession: createCaptureSession10,
|
|
18110
18350
|
assertSwiftShader,
|
|
18111
|
-
initializeSession:
|
|
18112
|
-
closeCaptureSession:
|
|
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 =
|
|
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(
|
|
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 =
|
|
18304
|
-
const encoderJsonPath =
|
|
18305
|
-
const chunksJsonPath =
|
|
18306
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
18447
|
+
const videosJsonPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18330
18448
|
let planVideos = null;
|
|
18331
|
-
if (
|
|
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 =
|
|
18360
|
-
if (!
|
|
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
|
-
|
|
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 =
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
18454
|
-
|
|
18455
|
-
|
|
18456
|
-
|
|
18457
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
18736
|
+
if (!existsSync27(outputChunkPath)) mkdirSync17(outputChunkPath, { recursive: true });
|
|
18635
18737
|
} else {
|
|
18636
|
-
const outDir =
|
|
18637
|
-
if (!
|
|
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
|
|
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(
|
|
18761
|
-
const materializedPlanDir =
|
|
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(
|
|
18771
|
-
const materializedPlanDir =
|
|
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
|
-
|
|
18921
|
+
closeCaptureSession10 as closeCaptureSession,
|
|
18820
18922
|
collectRenderMedia,
|
|
18821
|
-
|
|
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
|
-
|
|
18941
|
+
initializeSession9 as initializeSession,
|
|
18840
18942
|
injectDeterministicFontFaces2 as injectDeterministicFontFaces,
|
|
18841
18943
|
isTransientBrowserError3 as isTransientBrowserError,
|
|
18842
18944
|
listAbandonedRenderDirs,
|