@hyperframes/producer 0.8.205 → 0.8.207
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 +891 -494
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +75 -68
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1286 -770
- package/dist/public-server.js +974 -492
- package/dist/renderRequest.d.ts +3 -0
- package/dist/server.d.ts +3 -0
- package/dist/services/animatedGifPrep.d.ts +6 -0
- package/dist/services/assetMediaType.d.ts +1 -0
- package/dist/services/distributed/assemble.d.ts +1 -1
- package/dist/services/distributed/plan.d.ts +6 -0
- package/dist/services/heifImagePrep.d.ts +69 -0
- package/dist/services/heifRotation.d.ts +2 -0
- package/dist/services/htmlCompiler.d.ts +1 -4
- package/dist/services/render/deliveryEncoder.d.ts +22 -0
- package/dist/services/render/hlsConfig.d.ts +1 -1
- package/dist/services/render/segmentManifest.d.ts +3 -0
- package/dist/services/render/stages/captureHdrStage.d.ts +2 -0
- package/dist/services/render/stages/encodeStage.d.ts +2 -0
- package/dist/services/render/stages/freezePlan.d.ts +3 -0
- package/dist/services/render/stages/probeStage.d.ts +2 -3
- package/dist/services/renderOrchestrator.d.ts +6 -0
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
|
|
3
3
|
import {
|
|
4
4
|
existsSync as existsSync16,
|
|
5
|
-
mkdirSync as
|
|
6
|
-
readFileSync as
|
|
5
|
+
mkdirSync as mkdirSync10,
|
|
6
|
+
readFileSync as readFileSync9,
|
|
7
7
|
readdirSync as readdirSync5,
|
|
8
|
-
rmSync as
|
|
9
|
-
statSync as
|
|
10
|
-
writeFileSync as
|
|
11
|
-
copyFileSync as
|
|
8
|
+
rmSync as rmSync10,
|
|
9
|
+
statSync as statSync8,
|
|
10
|
+
writeFileSync as writeFileSync6,
|
|
11
|
+
copyFileSync as copyFileSync5,
|
|
12
12
|
appendFileSync
|
|
13
13
|
} from "fs";
|
|
14
14
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
15
15
|
import {
|
|
16
16
|
chainCapture,
|
|
17
|
-
fpsToNumber as
|
|
17
|
+
fpsToNumber as fpsToNumber8,
|
|
18
18
|
HF_COLOR_GRADING_ATTR,
|
|
19
19
|
HF_VFX_ATTR,
|
|
20
20
|
parseVfxChain,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
augmentProtocolTimeoutError,
|
|
57
57
|
augmentPageNavigationTimeoutError
|
|
58
58
|
} from "@hyperframes/engine";
|
|
59
|
-
import { join as
|
|
59
|
+
import { join as join27, dirname as dirname11, resolve as resolve8 } from "path";
|
|
60
60
|
import { totalmem as totalmem2 } from "node:os";
|
|
61
61
|
import { randomUUID } from "crypto";
|
|
62
62
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -80,6 +80,7 @@ import { Readable } from "node:stream";
|
|
|
80
80
|
import { join, extname, resolve as resolve2, sep } from "node:path";
|
|
81
81
|
import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
|
|
82
82
|
import { fpsToNumber } from "@hyperframes/core";
|
|
83
|
+
import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
|
|
83
84
|
|
|
84
85
|
// src/services/hyperframeRuntimeLoader.ts
|
|
85
86
|
import { createHash } from "node:crypto";
|
|
@@ -196,11 +197,11 @@ var defaultLogger = createConsoleLogger("info");
|
|
|
196
197
|
|
|
197
198
|
// src/services/fileServer.ts
|
|
198
199
|
var readFileAsync = promisify(readFile);
|
|
199
|
-
function isPathInside(
|
|
200
|
+
function isPathInside(child3, parent, options = {}) {
|
|
200
201
|
const { resolveSymlinks = false, pathModule } = options;
|
|
201
202
|
const resolveFn = pathModule?.resolve ?? resolve2;
|
|
202
203
|
const separator = pathModule?.sep ?? sep;
|
|
203
|
-
const resolvedChild = resolveFn(
|
|
204
|
+
const resolvedChild = resolveFn(child3);
|
|
204
205
|
const resolvedParent = resolveFn(parent);
|
|
205
206
|
const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
|
|
206
207
|
const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
|
|
@@ -720,6 +721,18 @@ function createFileServer(options) {
|
|
|
720
721
|
html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
|
|
721
722
|
return c.text(html, 200, { "Content-Type": contentType });
|
|
722
723
|
}
|
|
724
|
+
if (ext === ".png") {
|
|
725
|
+
const untagged = stripSdrPngColourTags(await readFileAsync(fd));
|
|
726
|
+
if (untagged) {
|
|
727
|
+
const headers = {
|
|
728
|
+
"Content-Type": contentType,
|
|
729
|
+
"Content-Length": String(untagged.length)
|
|
730
|
+
};
|
|
731
|
+
return new Response(c.req.method === "HEAD" ? null : new Uint8Array(untagged), {
|
|
732
|
+
headers
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
723
736
|
const totalSize = stat.size;
|
|
724
737
|
const rangeHeader = c.req.header("range");
|
|
725
738
|
const rangeRequest = parseRangeHeader(rangeHeader, totalSize);
|
|
@@ -747,7 +760,8 @@ function createFileServer(options) {
|
|
|
747
760
|
const stream = createReadStream(filePath, {
|
|
748
761
|
fd,
|
|
749
762
|
autoClose: true,
|
|
750
|
-
|
|
763
|
+
start: range?.start ?? 0,
|
|
764
|
+
...range ? { end: range.end } : {}
|
|
751
765
|
});
|
|
752
766
|
streamOwnsFd = true;
|
|
753
767
|
try {
|
|
@@ -761,10 +775,10 @@ function createFileServer(options) {
|
|
|
761
775
|
if (!streamOwnsFd) closeSync(fd);
|
|
762
776
|
}
|
|
763
777
|
});
|
|
764
|
-
return new Promise((
|
|
778
|
+
return new Promise((resolve13) => {
|
|
765
779
|
const connections = /* @__PURE__ */ new Set();
|
|
766
780
|
const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
|
|
767
|
-
|
|
781
|
+
resolve13({
|
|
768
782
|
url: `http://localhost:${info.port}`,
|
|
769
783
|
port: info.port,
|
|
770
784
|
addPreHeadScript: (script) => {
|
|
@@ -796,7 +810,7 @@ function outputUsesH264Pipeline(format) {
|
|
|
796
810
|
}
|
|
797
811
|
|
|
798
812
|
// src/services/render/hlsConfig.ts
|
|
799
|
-
import {
|
|
813
|
+
import { keyframeIntervalToGopSize } from "@hyperframes/engine";
|
|
800
814
|
var DEFAULT_HLS_SEGMENT_SECONDS = 4;
|
|
801
815
|
function validateHlsRenderConfig(config) {
|
|
802
816
|
if (config.format !== "hls") return;
|
|
@@ -821,7 +835,7 @@ function resolveHlsSegmentSeconds(config) {
|
|
|
821
835
|
return config.hlsSegmentSeconds ?? DEFAULT_HLS_SEGMENT_SECONDS;
|
|
822
836
|
}
|
|
823
837
|
function resolveHlsGopSize(fps, segmentSeconds) {
|
|
824
|
-
return
|
|
838
|
+
return keyframeIntervalToGopSize(fps, segmentSeconds);
|
|
825
839
|
}
|
|
826
840
|
function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
827
841
|
if (outputFormat !== "hls") return { lockGopForChunkConcat: false, gopSize: void 0 };
|
|
@@ -831,6 +845,41 @@ function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
|
831
845
|
};
|
|
832
846
|
}
|
|
833
847
|
|
|
848
|
+
// src/services/render/deliveryEncoder.ts
|
|
849
|
+
import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
|
|
850
|
+
function validateDeliveryEncoderConfig(config) {
|
|
851
|
+
const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
|
|
852
|
+
if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
|
|
853
|
+
return;
|
|
854
|
+
if (keyframeIntervalSeconds !== void 0) {
|
|
855
|
+
if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
|
|
856
|
+
throw new Error(
|
|
857
|
+
`keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
if (format === "hls") {
|
|
861
|
+
throw new Error(
|
|
862
|
+
"keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (format !== void 0 && format !== "mp4") {
|
|
867
|
+
throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
|
|
868
|
+
}
|
|
869
|
+
appendRateCapArgs([], { maxBitrate, bufferSize });
|
|
870
|
+
}
|
|
871
|
+
function chunkGopFrames(framesInChunk, intervalFrames) {
|
|
872
|
+
return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
|
|
873
|
+
}
|
|
874
|
+
function resolveDeliveryEncoderOptions(config) {
|
|
875
|
+
const { maxBitrate, bufferSize, keyframeIntervalSeconds } = config;
|
|
876
|
+
const gop = keyframeIntervalSeconds === void 0 ? resolveHlsEncoderGopLock(config.format ?? "mp4", config.fps, config.hlsSegmentSeconds) : {
|
|
877
|
+
lockGopForChunkConcat: true,
|
|
878
|
+
gopSize: keyframeIntervalToGopSize2(config.fps, keyframeIntervalSeconds)
|
|
879
|
+
};
|
|
880
|
+
return { maxBitrate, bufferSize, ...gop };
|
|
881
|
+
}
|
|
882
|
+
|
|
834
883
|
// src/services/render/shared.ts
|
|
835
884
|
import {
|
|
836
885
|
copyFileSync,
|
|
@@ -1271,8 +1320,8 @@ var OrderedRenderEventPublisher = class {
|
|
|
1271
1320
|
let timer;
|
|
1272
1321
|
const drained = await Promise.race([
|
|
1273
1322
|
this.tail.then(() => true),
|
|
1274
|
-
new Promise((
|
|
1275
|
-
timer = setTimeout(() =>
|
|
1323
|
+
new Promise((resolve13) => {
|
|
1324
|
+
timer = setTimeout(() => resolve13(false), FLUSH_TIMEOUT_MS);
|
|
1276
1325
|
})
|
|
1277
1326
|
]);
|
|
1278
1327
|
clearTimeout(timer);
|
|
@@ -1850,7 +1899,7 @@ function replanSegmentedAfterFailure(plan2, failure) {
|
|
|
1850
1899
|
const keepSegmenting = failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture";
|
|
1851
1900
|
return createCapturePlan({
|
|
1852
1901
|
...plan2,
|
|
1853
|
-
forceScreenshot: keepSegmenting || failure.kind === "capture_failure",
|
|
1902
|
+
forceScreenshot: plan2.forceScreenshot || keepSegmenting || failure.kind === "capture_failure",
|
|
1854
1903
|
useStreamingEncode: true,
|
|
1855
1904
|
useLayeredComposite: false,
|
|
1856
1905
|
useSegmentedCapture: keepSegmenting,
|
|
@@ -1920,7 +1969,7 @@ import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
|
1920
1969
|
|
|
1921
1970
|
// src/services/render/perfSummary.ts
|
|
1922
1971
|
import { arch, cpus, platform, totalmem } from "node:os";
|
|
1923
|
-
import { fpsToNumber as
|
|
1972
|
+
import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
|
|
1924
1973
|
|
|
1925
1974
|
// src/services/render/hdrPerf.ts
|
|
1926
1975
|
function createHdrPerfCollector() {
|
|
@@ -2191,7 +2240,7 @@ function buildRenderPerfSummary(input) {
|
|
|
2191
2240
|
// payloads (CLI telemetry, regression-harness reports) where a single
|
|
2192
2241
|
// number is friendlier than `{num,den}`. Callers needing the rational
|
|
2193
2242
|
// back can read `job.config.fps`.
|
|
2194
|
-
fps:
|
|
2243
|
+
fps: fpsToNumber2(input.job.config.fps),
|
|
2195
2244
|
quality: input.job.config.quality,
|
|
2196
2245
|
workers: input.workerCount,
|
|
2197
2246
|
workerSizing: input.workerSizing,
|
|
@@ -2776,7 +2825,7 @@ import {
|
|
|
2776
2825
|
resolveVideoExtractionWindow,
|
|
2777
2826
|
runFfmpeg
|
|
2778
2827
|
} from "@hyperframes/engine";
|
|
2779
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
2828
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
2780
2829
|
|
|
2781
2830
|
// src/services/hdrFrameWindows.ts
|
|
2782
2831
|
import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
|
|
@@ -3079,7 +3128,7 @@ async function extractHdrVideoFrames(args) {
|
|
|
3079
3128
|
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
|
|
3080
3129
|
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
|
|
3081
3130
|
const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
|
|
3082
|
-
const fps =
|
|
3131
|
+
const fps = fpsToNumber3(job.config.fps);
|
|
3083
3132
|
const out = /* @__PURE__ */ new Map();
|
|
3084
3133
|
mkdirSync2(framesDir, { recursive: true });
|
|
3085
3134
|
let scratchBytes = 0;
|
|
@@ -3616,7 +3665,7 @@ var SequentialCaptureStallError = class extends Error {
|
|
|
3616
3665
|
}
|
|
3617
3666
|
};
|
|
3618
3667
|
function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
3619
|
-
return new Promise((
|
|
3668
|
+
return new Promise((resolve13, reject) => {
|
|
3620
3669
|
const timer = setTimeout(
|
|
3621
3670
|
() => {
|
|
3622
3671
|
reject(
|
|
@@ -3635,7 +3684,7 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
|
3635
3684
|
promise.then(
|
|
3636
3685
|
(value) => {
|
|
3637
3686
|
clearTimeout(timer);
|
|
3638
|
-
|
|
3687
|
+
resolve13(value);
|
|
3639
3688
|
},
|
|
3640
3689
|
(error) => {
|
|
3641
3690
|
clearTimeout(timer);
|
|
@@ -3917,7 +3966,6 @@ async function runCaptureStreamingStage(input) {
|
|
|
3917
3966
|
const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
|
|
3918
3967
|
if (workerCount > 1) {
|
|
3919
3968
|
const deParallelStream = forceParallelStream === true || process.env.HF_DE_PARALLEL_STREAM === "true";
|
|
3920
|
-
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames, workerCount, workDir) : distributeFrames(totalFrames, workerCount, workDir);
|
|
3921
3969
|
const parallelStats = {
|
|
3922
3970
|
verifyChecked: 0,
|
|
3923
3971
|
blankSuspects: 0,
|
|
@@ -3930,143 +3978,158 @@ async function runCaptureStreamingStage(input) {
|
|
|
3930
3978
|
frameTime: (i) => i * job.config.fps.den / job.config.fps.num
|
|
3931
3979
|
});
|
|
3932
3980
|
let parallelGuardRan = false;
|
|
3933
|
-
let
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3981
|
+
let writtenFrames = 0;
|
|
3982
|
+
let resumeFrom = 0;
|
|
3983
|
+
let resumedAfterStall = false;
|
|
3984
|
+
let workerResults;
|
|
3985
|
+
for (; ; ) {
|
|
3986
|
+
const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
|
|
3987
|
+
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames - resumeFrom, workerCount, workDir, resumeFrom) : distributeFrames(totalFrames - resumeFrom, workerCount, workDir, resumeFrom);
|
|
3988
|
+
let parallelDrainError = null;
|
|
3989
|
+
const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
|
|
3990
|
+
try {
|
|
3991
|
+
if (deParallelStream && workerSession.captureMode === "drawelement") {
|
|
3992
|
+
parallelGuardRan = true;
|
|
3993
|
+
buffer = await parallelGuard(workerSession, frameIndex, buffer);
|
|
3994
|
+
}
|
|
3995
|
+
await parallelReorder.waitForFrame(frameIndex);
|
|
3996
|
+
ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
|
|
3997
|
+
parallelReorder.advanceTo(frameIndex + 1);
|
|
3998
|
+
writtenFrames = frameIndex + 1;
|
|
3999
|
+
} catch (err) {
|
|
4000
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
4001
|
+
if (!parallelDrainError) {
|
|
4002
|
+
parallelDrainError = e;
|
|
4003
|
+
parallelReorder.abort(e);
|
|
4004
|
+
}
|
|
4005
|
+
throw e;
|
|
3948
4006
|
}
|
|
3949
|
-
|
|
4007
|
+
};
|
|
4008
|
+
const captureLabel = input.parallelCaptureLabel ?? "worker";
|
|
4009
|
+
let watchdogArmed = false;
|
|
4010
|
+
const stallController = new AbortController();
|
|
4011
|
+
const forwardParentAbort = () => stallController.abort();
|
|
4012
|
+
if (abortSignal) {
|
|
4013
|
+
if (abortSignal.aborted) stallController.abort();
|
|
4014
|
+
else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
|
|
3950
4015
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
4016
|
+
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4017
|
+
let lastCapturedFrames = 0;
|
|
4018
|
+
let lastProgressAt = Date.now();
|
|
4019
|
+
const workerPhases = /* @__PURE__ */ new Map();
|
|
4020
|
+
const phaseDetail = (phase) => `phase=${phase.phase} frame=${phase.frameIndex ?? "n/a"} step=${phase.readStep?.() ?? "n/a"} browser=${phase.browserExecutable} version=${phase.browserVersion} CanvasDrawElement=${phase.canvasDrawElement} gpu=${phase.gpuBackend}`;
|
|
4021
|
+
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
4022
|
+
let stalled = false;
|
|
4023
|
+
const stallTimer = setInterval(
|
|
4024
|
+
() => {
|
|
4025
|
+
if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
|
|
4026
|
+
stalled = true;
|
|
4027
|
+
const stallErr = new ParallelCaptureStallError({
|
|
4028
|
+
variant: "drain",
|
|
4029
|
+
captureLabel,
|
|
4030
|
+
stallTimeoutMs,
|
|
4031
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4032
|
+
totalFrames,
|
|
4033
|
+
phaseSummary: phaseSummary()
|
|
4034
|
+
});
|
|
4035
|
+
parallelReorder.abort(stallErr);
|
|
4036
|
+
stallController.abort();
|
|
4037
|
+
},
|
|
4038
|
+
Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
|
|
4039
|
+
);
|
|
4040
|
+
try {
|
|
4041
|
+
workerResults = await executeParallelCapture(
|
|
4042
|
+
fileServer.url,
|
|
4043
|
+
workDir,
|
|
4044
|
+
tasks,
|
|
4045
|
+
buildCaptureOptions(),
|
|
4046
|
+
createRenderVideoFrameInjector,
|
|
4047
|
+
stallController.signal,
|
|
4048
|
+
(progress) => {
|
|
4049
|
+
if (progress.latestWorkerPhase) {
|
|
4050
|
+
const phase = progress.latestWorkerPhase;
|
|
4051
|
+
workerPhases.set(phase.workerId, phaseDetail(phase));
|
|
4052
|
+
if (phase.phase === "frame_capture" && !watchdogArmed) {
|
|
4053
|
+
watchdogArmed = true;
|
|
4054
|
+
lastProgressAt = Date.now();
|
|
4055
|
+
}
|
|
4056
|
+
log.info("[Render] Parallel capture worker phase", {
|
|
4057
|
+
workerId: phase.workerId,
|
|
4058
|
+
phase: phase.phase,
|
|
4059
|
+
frameIndex: phase.frameIndex,
|
|
4060
|
+
browserExecutable: phase.browserExecutable,
|
|
4061
|
+
browserVersion: phase.browserVersion,
|
|
4062
|
+
canvasDrawElement: phase.canvasDrawElement,
|
|
4063
|
+
gpuBackend: phase.gpuBackend
|
|
4064
|
+
});
|
|
4065
|
+
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4069
|
+
lastCapturedFrames = progress.capturedFrames;
|
|
3999
4070
|
lastProgressAt = Date.now();
|
|
4071
|
+
watchdogArmed = true;
|
|
4072
|
+
}
|
|
4073
|
+
const doneFrames = resumeFrom + progress.capturedFrames;
|
|
4074
|
+
job.framesRendered = doneFrames;
|
|
4075
|
+
const frameProgress = doneFrames / totalFrames;
|
|
4076
|
+
reportFrameProgress(
|
|
4077
|
+
job,
|
|
4078
|
+
`Streaming frame ${doneFrames}/${totalFrames} (${workerCount} workers)`,
|
|
4079
|
+
Math.round(25 + frameProgress * 55),
|
|
4080
|
+
onProgress,
|
|
4081
|
+
doneFrames,
|
|
4082
|
+
totalFrames
|
|
4083
|
+
);
|
|
4084
|
+
},
|
|
4085
|
+
onFrameBuffer,
|
|
4086
|
+
// Interleaved DE workers each get their own browser process: co-tenant pages starve
|
|
4087
|
+
// each other's paint events.
|
|
4088
|
+
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4089
|
+
{
|
|
4090
|
+
// A worker died: release parked peers with its original failure, not the stall the
|
|
4091
|
+
// watchdog raises later.
|
|
4092
|
+
onWorkerOperation: (phase) => workerPhases.set(phase.workerId, phaseDetail(phase)),
|
|
4093
|
+
onWorkerFailure: (failure) => {
|
|
4094
|
+
if (parallelDrainError) return;
|
|
4095
|
+
parallelDrainError = failure;
|
|
4096
|
+
parallelReorder.abort(failure);
|
|
4000
4097
|
}
|
|
4001
|
-
log.info("[Render] Parallel capture worker phase", {
|
|
4002
|
-
workerId: phase.workerId,
|
|
4003
|
-
phase: phase.phase,
|
|
4004
|
-
frameIndex: phase.frameIndex,
|
|
4005
|
-
browserExecutable: phase.browserExecutable,
|
|
4006
|
-
browserVersion: phase.browserVersion,
|
|
4007
|
-
canvasDrawElement: phase.canvasDrawElement,
|
|
4008
|
-
gpuBackend: phase.gpuBackend
|
|
4009
|
-
});
|
|
4010
|
-
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4011
|
-
return;
|
|
4012
|
-
}
|
|
4013
|
-
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4014
|
-
lastCapturedFrames = progress.capturedFrames;
|
|
4015
|
-
lastProgressAt = Date.now();
|
|
4016
|
-
watchdogArmed = true;
|
|
4017
4098
|
}
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4099
|
+
);
|
|
4100
|
+
} catch (err) {
|
|
4101
|
+
if (stalled && abortSignal?.aborted !== true && captureLabel === "BeginFrame" && !resumedAfterStall && writtenFrames > resumeFrom) {
|
|
4102
|
+
resumedAfterStall = true;
|
|
4103
|
+
log.warn(
|
|
4104
|
+
"[Render] Parallel BeginFrame capture stalled; resuming from the first unwritten frame",
|
|
4105
|
+
{
|
|
4106
|
+
resumeFrom: writtenFrames,
|
|
4107
|
+
totalFrames,
|
|
4108
|
+
workerPhases: phaseSummary()
|
|
4109
|
+
}
|
|
4027
4110
|
);
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
// Interleaved DE workers each need their own browser PROCESS:
|
|
4031
|
-
// pages co-tenant in one browser share one compositor, whose
|
|
4032
|
-
// internal frame scheduling deprioritizes non-active pages — their
|
|
4033
|
-
// canvas `paint` events starve and the drawElement paint-wait slows
|
|
4034
|
-
// ~3x (measured: 86s vs 30s on a 3,245-frame rAF comp). This is
|
|
4035
|
-
// Chromium's internal frame-production scheduling on ALL platforms,
|
|
4036
|
-
// NOT the Linux-only HeadlessExperimental.beginFrame capture mode.
|
|
4037
|
-
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4038
|
-
{
|
|
4039
|
-
// A worker died (Target closed, Page crashed, launch failure).
|
|
4040
|
-
// Its frames are gone and peers are parked in waitForFrame on the
|
|
4041
|
-
// first of them; release them with the ORIGINAL failure so the
|
|
4042
|
-
// orchestrator sees a transient browser error it knows how to
|
|
4043
|
-
// retry, not the stall the watchdog would have raised a minute
|
|
4044
|
-
// later. First failure wins, same as the drain guard above.
|
|
4045
|
-
onWorkerFailure: (failure) => {
|
|
4046
|
-
if (parallelDrainError) return;
|
|
4047
|
-
parallelDrainError = failure;
|
|
4048
|
-
reorderBuffer.abort(failure);
|
|
4049
|
-
}
|
|
4111
|
+
resumeFrom = writtenFrames;
|
|
4112
|
+
continue;
|
|
4050
4113
|
}
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4114
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4115
|
+
if (stalled && abortSignal?.aborted !== true) {
|
|
4116
|
+
throw new ParallelCaptureStallError({
|
|
4117
|
+
variant: "pool",
|
|
4118
|
+
captureLabel,
|
|
4119
|
+
stallTimeoutMs,
|
|
4120
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4121
|
+
totalFrames,
|
|
4122
|
+
phaseSummary: phaseSummary()
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
throw poolFailure(err, probeSession);
|
|
4126
|
+
} finally {
|
|
4127
|
+
clearInterval(stallTimer);
|
|
4128
|
+
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4063
4129
|
}
|
|
4064
|
-
throw poolFailure(
|
|
4065
|
-
|
|
4066
|
-
clearInterval(stallTimer);
|
|
4067
|
-
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4130
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4131
|
+
break;
|
|
4068
4132
|
}
|
|
4069
|
-
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4070
4133
|
pushWorkerDedupPerfs(workerResults, dedupPerfs);
|
|
4071
4134
|
const workerPageErrors = [];
|
|
4072
4135
|
pushWorkerPageErrors(workerResults, workerPageErrors);
|
|
@@ -4227,7 +4290,7 @@ function segmentEncoderOptions(base, segment) {
|
|
|
4227
4290
|
return {
|
|
4228
4291
|
...base,
|
|
4229
4292
|
lockGopForChunkConcat: true,
|
|
4230
|
-
gopSize: segment.endFrame - segment.startFrame
|
|
4293
|
+
gopSize: Math.min(base.gopSize ?? Infinity, segment.endFrame - segment.startFrame)
|
|
4231
4294
|
};
|
|
4232
4295
|
}
|
|
4233
4296
|
var SegmentEncoderSpawnError = class extends Error {
|
|
@@ -4321,6 +4384,12 @@ function defaultSessionFactory(input) {
|
|
|
4321
4384
|
let probeSession = input.probeSession;
|
|
4322
4385
|
return {
|
|
4323
4386
|
create: async () => {
|
|
4387
|
+
const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
|
|
4388
|
+
if (probeSession && probeCannotCaptureEveryFrame) {
|
|
4389
|
+
await closeCaptureSession2(probeSession).catch(() => {
|
|
4390
|
+
});
|
|
4391
|
+
probeSession = null;
|
|
4392
|
+
}
|
|
4324
4393
|
const session = probeSession ?? await openSegmentedSession(input);
|
|
4325
4394
|
if (probeSession) {
|
|
4326
4395
|
openSegmentedSessionReuse(input, probeSession);
|
|
@@ -4651,8 +4720,10 @@ function computeSegmentPlanHash(input) {
|
|
|
4651
4720
|
input.motionBlur,
|
|
4652
4721
|
input.pageMotionBlur ?? "",
|
|
4653
4722
|
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4654
|
-
]
|
|
4655
|
-
|
|
4723
|
+
];
|
|
4724
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
4725
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
4726
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
4656
4727
|
}
|
|
4657
4728
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
4658
4729
|
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
@@ -4799,7 +4870,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
4799
4870
|
|
|
4800
4871
|
// src/services/render/captureCost.ts
|
|
4801
4872
|
import { join as join12 } from "node:path";
|
|
4802
|
-
import { fpsToNumber as
|
|
4873
|
+
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
4803
4874
|
import {
|
|
4804
4875
|
calculateOptimalWorkers,
|
|
4805
4876
|
captureFrameToBuffer as captureFrameToBuffer3,
|
|
@@ -5003,7 +5074,7 @@ async function runCaptureCalibration(input) {
|
|
|
5003
5074
|
let probeSession = input.probeSession;
|
|
5004
5075
|
let forceScreenshot = input.forceScreenshot;
|
|
5005
5076
|
let lastBrowserConsole = [];
|
|
5006
|
-
const fps =
|
|
5077
|
+
const fps = fpsToNumber4(job.config.fps);
|
|
5007
5078
|
const sessionRef = { current: null };
|
|
5008
5079
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
5009
5080
|
log.info("Launching browser for capture calibration...");
|
|
@@ -5463,7 +5534,7 @@ function emitFallbackCaptureProfile(recorder, perfSummaries, env = process.env)
|
|
|
5463
5534
|
// src/services/render/videoFrameCoverage.ts
|
|
5464
5535
|
import { parseHTML } from "linkedom";
|
|
5465
5536
|
import {
|
|
5466
|
-
fpsToNumber as
|
|
5537
|
+
fpsToNumber as fpsToNumber5,
|
|
5467
5538
|
normalizeRateSpec,
|
|
5468
5539
|
sourceTimeAt,
|
|
5469
5540
|
toFps
|
|
@@ -5498,7 +5569,7 @@ function resolveVideoCoverageThreshold(envValue = process.env.HF_VIDEO_COVERAGE_
|
|
|
5498
5569
|
return parsed;
|
|
5499
5570
|
}
|
|
5500
5571
|
function expectedFramesForClip(start, end, fps, rounding = "ceil") {
|
|
5501
|
-
const fpsValue =
|
|
5572
|
+
const fpsValue = fpsToNumber5(toFps(fps));
|
|
5502
5573
|
if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(fpsValue)) return 0;
|
|
5503
5574
|
if (fpsValue <= 0) return 0;
|
|
5504
5575
|
const duration = Math.max(0, end - start);
|
|
@@ -5563,15 +5634,15 @@ function countAuthoredTimedClips(html) {
|
|
|
5563
5634
|
}
|
|
5564
5635
|
|
|
5565
5636
|
// src/services/render/stages/compileStage.ts
|
|
5566
|
-
import { join as
|
|
5637
|
+
import { join as join16 } from "node:path";
|
|
5567
5638
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
5568
5639
|
|
|
5569
5640
|
// src/services/htmlCompiler.ts
|
|
5570
5641
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
5571
|
-
import { createHash as
|
|
5572
|
-
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as
|
|
5573
|
-
import { join as
|
|
5574
|
-
import { parseHTML as
|
|
5642
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
5643
|
+
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
|
|
5644
|
+
import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
|
|
5645
|
+
import { parseHTML as parseHTML5 } from "linkedom";
|
|
5575
5646
|
import {
|
|
5576
5647
|
COMPOSITION_SOURCE_URL,
|
|
5577
5648
|
compileTimingAttrs,
|
|
@@ -5847,7 +5918,7 @@ function readLoopOverride(el) {
|
|
|
5847
5918
|
}
|
|
5848
5919
|
return null;
|
|
5849
5920
|
}
|
|
5850
|
-
function
|
|
5921
|
+
function resolveLocalImageSourcePath(src, options) {
|
|
5851
5922
|
const trimmed = src.trim();
|
|
5852
5923
|
if (!trimmed || trimmed.startsWith("data:")) return null;
|
|
5853
5924
|
const { basePath } = splitUrlSuffix(trimmed);
|
|
@@ -6070,7 +6141,7 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6070
6141
|
const img = images[i];
|
|
6071
6142
|
const src = (img.getAttribute("src") || "").trim();
|
|
6072
6143
|
if (!hasGifExtension(src)) continue;
|
|
6073
|
-
const sourcePath =
|
|
6144
|
+
const sourcePath = resolveLocalImageSourcePath(src, options);
|
|
6074
6145
|
if (!sourcePath) continue;
|
|
6075
6146
|
const bytes = readFileSync5(sourcePath);
|
|
6076
6147
|
const metadata = parseAnimatedGifMetadata(bytes);
|
|
@@ -6118,19 +6189,74 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6118
6189
|
};
|
|
6119
6190
|
}
|
|
6120
6191
|
|
|
6121
|
-
// src/services/
|
|
6122
|
-
import {
|
|
6123
|
-
import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
|
|
6124
|
-
|
|
6125
|
-
// src/services/assetMediaType.ts
|
|
6126
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
6192
|
+
// src/services/heifImagePrep.ts
|
|
6193
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6127
6194
|
import {
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6195
|
+
closeSync as closeSync4,
|
|
6196
|
+
copyFileSync as copyFileSync3,
|
|
6197
|
+
mkdirSync as mkdirSync6,
|
|
6198
|
+
openSync as openSync4,
|
|
6199
|
+
readFileSync as readFileSync6,
|
|
6200
|
+
readSync as readSync3,
|
|
6201
|
+
rmSync as rmSync8,
|
|
6202
|
+
statSync as statSync6,
|
|
6203
|
+
writeFileSync as writeFileSync4
|
|
6204
|
+
} from "node:fs";
|
|
6205
|
+
import { dirname as dirname6, join as join14, resolve as resolve6 } from "node:path";
|
|
6206
|
+
import { parseHTML as parseHTML4 } from "linkedom";
|
|
6207
|
+
import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
|
|
6208
|
+
|
|
6209
|
+
// src/services/heifRotation.ts
|
|
6210
|
+
function* boxes(buf, start, end) {
|
|
6211
|
+
let at = start;
|
|
6212
|
+
while (at + 8 <= end) {
|
|
6213
|
+
let size = buf.readUInt32BE(at);
|
|
6214
|
+
let header = 8;
|
|
6215
|
+
if (size === 1 && at + 16 <= end) {
|
|
6216
|
+
size = Number(buf.readBigUInt64BE(at + 8));
|
|
6217
|
+
header = 16;
|
|
6218
|
+
} else if (size === 0) {
|
|
6219
|
+
size = end - at;
|
|
6220
|
+
}
|
|
6221
|
+
if (size < header || at + size > end) return;
|
|
6222
|
+
yield { type: buf.toString("latin1", at + 4, at + 8), start: at + header, end: at + size };
|
|
6223
|
+
at += size;
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
function child(buf, parent, type, skip = 0) {
|
|
6227
|
+
return [...boxes(buf, parent.start + skip, parent.end)].filter((box2) => box2.type === type);
|
|
6228
|
+
}
|
|
6229
|
+
function readHeifPrimaryRotation(buf) {
|
|
6230
|
+
try {
|
|
6231
|
+
const root = { type: "", start: 0, end: buf.length };
|
|
6232
|
+
const meta = child(buf, root, "meta")[0];
|
|
6233
|
+
const pitm = meta && child(buf, meta, "pitm", 4)[0];
|
|
6234
|
+
const iprp = meta && child(buf, meta, "iprp", 4)[0];
|
|
6235
|
+
const ipco = iprp && child(buf, iprp, "ipco")[0];
|
|
6236
|
+
if (!pitm || !ipco || !iprp) return 0;
|
|
6237
|
+
const primaryId = buf[pitm.start] === 0 ? buf.readUInt16BE(pitm.start + 4) : buf.readUInt32BE(pitm.start + 4);
|
|
6238
|
+
const props = [...boxes(buf, ipco.start, ipco.end)];
|
|
6239
|
+
for (const ipma of child(buf, iprp, "ipma")) {
|
|
6240
|
+
const version = buf[ipma.start];
|
|
6241
|
+
const wide = (buf[ipma.start + 3] & 1) === 1;
|
|
6242
|
+
let at = ipma.start + 8;
|
|
6243
|
+
for (let n = buf.readUInt32BE(ipma.start + 4); n > 0 && at < ipma.end; n--) {
|
|
6244
|
+
const id = version < 1 ? buf.readUInt16BE(at) : buf.readUInt32BE(at);
|
|
6245
|
+
at += version < 1 ? 2 : 4;
|
|
6246
|
+
const count = buf[at++];
|
|
6247
|
+
for (let i = 0; i < count; i++) {
|
|
6248
|
+
const index = wide ? buf.readUInt16BE(at) & 32767 : buf[at] & 127;
|
|
6249
|
+
at += wide ? 2 : 1;
|
|
6250
|
+
const prop = props[index - 1];
|
|
6251
|
+
if (id === primaryId && prop?.type === "irot")
|
|
6252
|
+
return (buf[prop.start] & 3) * 90;
|
|
6253
|
+
}
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
} catch {
|
|
6257
|
+
}
|
|
6258
|
+
return 0;
|
|
6259
|
+
}
|
|
6134
6260
|
|
|
6135
6261
|
// src/utils/semaphore.ts
|
|
6136
6262
|
var Semaphore = class {
|
|
@@ -6145,10 +6271,10 @@ var Semaphore = class {
|
|
|
6145
6271
|
this.active++;
|
|
6146
6272
|
return () => this.release();
|
|
6147
6273
|
}
|
|
6148
|
-
return new Promise((
|
|
6274
|
+
return new Promise((resolve13) => {
|
|
6149
6275
|
this.queue.push(() => {
|
|
6150
6276
|
this.active++;
|
|
6151
|
-
|
|
6277
|
+
resolve13(() => this.release());
|
|
6152
6278
|
});
|
|
6153
6279
|
});
|
|
6154
6280
|
}
|
|
@@ -6179,7 +6305,229 @@ async function withMediaProbeSlot(operation) {
|
|
|
6179
6305
|
}
|
|
6180
6306
|
}
|
|
6181
6307
|
|
|
6308
|
+
// src/services/heifImagePrep.ts
|
|
6309
|
+
var PREPARED_HEIF_SUBDIR = "_heif_images";
|
|
6310
|
+
var CACHE_SCHEMA2 = "hfheif-v1";
|
|
6311
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
6312
|
+
var HEIF_IMAGE_CONVERSION_FAILED = "HEIF_IMAGE_CONVERSION_FAILED";
|
|
6313
|
+
var HEIF_TOOL_UNAVAILABLE = "HEIF_TOOL_UNAVAILABLE";
|
|
6314
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
|
|
6315
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
6316
|
+
var DATA_URL_RE = /^data:image\/(?:heic|heif)(?:-sequence)?;base64,(.*)$/is;
|
|
6317
|
+
var HeifImageConversionError = class extends Error {
|
|
6318
|
+
code = HEIF_IMAGE_CONVERSION_FAILED;
|
|
6319
|
+
owner = "user";
|
|
6320
|
+
retryable = false;
|
|
6321
|
+
constructor(label, reason) {
|
|
6322
|
+
super(
|
|
6323
|
+
`Could not convert HEIC/HEIF image ${label} to JPEG: ${reason}. Re-export the photo as JPEG or PNG and update its src.`
|
|
6324
|
+
);
|
|
6325
|
+
this.name = "HeifImageConversionError";
|
|
6326
|
+
}
|
|
6327
|
+
};
|
|
6328
|
+
var HeifToolUnavailableError = class extends Error {
|
|
6329
|
+
code = HEIF_TOOL_UNAVAILABLE;
|
|
6330
|
+
owner = "system";
|
|
6331
|
+
retryable = false;
|
|
6332
|
+
constructor(label, reason) {
|
|
6333
|
+
super(
|
|
6334
|
+
`Could not convert HEIC/HEIF image ${label}: ${reason}. Converting HEIC needs ffmpeg and ffprobe 7.0 or newer with the hevc decoder.`
|
|
6335
|
+
);
|
|
6336
|
+
this.name = "HeifToolUnavailableError";
|
|
6337
|
+
}
|
|
6338
|
+
};
|
|
6339
|
+
var TOOL_FAILURE_RE = /(?:ffmpeg|ffprobe) not found|ENOENT|Option not found|Unrecognized option|Unknown decoder|Decoder \S+ not found/i;
|
|
6340
|
+
function isHeifBytes(head) {
|
|
6341
|
+
const bytes = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
|
|
6342
|
+
if (bytes.length < 16 || bytes.toString("ascii", 4, 8) !== "ftyp") return false;
|
|
6343
|
+
const boxEnd = Math.min(bytes.readUInt32BE(0), bytes.length);
|
|
6344
|
+
if (boxEnd < 16) return false;
|
|
6345
|
+
const brands = [bytes.toString("ascii", 8, 12)];
|
|
6346
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
|
|
6347
|
+
brands.push(bytes.toString("ascii", offset, offset + 4));
|
|
6348
|
+
}
|
|
6349
|
+
return !brands.some((b) => AVIF_BRANDS.has(b)) && brands.some((b) => HEIF_BRANDS.has(b));
|
|
6350
|
+
}
|
|
6351
|
+
function readHead(path) {
|
|
6352
|
+
let fd;
|
|
6353
|
+
try {
|
|
6354
|
+
fd = openSync4(path, "r");
|
|
6355
|
+
const buf = Buffer.alloc(4096);
|
|
6356
|
+
return buf.subarray(0, readSync3(fd, buf, 0, buf.length, 0));
|
|
6357
|
+
} catch {
|
|
6358
|
+
return null;
|
|
6359
|
+
} finally {
|
|
6360
|
+
if (fd !== void 0) closeSync4(fd);
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
async function probeJson(path, withGroups) {
|
|
6364
|
+
const stdout = await runFfprobe(
|
|
6365
|
+
path,
|
|
6366
|
+
withGroups ? ["-show_stream_groups", "-show_streams", "-of", "json"] : ["-show_streams", "-of", "json"]
|
|
6367
|
+
);
|
|
6368
|
+
return JSON.parse(stdout);
|
|
6369
|
+
}
|
|
6370
|
+
var ROTATION_FILTER = {
|
|
6371
|
+
0: "",
|
|
6372
|
+
90: "transpose=2",
|
|
6373
|
+
180: "hflip,vflip",
|
|
6374
|
+
270: "transpose=1"
|
|
6375
|
+
};
|
|
6376
|
+
function primaryGrid(probed) {
|
|
6377
|
+
const groups = probed.stream_groups ?? [];
|
|
6378
|
+
return (groups.find((g) => g.disposition?.default === 1) ?? groups[0])?.components?.[0];
|
|
6379
|
+
}
|
|
6380
|
+
async function convertHeifToJpeg(request) {
|
|
6381
|
+
const probed = await probeJson(request.inputPath, true);
|
|
6382
|
+
const grid = primaryGrid(probed);
|
|
6383
|
+
const rotation = readHeifPrimaryRotation(readFileSync6(request.inputPath));
|
|
6384
|
+
const filters = [];
|
|
6385
|
+
let inputs = [];
|
|
6386
|
+
let size = probed.streams?.[0];
|
|
6387
|
+
if (grid?.subcomponents?.length) {
|
|
6388
|
+
size = grid;
|
|
6389
|
+
const layout = grid.subcomponents.map(
|
|
6390
|
+
(t) => `${t.tile_horizontal_offset}_${t.tile_vertical_offset}`
|
|
6391
|
+
);
|
|
6392
|
+
inputs = grid.subcomponents.map((t) => `[0:${t.stream_index}]`);
|
|
6393
|
+
filters.push(
|
|
6394
|
+
`${inputs.join("")}xstack=inputs=${inputs.length}:layout=${layout.join("|")}`,
|
|
6395
|
+
`crop=${grid.width}:${grid.height}:${grid.horizontal_offset ?? 0}:${grid.vertical_offset ?? 0}`
|
|
6396
|
+
);
|
|
6397
|
+
}
|
|
6398
|
+
if (!size?.width || !size.height) throw new Error("no declared image size");
|
|
6399
|
+
if (rotation && grid) filters.push(ROTATION_FILTER[rotation]);
|
|
6400
|
+
const mapArgs = filters.length > 0 ? ["-filter_complex", `${filters.join(",")}[v]`, "-map", "[v]"] : [];
|
|
6401
|
+
const result = await runFfmpeg3(
|
|
6402
|
+
[
|
|
6403
|
+
"-hide_banner",
|
|
6404
|
+
"-loglevel",
|
|
6405
|
+
"error",
|
|
6406
|
+
"-i",
|
|
6407
|
+
request.inputPath,
|
|
6408
|
+
...mapArgs,
|
|
6409
|
+
"-frames:v",
|
|
6410
|
+
"1",
|
|
6411
|
+
"-q:v",
|
|
6412
|
+
"2",
|
|
6413
|
+
"-f",
|
|
6414
|
+
"mjpeg",
|
|
6415
|
+
"-y",
|
|
6416
|
+
request.outputPath
|
|
6417
|
+
],
|
|
6418
|
+
{ timeout: request.timeoutMs ?? DEFAULT_TIMEOUT_MS }
|
|
6419
|
+
);
|
|
6420
|
+
if (!result.success) {
|
|
6421
|
+
throw result.error ?? new Error(`ffmpeg exited ${result.exitCode}: ${result.stderr.slice(-300)}`);
|
|
6422
|
+
}
|
|
6423
|
+
const out = (await probeJson(request.outputPath, false)).streams?.[0];
|
|
6424
|
+
const same = out?.width === size.width && out.height === size.height;
|
|
6425
|
+
const swapped = out?.width === size.height && out.height === size.width;
|
|
6426
|
+
if (!same && !swapped) {
|
|
6427
|
+
throw new Error(
|
|
6428
|
+
`wrong size: decoded ${out?.width}x${out?.height}, file declares ${size.width}x${size.height}`
|
|
6429
|
+
);
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
function resolveImageSource(src, options) {
|
|
6433
|
+
const data = DATA_URL_RE.exec(src);
|
|
6434
|
+
if (data) {
|
|
6435
|
+
const bytes = Buffer.from(data[1], "base64");
|
|
6436
|
+
return isHeifBytes(bytes.subarray(0, 4096)) ? { path: null, bytes, label: '"inline image"' } : null;
|
|
6437
|
+
}
|
|
6438
|
+
const path = resolveLocalImageSourcePath(src, options);
|
|
6439
|
+
const head = path ? readHead(path) : null;
|
|
6440
|
+
if (!path || !head || !isHeifBytes(head)) return null;
|
|
6441
|
+
return { path, bytes: readFileSync6(path), label: `"${splitUrlSuffix(src).basePath}"` };
|
|
6442
|
+
}
|
|
6443
|
+
function isUsableFile2(path) {
|
|
6444
|
+
try {
|
|
6445
|
+
return statSync6(path).size > 0;
|
|
6446
|
+
} catch {
|
|
6447
|
+
return false;
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
6451
|
+
mkdirSync6(dirname6(cachePath), { recursive: true });
|
|
6452
|
+
mkdirSync6(dirname6(outputPath), { recursive: true });
|
|
6453
|
+
if (!isUsableFile2(cachePath)) {
|
|
6454
|
+
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
6455
|
+
const inlinePath = `${tmpPath}.heic`;
|
|
6456
|
+
const inputPath = source.path ?? inlinePath;
|
|
6457
|
+
try {
|
|
6458
|
+
if (!source.path) writeFileSync4(inlinePath, source.bytes);
|
|
6459
|
+
await (options.transcode ?? convertHeifToJpeg)({
|
|
6460
|
+
inputPath,
|
|
6461
|
+
outputPath: tmpPath,
|
|
6462
|
+
timeoutMs: options.timeoutMs
|
|
6463
|
+
});
|
|
6464
|
+
if (!isUsableFile2(tmpPath)) throw new Error("conversion produced an empty file");
|
|
6465
|
+
renameWithRetry4(tmpPath, cachePath);
|
|
6466
|
+
} catch (error) {
|
|
6467
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6468
|
+
throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
|
|
6469
|
+
} finally {
|
|
6470
|
+
rmSync8(tmpPath, { force: true });
|
|
6471
|
+
rmSync8(inlinePath, { force: true });
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
|
|
6475
|
+
copyFileSync3(cachePath, outputPath);
|
|
6476
|
+
}
|
|
6477
|
+
}
|
|
6478
|
+
async function prepareHeifImageInputs(html, options) {
|
|
6479
|
+
const outputDir = options.outputDir ?? join14(options.downloadDir, PREPARED_HEIF_SUBDIR);
|
|
6480
|
+
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
|
|
6481
|
+
const cacheDir = options.cacheDir ?? outputDir;
|
|
6482
|
+
const preparedAssets = /* @__PURE__ */ new Map();
|
|
6483
|
+
const preparedImages = [];
|
|
6484
|
+
if (!/<img\b/i.test(html)) return { html, preparedAssets, preparedImages };
|
|
6485
|
+
const { document: document2 } = parseHTML4(html);
|
|
6486
|
+
const found = [];
|
|
6487
|
+
for (const img of Array.from(document2.querySelectorAll("img[src]"))) {
|
|
6488
|
+
const src = (img.getAttribute("src") || "").trim();
|
|
6489
|
+
const source = src ? resolveImageSource(src, options) : null;
|
|
6490
|
+
if (!source) continue;
|
|
6491
|
+
const hash = createHash5("sha256").update(CACHE_SCHEMA2).update("\0").update(source.bytes).digest("hex");
|
|
6492
|
+
found.push({ img, src, source, filename: `${CACHE_SCHEMA2}-${hash.slice(0, 24)}.jpg` });
|
|
6493
|
+
}
|
|
6494
|
+
if (found.length === 0) return { html, preparedAssets, preparedImages };
|
|
6495
|
+
const unique = new Map(found.map((item) => [item.filename, item.source]));
|
|
6496
|
+
const failed = /* @__PURE__ */ new Set();
|
|
6497
|
+
await Promise.all(
|
|
6498
|
+
[...unique].map(
|
|
6499
|
+
([filename, source]) => withMediaProbeSlot(
|
|
6500
|
+
() => ensurePreparedJpeg(source, join14(cacheDir, filename), join14(outputDir, filename), options)
|
|
6501
|
+
).catch((error) => {
|
|
6502
|
+
if (!options.onFailure) throw error;
|
|
6503
|
+
failed.add(filename);
|
|
6504
|
+
options.onFailure(error);
|
|
6505
|
+
})
|
|
6506
|
+
)
|
|
6507
|
+
);
|
|
6508
|
+
for (const { img, src, filename } of found) {
|
|
6509
|
+
if (failed.has(filename)) continue;
|
|
6510
|
+
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
6511
|
+
img.setAttribute("src", outputSrc);
|
|
6512
|
+
preparedAssets.set(outputSrc, join14(outputDir, filename));
|
|
6513
|
+
preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join14(outputDir, filename) });
|
|
6514
|
+
}
|
|
6515
|
+
return { html: document2.toString(), preparedAssets, preparedImages };
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6518
|
+
// src/services/htmlCompiler.ts
|
|
6519
|
+
import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
|
|
6520
|
+
import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
|
|
6521
|
+
|
|
6182
6522
|
// src/services/assetMediaType.ts
|
|
6523
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
6524
|
+
import {
|
|
6525
|
+
fingerprintElementId,
|
|
6526
|
+
isNotMediaPayload,
|
|
6527
|
+
NotMediaPayloadError,
|
|
6528
|
+
probeMediaProfile,
|
|
6529
|
+
resolveProjectRelativeSrc as resolveProjectRelativeSrc2
|
|
6530
|
+
} from "@hyperframes/engine";
|
|
6183
6531
|
var ASSET_MEDIA_TYPE_MISMATCH = "ASSET_MEDIA_TYPE_MISMATCH";
|
|
6184
6532
|
var AssetMediaTypeMismatchError = class extends Error {
|
|
6185
6533
|
code = ASSET_MEDIA_TYPE_MISMATCH;
|
|
@@ -6289,7 +6637,7 @@ function logRemoteDownloadTelemetry(event) {
|
|
|
6289
6637
|
}
|
|
6290
6638
|
var INFERRED_MEDIA_DURATION_ATTR = "data-hf-inferred-duration";
|
|
6291
6639
|
function parseSubCompHtmlForValidity(html) {
|
|
6292
|
-
return
|
|
6640
|
+
return parseHTML5(html).document;
|
|
6293
6641
|
}
|
|
6294
6642
|
function endsWithSourceUrl(code) {
|
|
6295
6643
|
const trimmed = code.trimEnd();
|
|
@@ -6297,7 +6645,7 @@ function endsWithSourceUrl(code) {
|
|
|
6297
6645
|
return lastLine.startsWith("//# sourceURL=") || lastLine.startsWith("//@ sourceURL=");
|
|
6298
6646
|
}
|
|
6299
6647
|
function prepareCompositionScripts(html) {
|
|
6300
|
-
const { document: document2 } =
|
|
6648
|
+
const { document: document2 } = parseHTML5(html);
|
|
6301
6649
|
for (const el of document2.querySelectorAll("script:not([src])")) {
|
|
6302
6650
|
const isModule = (el.getAttribute("type") || "").trim().toLowerCase() === "module";
|
|
6303
6651
|
const runsAsScript = isModule || isJavaScriptType(el);
|
|
@@ -6334,14 +6682,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
|
|
|
6334
6682
|
}
|
|
6335
6683
|
};
|
|
6336
6684
|
function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
|
|
6337
|
-
const { document: document2 } =
|
|
6685
|
+
const { document: document2 } = parseHTML5(html);
|
|
6338
6686
|
const hosts = [...document2.querySelectorAll("[data-composition-src]")];
|
|
6339
6687
|
const problems = [];
|
|
6340
6688
|
for (const el of hosts) {
|
|
6341
6689
|
const srcPath = el.getAttribute("data-composition-src");
|
|
6342
6690
|
if (!srcPath) continue;
|
|
6343
6691
|
if (isUnresolvedAssetPlaceholder(srcPath)) continue;
|
|
6344
|
-
const filePath =
|
|
6692
|
+
const filePath = resolve7(projectDir, srcPath);
|
|
6345
6693
|
if (visited.has(filePath)) continue;
|
|
6346
6694
|
const read = readProjectFile(filePath);
|
|
6347
6695
|
if (read.kind === "missing") {
|
|
@@ -6397,7 +6745,7 @@ function runtimeRoot(document2) {
|
|
|
6397
6745
|
}
|
|
6398
6746
|
function detectRenderModeHints(html, pageMotionBlur) {
|
|
6399
6747
|
const reasons = [];
|
|
6400
|
-
const { document: document2 } =
|
|
6748
|
+
const { document: document2 } = parseHTML5(html);
|
|
6401
6749
|
if (document2.querySelector("canvas[layoutsubtree]")) {
|
|
6402
6750
|
reasons.push({
|
|
6403
6751
|
code: "htmlInCanvas",
|
|
@@ -6439,7 +6787,7 @@ function detectThreeDTransformUsage(html) {
|
|
|
6439
6787
|
}
|
|
6440
6788
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
6441
6789
|
function detectAncestorBackgroundImage(html) {
|
|
6442
|
-
const { document: document2 } =
|
|
6790
|
+
const { document: document2 } = parseHTML5(html);
|
|
6443
6791
|
const root = document2.querySelector("[data-composition-id]");
|
|
6444
6792
|
if (!root) return false;
|
|
6445
6793
|
const ancestors = [];
|
|
@@ -6483,10 +6831,10 @@ function detectShaderTransitionUsage(html) {
|
|
|
6483
6831
|
}
|
|
6484
6832
|
return false;
|
|
6485
6833
|
}
|
|
6486
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
6834
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
6487
6835
|
let filePath = src;
|
|
6488
6836
|
if (isHttpUrl(src)) {
|
|
6489
|
-
if (!existsSync10(downloadDir))
|
|
6837
|
+
if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
|
|
6490
6838
|
try {
|
|
6491
6839
|
filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
|
|
6492
6840
|
onTelemetry: logRemoteDownloadTelemetry
|
|
@@ -6496,7 +6844,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6496
6844
|
}
|
|
6497
6845
|
} else {
|
|
6498
6846
|
filePath = decodedUrlPath(src);
|
|
6499
|
-
if (!filePath.startsWith("/")) filePath =
|
|
6847
|
+
if (!filePath.startsWith("/")) filePath = join15(baseDir, filePath);
|
|
6500
6848
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
6501
6849
|
}
|
|
6502
6850
|
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
@@ -6510,50 +6858,57 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6510
6858
|
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
|
|
6511
6859
|
return wrapped;
|
|
6512
6860
|
};
|
|
6513
|
-
|
|
6514
|
-
let profile;
|
|
6861
|
+
const probe = () => withMediaProbeSlot(async () => {
|
|
6515
6862
|
try {
|
|
6516
6863
|
await assertMediaPayload(filePath, elementIdentity);
|
|
6517
|
-
profile = await probeMediaProfile2(filePath);
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
if (error instanceof NotMediaPayloadError2) {
|
|
6521
|
-
log?.warn(
|
|
6522
|
-
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
|
|
6523
|
-
);
|
|
6524
|
-
}
|
|
6525
|
-
return { duration: null, resolvedPath: filePath };
|
|
6864
|
+
const profile = await probeMediaProfile2(filePath);
|
|
6865
|
+
if (tagName === "video") {
|
|
6866
|
+
return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
|
|
6526
6867
|
}
|
|
6527
|
-
throw withSrcContext(error);
|
|
6528
|
-
}
|
|
6529
|
-
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
|
|
6530
|
-
let metadata;
|
|
6531
|
-
if (tagName === "video") {
|
|
6532
6868
|
try {
|
|
6533
|
-
|
|
6534
|
-
} catch (error) {
|
|
6535
|
-
throw withSrcContext(error);
|
|
6536
|
-
}
|
|
6537
|
-
} else {
|
|
6538
|
-
try {
|
|
6539
|
-
metadata = await extractAudioMetadata(filePath);
|
|
6869
|
+
return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
|
|
6540
6870
|
} catch {
|
|
6541
|
-
return {
|
|
6871
|
+
return { profile, fileDuration: null };
|
|
6542
6872
|
}
|
|
6873
|
+
} catch (error) {
|
|
6874
|
+
return { error };
|
|
6543
6875
|
}
|
|
6544
|
-
const fileDuration = metadata.durationSeconds;
|
|
6545
|
-
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6546
|
-
fileDuration,
|
|
6547
|
-
mediaStart,
|
|
6548
|
-
playbackRate
|
|
6549
|
-
);
|
|
6550
|
-
return { duration, resolvedPath: filePath };
|
|
6551
6876
|
});
|
|
6877
|
+
const key = `${tagName}\0${filePath}`;
|
|
6878
|
+
let probed = probes?.get(key);
|
|
6879
|
+
if (!probed) {
|
|
6880
|
+
probed = probe();
|
|
6881
|
+
probes?.set(key, probed);
|
|
6882
|
+
}
|
|
6883
|
+
const outcome = await probed;
|
|
6884
|
+
if ("error" in outcome) {
|
|
6885
|
+
if (tagName !== "video") {
|
|
6886
|
+
if (outcome.error instanceof NotMediaPayloadError2) {
|
|
6887
|
+
log?.warn(
|
|
6888
|
+
`[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
|
|
6889
|
+
);
|
|
6890
|
+
}
|
|
6891
|
+
return { duration: null, resolvedPath: filePath };
|
|
6892
|
+
}
|
|
6893
|
+
throw withSrcContext(outcome.error);
|
|
6894
|
+
}
|
|
6895
|
+
assertAssetMediaTypeProfile(
|
|
6896
|
+
tagName === "video" ? "video" : "audio",
|
|
6897
|
+
outcome.profile,
|
|
6898
|
+
elementIdentity
|
|
6899
|
+
);
|
|
6900
|
+
if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
|
|
6901
|
+
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6902
|
+
outcome.fileDuration,
|
|
6903
|
+
mediaStart,
|
|
6904
|
+
playbackRate
|
|
6905
|
+
);
|
|
6906
|
+
return { duration, resolvedPath: filePath };
|
|
6552
6907
|
}
|
|
6553
6908
|
function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
6554
6909
|
const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
|
|
6555
6910
|
if (unresolvedIds.size === 0) return html;
|
|
6556
|
-
const { document: document2 } =
|
|
6911
|
+
const { document: document2 } = parseHTML5(html);
|
|
6557
6912
|
let changed = false;
|
|
6558
6913
|
for (const element of document2.querySelectorAll("video[data-var-src], audio[data-var-src]")) {
|
|
6559
6914
|
if (!unresolvedIds.has(element.id)) continue;
|
|
@@ -6562,7 +6917,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
6562
6917
|
}
|
|
6563
6918
|
return changed ? document2.toString() : html;
|
|
6564
6919
|
}
|
|
6565
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
6920
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
|
|
6566
6921
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
6567
6922
|
const mediaUnresolved = unresolved.filter(
|
|
6568
6923
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -6579,7 +6934,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6579
6934
|
el.tagName,
|
|
6580
6935
|
el.id,
|
|
6581
6936
|
log,
|
|
6582
|
-
urlByAbsentLocalPath
|
|
6937
|
+
urlByAbsentLocalPath,
|
|
6938
|
+
probes
|
|
6583
6939
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
6584
6940
|
)
|
|
6585
6941
|
);
|
|
@@ -6600,7 +6956,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6600
6956
|
downloadDir,
|
|
6601
6957
|
el.tagName,
|
|
6602
6958
|
el.id,
|
|
6603
|
-
log
|
|
6959
|
+
log,
|
|
6960
|
+
void 0,
|
|
6961
|
+
probes
|
|
6604
6962
|
);
|
|
6605
6963
|
return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
|
|
6606
6964
|
})
|
|
@@ -6622,15 +6980,15 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6622
6980
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
6623
6981
|
return { html: compiledHtml, unresolvedCompositions };
|
|
6624
6982
|
}
|
|
6625
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
6983
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
|
|
6626
6984
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
6627
|
-
const { document: document2 } =
|
|
6985
|
+
const { document: document2 } = parseHTML5(html);
|
|
6628
6986
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
6629
6987
|
const workItems = [];
|
|
6630
6988
|
for (const el of compEls) {
|
|
6631
6989
|
const srcPath = el.getAttribute("data-composition-src");
|
|
6632
6990
|
if (!srcPath) continue;
|
|
6633
|
-
const filePath =
|
|
6991
|
+
const filePath = resolve7(projectDir, srcPath);
|
|
6634
6992
|
if (visited.has(filePath)) {
|
|
6635
6993
|
continue;
|
|
6636
6994
|
}
|
|
@@ -6647,17 +7005,19 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6647
7005
|
workItems.map(async (item) => {
|
|
6648
7006
|
const { html: compiledSub } = await compileHtmlFile(
|
|
6649
7007
|
rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
|
|
6650
|
-
|
|
7008
|
+
dirname7(item.filePath),
|
|
6651
7009
|
downloadDir,
|
|
6652
7010
|
void 0,
|
|
6653
|
-
urlByAbsentLocalPath
|
|
7011
|
+
urlByAbsentLocalPath,
|
|
7012
|
+
probes
|
|
6654
7013
|
);
|
|
6655
7014
|
const nested = await parseSubCompositions(
|
|
6656
7015
|
compiledSub,
|
|
6657
7016
|
projectDir,
|
|
6658
7017
|
downloadDir,
|
|
6659
7018
|
item.nestedVisited,
|
|
6660
|
-
urlByAbsentLocalPath
|
|
7019
|
+
urlByAbsentLocalPath,
|
|
7020
|
+
probes
|
|
6661
7021
|
);
|
|
6662
7022
|
return {
|
|
6663
7023
|
srcPath: item.srcPath,
|
|
@@ -6675,7 +7035,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6675
7035
|
return { subCompositions };
|
|
6676
7036
|
}
|
|
6677
7037
|
function promoteCssImportsToLinkTags(html) {
|
|
6678
|
-
const { document: document2 } =
|
|
7038
|
+
const { document: document2 } = parseHTML5(html);
|
|
6679
7039
|
const head = document2.querySelector("head");
|
|
6680
7040
|
if (!head) return html;
|
|
6681
7041
|
const importRe = /@import\s+url\(\s*['"]?([^'")\s]+)['"]?\s*\)\s*;?/gi;
|
|
@@ -6744,7 +7104,7 @@ var ProducerHostIdentityMap = class extends Map {
|
|
|
6744
7104
|
}
|
|
6745
7105
|
};
|
|
6746
7106
|
function coalesceHeadStylesAndBodyScripts(html) {
|
|
6747
|
-
const { document: document2 } =
|
|
7107
|
+
const { document: document2 } = parseHTML5(html);
|
|
6748
7108
|
const head = document2.querySelector("head");
|
|
6749
7109
|
const body = document2.querySelector("body");
|
|
6750
7110
|
if (!head && !body) return html;
|
|
@@ -6795,7 +7155,7 @@ function coalesceHeadStylesAndBodyScripts(html) {
|
|
|
6795
7155
|
return document2.toString();
|
|
6796
7156
|
}
|
|
6797
7157
|
function inlineSubCompositions(html, subCompositions, projectDir, variableOverrides = {}) {
|
|
6798
|
-
const { document: document2 } =
|
|
7158
|
+
const { document: document2 } = parseHTML5(html);
|
|
6799
7159
|
const head = document2.querySelector("head");
|
|
6800
7160
|
const body = document2.querySelector("body");
|
|
6801
7161
|
const hosts = Array.from(document2.querySelectorAll("[data-composition-src]"));
|
|
@@ -6824,17 +7184,17 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
|
|
|
6824
7184
|
resolveHtml: (srcPath) => {
|
|
6825
7185
|
let compHtml = subCompositions.get(srcPath) || null;
|
|
6826
7186
|
if (!compHtml) {
|
|
6827
|
-
const read = readProjectFile(
|
|
7187
|
+
const read = readProjectFile(resolve7(projectDir, srcPath));
|
|
6828
7188
|
if (read.kind === "file") {
|
|
6829
7189
|
compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
|
|
6830
7190
|
}
|
|
6831
7191
|
}
|
|
6832
7192
|
return compHtml;
|
|
6833
7193
|
},
|
|
6834
|
-
parseHtml: (htmlStr) =>
|
|
7194
|
+
parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
|
|
6835
7195
|
// Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
|
|
6836
7196
|
// against its own directory, project-root refs stay as authored.
|
|
6837
|
-
assetExists: (path) => existsSync10(
|
|
7197
|
+
assetExists: (path) => existsSync10(resolve7(projectDir, path)),
|
|
6838
7198
|
// Preserve the authored root wrapper as a child of the host, matching
|
|
6839
7199
|
// the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
|
|
6840
7200
|
// which the runtime compositionLoader mirrors with its own copy for the
|
|
@@ -6935,7 +7295,7 @@ ${html}
|
|
|
6935
7295
|
</html>`;
|
|
6936
7296
|
}
|
|
6937
7297
|
function injectTextRenderingRule(html) {
|
|
6938
|
-
const { document: document2 } =
|
|
7298
|
+
const { document: document2 } = parseHTML5(html);
|
|
6939
7299
|
const head = document2.querySelector("head");
|
|
6940
7300
|
if (!head) return html;
|
|
6941
7301
|
if (document2.querySelector("style[data-hyperframes-text-rendering]")) {
|
|
@@ -6959,7 +7319,7 @@ function matchesScriptIntegrity(bytes, metadata) {
|
|
|
6959
7319
|
(alg) => hashes.some((hash) => hash[1]?.toLowerCase() === alg)
|
|
6960
7320
|
);
|
|
6961
7321
|
if (!strongest) return true;
|
|
6962
|
-
const actual =
|
|
7322
|
+
const actual = createHash6(strongest).update(bytes).digest();
|
|
6963
7323
|
return hashes.some(
|
|
6964
7324
|
(hash) => hash[1]?.toLowerCase() === strongest && actual.equals(Buffer.from(hash[2], "base64"))
|
|
6965
7325
|
);
|
|
@@ -6967,7 +7327,7 @@ function matchesScriptIntegrity(bytes, metadata) {
|
|
|
6967
7327
|
async function inlineExternalScripts(html) {
|
|
6968
7328
|
const fullHtml = ensureFullDocument(html);
|
|
6969
7329
|
const wrappedFragment = fullHtml !== html;
|
|
6970
|
-
const { document: document2 } =
|
|
7330
|
+
const { document: document2 } = parseHTML5(fullHtml);
|
|
6971
7331
|
const scripts = document2.querySelectorAll("script[src]");
|
|
6972
7332
|
const externalScripts = [];
|
|
6973
7333
|
for (const el of scripts) {
|
|
@@ -7016,12 +7376,12 @@ ${safeText}
|
|
|
7016
7376
|
return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
7017
7377
|
}
|
|
7018
7378
|
function collectExternalAssets(html, projectDir) {
|
|
7019
|
-
const absProjectDir =
|
|
7379
|
+
const absProjectDir = resolve7(projectDir);
|
|
7020
7380
|
const externalAssets = /* @__PURE__ */ new Map();
|
|
7021
7381
|
function processPath(rawPath) {
|
|
7022
7382
|
const trimmed = rawPath.trim();
|
|
7023
7383
|
if (isNonRelativeUrl(trimmed)) return null;
|
|
7024
|
-
const absPath =
|
|
7384
|
+
const absPath = resolve7(absProjectDir, trimmed);
|
|
7025
7385
|
if (isPathInside2(absPath, absProjectDir)) {
|
|
7026
7386
|
return null;
|
|
7027
7387
|
}
|
|
@@ -7030,7 +7390,7 @@ function collectExternalAssets(html, projectDir) {
|
|
|
7030
7390
|
externalAssets.set(safeKey, absPath);
|
|
7031
7391
|
return safeKey;
|
|
7032
7392
|
}
|
|
7033
|
-
const { document: document2 } =
|
|
7393
|
+
const { document: document2 } = parseHTML5(html);
|
|
7034
7394
|
for (const el of document2.querySelectorAll("[src], [href]")) {
|
|
7035
7395
|
for (const attr of ["src", "href"]) {
|
|
7036
7396
|
const val = (el.getAttribute(attr) || "").trim();
|
|
@@ -7075,7 +7435,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
|
|
|
7075
7435
|
var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
7076
7436
|
async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
|
|
7077
7437
|
if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
|
|
7078
|
-
if (!existsSync10(remoteDir))
|
|
7438
|
+
if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
|
|
7079
7439
|
const urlToLocal = /* @__PURE__ */ new Map();
|
|
7080
7440
|
await Promise.all(
|
|
7081
7441
|
[...urlSet].map(async (url) => {
|
|
@@ -7122,7 +7482,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
7122
7482
|
return downloadAndRewriteUrls(
|
|
7123
7483
|
urlSet,
|
|
7124
7484
|
html,
|
|
7125
|
-
|
|
7485
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7126
7486
|
"Remote media download failed",
|
|
7127
7487
|
"Localized remote media source(s)"
|
|
7128
7488
|
);
|
|
@@ -7137,7 +7497,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
7137
7497
|
return downloadAndRewriteUrls(
|
|
7138
7498
|
urlSet,
|
|
7139
7499
|
html,
|
|
7140
|
-
|
|
7500
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7141
7501
|
"Remote image download failed",
|
|
7142
7502
|
"Localized remote image source(s)"
|
|
7143
7503
|
);
|
|
@@ -7153,7 +7513,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
7153
7513
|
return downloadAndRewriteUrls(
|
|
7154
7514
|
urlSet,
|
|
7155
7515
|
html,
|
|
7156
|
-
|
|
7516
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7157
7517
|
"Remote background-image download failed",
|
|
7158
7518
|
"Localized remote background-image(s)",
|
|
7159
7519
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -7288,7 +7648,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
|
|
|
7288
7648
|
return downloadAndRewriteUrls(
|
|
7289
7649
|
urlSet,
|
|
7290
7650
|
processed,
|
|
7291
|
-
|
|
7651
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7292
7652
|
"Remote font download failed",
|
|
7293
7653
|
"Localized remote font face(s)",
|
|
7294
7654
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -7333,7 +7693,7 @@ async function embedLocalFontFaces(html, projectDir) {
|
|
|
7333
7693
|
while ((urlMatch = urlRe.exec(block)) !== null) {
|
|
7334
7694
|
const localPath = urlMatch[1];
|
|
7335
7695
|
if (!localPath || embeddedPaths.has(localPath)) continue;
|
|
7336
|
-
const absPath = localPath.startsWith("/") ? localPath :
|
|
7696
|
+
const absPath = localPath.startsWith("/") ? localPath : resolve7(projectDir, localPath);
|
|
7337
7697
|
if (!isPathInside2(absPath, projectDir)) continue;
|
|
7338
7698
|
const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
|
|
7339
7699
|
try {
|
|
@@ -7382,7 +7742,7 @@ function renderGsapDist() {
|
|
|
7382
7742
|
}
|
|
7383
7743
|
function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
|
|
7384
7744
|
let gsapDist;
|
|
7385
|
-
const exists = (path) => existsSync10(
|
|
7745
|
+
const exists = (path) => existsSync10(resolve7(projectDir, path));
|
|
7386
7746
|
return html.replace(
|
|
7387
7747
|
/(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
|
|
7388
7748
|
(full, prefix, src, file, suffix) => {
|
|
@@ -7398,8 +7758,8 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
|
|
|
7398
7758
|
if (!isPathInside2(htmlPath, projectDir)) return html;
|
|
7399
7759
|
const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
|
|
7400
7760
|
if (!entryPath.includes("/")) return html;
|
|
7401
|
-
const { document: document2 } =
|
|
7402
|
-
const assetExists = (path) => existsSync10(
|
|
7761
|
+
const { document: document2 } = parseHTML5(html);
|
|
7762
|
+
const assetExists = (path) => existsSync10(resolve7(projectDir, path));
|
|
7403
7763
|
rewriteAssetPaths(
|
|
7404
7764
|
document2.querySelectorAll("[src], [href]"),
|
|
7405
7765
|
entryPath,
|
|
@@ -7421,25 +7781,28 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
|
|
|
7421
7781
|
}
|
|
7422
7782
|
async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
|
|
7423
7783
|
const entryHtml = rebaseDirectEntryAssetPaths(
|
|
7424
|
-
|
|
7784
|
+
readFileSync7(htmlPath, "utf-8"),
|
|
7425
7785
|
projectDir,
|
|
7426
7786
|
htmlPath
|
|
7427
7787
|
);
|
|
7428
7788
|
const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
|
|
7429
7789
|
assertSubCompositionsUsable(rawHtml, projectDir);
|
|
7790
|
+
const probes = /* @__PURE__ */ new Map();
|
|
7430
7791
|
const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
|
|
7431
7792
|
rawHtml,
|
|
7432
7793
|
projectDir,
|
|
7433
7794
|
downloadDir,
|
|
7434
7795
|
options.log,
|
|
7435
|
-
options.urlByAbsentLocalPath
|
|
7796
|
+
options.urlByAbsentLocalPath,
|
|
7797
|
+
probes
|
|
7436
7798
|
);
|
|
7437
7799
|
const { subCompositions } = await parseSubCompositions(
|
|
7438
7800
|
compiledHtml,
|
|
7439
7801
|
projectDir,
|
|
7440
7802
|
downloadDir,
|
|
7441
7803
|
void 0,
|
|
7442
|
-
options.urlByAbsentLocalPath
|
|
7804
|
+
options.urlByAbsentLocalPath,
|
|
7805
|
+
probes
|
|
7443
7806
|
);
|
|
7444
7807
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
7445
7808
|
const inlinedHtml = inlineSubCompositions(
|
|
@@ -7491,12 +7854,26 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7491
7854
|
const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
|
|
7492
7855
|
const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
|
|
7493
7856
|
const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
|
|
7857
|
+
const {
|
|
7858
|
+
html: htmlWithJpegs,
|
|
7859
|
+
preparedAssets: preparedHeifAssets,
|
|
7860
|
+
preparedImages: preparedHeifImages
|
|
7861
|
+
} = await prepareHeifImageInputs(htmlWithLocalizedFonts, {
|
|
7862
|
+
projectDir,
|
|
7863
|
+
downloadDir,
|
|
7864
|
+
cacheDir: options.animatedGifCacheDir,
|
|
7865
|
+
sourceAssets: remoteImageAssets,
|
|
7866
|
+
timeoutMs: options.ffmpegProcessTimeout
|
|
7867
|
+
});
|
|
7868
|
+
if (preparedHeifImages.length > 0) {
|
|
7869
|
+
defaultLogger.info(`[Compiler] Converted ${preparedHeifImages.length} HEIC image(s) to JPEG`);
|
|
7870
|
+
}
|
|
7494
7871
|
const gifSourceAssets = new Map(remoteImageAssets);
|
|
7495
7872
|
const {
|
|
7496
7873
|
html: htmlWithPreparedGifs,
|
|
7497
7874
|
preparedAssets: preparedGifAssets,
|
|
7498
7875
|
preparedGifs
|
|
7499
|
-
} = await prepareAnimatedGifInputs(
|
|
7876
|
+
} = await prepareAnimatedGifInputs(htmlWithJpegs, {
|
|
7500
7877
|
projectDir,
|
|
7501
7878
|
downloadDir,
|
|
7502
7879
|
cacheDir: options.animatedGifCacheDir,
|
|
@@ -7523,11 +7900,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7523
7900
|
for (const [relPath, absPath] of preparedGifAssets) {
|
|
7524
7901
|
externalAssets.set(relPath, absPath);
|
|
7525
7902
|
}
|
|
7903
|
+
for (const [relPath, absPath] of preparedHeifAssets) {
|
|
7904
|
+
externalAssets.set(relPath, absPath);
|
|
7905
|
+
}
|
|
7526
7906
|
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
|
|
7527
7907
|
const masterFxChain = parseMasterFxChain(html);
|
|
7908
|
+
const advised = /* @__PURE__ */ new Set();
|
|
7528
7909
|
for (const video of videos) {
|
|
7529
7910
|
if (isHttpUrl(video.src)) continue;
|
|
7530
|
-
const videoPath =
|
|
7911
|
+
const videoPath = resolve7(projectDir, video.src);
|
|
7912
|
+
if (advised.has(videoPath)) continue;
|
|
7913
|
+
advised.add(videoPath);
|
|
7531
7914
|
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
7532
7915
|
(async () => {
|
|
7533
7916
|
const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
|
|
@@ -7547,7 +7930,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7547
7930
|
})().catch(() => {
|
|
7548
7931
|
});
|
|
7549
7932
|
}
|
|
7550
|
-
const { document: document2 } =
|
|
7933
|
+
const { document: document2 } = parseHTML5(html);
|
|
7551
7934
|
const rootEl = document2.querySelector("[data-composition-id]");
|
|
7552
7935
|
const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
|
|
7553
7936
|
const height = rootEl ? parseInt(rootEl.getAttribute("data-height") || "1920", 10) : 1920;
|
|
@@ -7954,7 +8337,7 @@ async function runCompileStage(input) {
|
|
|
7954
8337
|
abortSignal
|
|
7955
8338
|
} = input;
|
|
7956
8339
|
const compileStart = Date.now();
|
|
7957
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
8340
|
+
const compiled = await compileForRender(projectDir, htmlPath, join16(workDir, "downloads"), {
|
|
7958
8341
|
log,
|
|
7959
8342
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
7960
8343
|
allowSystemFontCapture,
|
|
@@ -7962,7 +8345,7 @@ async function runCompileStage(input) {
|
|
|
7962
8345
|
variables: input.variables,
|
|
7963
8346
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
7964
8347
|
pageMotionBlur: input.pageMotionBlur,
|
|
7965
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
8348
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join16(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
7966
8349
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
7967
8350
|
});
|
|
7968
8351
|
assertNotAborted();
|
|
@@ -8018,7 +8401,7 @@ async function runCompileStage(input) {
|
|
|
8018
8401
|
};
|
|
8019
8402
|
await preflightCompositionAssetMediaTypes({
|
|
8020
8403
|
projectDir,
|
|
8021
|
-
compiledDir:
|
|
8404
|
+
compiledDir: join16(workDir, "compiled"),
|
|
8022
8405
|
composition,
|
|
8023
8406
|
signal: abortSignal
|
|
8024
8407
|
});
|
|
@@ -8063,8 +8446,8 @@ async function runCompileStage(input) {
|
|
|
8063
8446
|
}
|
|
8064
8447
|
|
|
8065
8448
|
// src/services/render/stages/probeStage.ts
|
|
8066
|
-
import { join as
|
|
8067
|
-
import { parseHTML as
|
|
8449
|
+
import { join as join17 } from "node:path";
|
|
8450
|
+
import { parseHTML as parseHTML6 } from "linkedom";
|
|
8068
8451
|
import {
|
|
8069
8452
|
closeCaptureSession as closeCaptureSession4,
|
|
8070
8453
|
compositionRequiresWebGpu,
|
|
@@ -8078,13 +8461,14 @@ import {
|
|
|
8078
8461
|
} from "@hyperframes/engine";
|
|
8079
8462
|
import {
|
|
8080
8463
|
MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
|
|
8081
|
-
fpsToNumber as
|
|
8464
|
+
fpsToNumber as fpsToNumber6,
|
|
8082
8465
|
normalizeRateSpec as normalizeRateSpec3,
|
|
8083
8466
|
readMediaStart as readMediaStart2,
|
|
8084
8467
|
shiftRateLane as shiftRateLane2,
|
|
8085
8468
|
sourceTimeAt as sourceTimeAt3,
|
|
8086
8469
|
timeAtSourceTime
|
|
8087
8470
|
} from "@hyperframes/core";
|
|
8471
|
+
import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
|
|
8088
8472
|
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
8089
8473
|
|
|
8090
8474
|
// src/services/render/stages/probeFailures.ts
|
|
@@ -8111,7 +8495,7 @@ function durationToFrameCount(duration, fps) {
|
|
|
8111
8495
|
}
|
|
8112
8496
|
function hasScriptedAudioVolumeAutomation(html, audioCount) {
|
|
8113
8497
|
if (audioCount <= 0) return false;
|
|
8114
|
-
const { document: document2 } =
|
|
8498
|
+
const { document: document2 } = parseHTML6(html);
|
|
8115
8499
|
const scriptBodies = [...document2.querySelectorAll("script")].map((script) => script.textContent ?? "").join("\n");
|
|
8116
8500
|
if (!scriptBodies) return false;
|
|
8117
8501
|
return /\.\s*volume\s*=/i.test(scriptBodies) || /\b(?:gsap|tl|timeline|tween)\s*\.\s*(?:to|fromTo|set)\s*\([\s\S]{0,2000}\bvolume\s*:/i.test(
|
|
@@ -8119,23 +8503,30 @@ function hasScriptedAudioVolumeAutomation(html, audioCount) {
|
|
|
8119
8503
|
);
|
|
8120
8504
|
}
|
|
8121
8505
|
function hasAutoStartVideos(html) {
|
|
8122
|
-
const { document: document2 } =
|
|
8506
|
+
const { document: document2 } = parseHTML6(html);
|
|
8123
8507
|
return document2.querySelector("video[data-hf-auto-start]") !== null;
|
|
8124
8508
|
}
|
|
8509
|
+
function variableHasValue(element, variableId, variables) {
|
|
8510
|
+
if (variables && Object.hasOwn(variables, variableId)) return true;
|
|
8511
|
+
for (let scope = element.parentElement; scope; scope = scope.parentElement) {
|
|
8512
|
+
if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
|
|
8513
|
+
if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
|
|
8514
|
+
}
|
|
8515
|
+
return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
|
|
8516
|
+
}
|
|
8125
8517
|
function hasVariableBoundMedia(html, variables) {
|
|
8126
|
-
|
|
8127
|
-
const { document: document2 } = parseHTML5(html);
|
|
8518
|
+
const { document: document2 } = parseHTML6(html);
|
|
8128
8519
|
return Array.from(
|
|
8129
8520
|
document2.querySelectorAll(
|
|
8130
8521
|
"img[data-var-src], audio[data-var-src], video[data-var-src], source[data-var-src]"
|
|
8131
8522
|
)
|
|
8132
8523
|
).some((element) => {
|
|
8133
8524
|
const variableId = element.getAttribute("data-var-src")?.trim();
|
|
8134
|
-
return Boolean(variableId &&
|
|
8525
|
+
return Boolean(variableId && variableHasValue(element, variableId, variables));
|
|
8135
8526
|
});
|
|
8136
8527
|
}
|
|
8137
8528
|
function authoredMediaStarts(html) {
|
|
8138
|
-
const { document: document2 } =
|
|
8529
|
+
const { document: document2 } = parseHTML6(html);
|
|
8139
8530
|
const starts = /* @__PURE__ */ new Map();
|
|
8140
8531
|
for (const media of document2.querySelectorAll("video, audio")) {
|
|
8141
8532
|
const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
|
|
@@ -8172,7 +8563,7 @@ function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, dura
|
|
|
8172
8563
|
return existingEnd <= 0 ? projectedEnd : Math.min(existingEnd, projectedEnd);
|
|
8173
8564
|
}
|
|
8174
8565
|
function hasRuntimeMediaChanges(html) {
|
|
8175
|
-
const { document: document2 } =
|
|
8566
|
+
const { document: document2 } = parseHTML6(html);
|
|
8176
8567
|
const scriptBodies = [...document2.querySelectorAll("script")].map(
|
|
8177
8568
|
(script) => script.textContent ?? ""
|
|
8178
8569
|
);
|
|
@@ -8255,7 +8646,7 @@ async function runProbeStage(input) {
|
|
|
8255
8646
|
});
|
|
8256
8647
|
fileServer = await createFileServer({
|
|
8257
8648
|
projectDir,
|
|
8258
|
-
compiledDir:
|
|
8649
|
+
compiledDir: join17(workDir, "compiled"),
|
|
8259
8650
|
port: 0,
|
|
8260
8651
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
8261
8652
|
fps: job.config.fps
|
|
@@ -8284,7 +8675,7 @@ async function runProbeStage(input) {
|
|
|
8284
8675
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
8285
8676
|
probeSession = await createCaptureSession4(
|
|
8286
8677
|
fileServer.url,
|
|
8287
|
-
|
|
8678
|
+
join17(workDir, "probe"),
|
|
8288
8679
|
captureOpts,
|
|
8289
8680
|
null,
|
|
8290
8681
|
probeCfg
|
|
@@ -8369,7 +8760,7 @@ async function runProbeStage(input) {
|
|
|
8369
8760
|
});
|
|
8370
8761
|
probeSession = await createCaptureSession4(
|
|
8371
8762
|
fileServer.url,
|
|
8372
|
-
|
|
8763
|
+
join17(workDir, "probe-screenshot"),
|
|
8373
8764
|
captureOpts,
|
|
8374
8765
|
null,
|
|
8375
8766
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -8405,7 +8796,7 @@ async function runProbeStage(input) {
|
|
|
8405
8796
|
compiled,
|
|
8406
8797
|
resolutions,
|
|
8407
8798
|
projectDir,
|
|
8408
|
-
|
|
8799
|
+
join17(workDir, "downloads")
|
|
8409
8800
|
);
|
|
8410
8801
|
assertNotAborted();
|
|
8411
8802
|
composition.videos = compiled.videos;
|
|
@@ -8557,7 +8948,7 @@ async function runProbeStage(input) {
|
|
|
8557
8948
|
session.page,
|
|
8558
8949
|
composition.audios.map((audio) => audio.id),
|
|
8559
8950
|
composition.duration,
|
|
8560
|
-
|
|
8951
|
+
fpsToNumber6(job.config.fps)
|
|
8561
8952
|
);
|
|
8562
8953
|
assertNotAborted();
|
|
8563
8954
|
if (automation.length > 0) {
|
|
@@ -8596,7 +8987,7 @@ async function runProbeStage(input) {
|
|
|
8596
8987
|
}
|
|
8597
8988
|
await preflightCompositionAssetMediaTypes({
|
|
8598
8989
|
projectDir,
|
|
8599
|
-
compiledDir:
|
|
8990
|
+
compiledDir: join17(workDir, "compiled"),
|
|
8600
8991
|
composition,
|
|
8601
8992
|
signal: abortSignal
|
|
8602
8993
|
});
|
|
@@ -8620,7 +9011,7 @@ async function runProbeStage(input) {
|
|
|
8620
9011
|
}
|
|
8621
9012
|
const browserProbeMs = Date.now() - probeStart;
|
|
8622
9013
|
const duration = composition.duration;
|
|
8623
|
-
const totalFrames = durationToFrameCount(duration,
|
|
9014
|
+
const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
|
|
8624
9015
|
if (duration <= 0) {
|
|
8625
9016
|
const diagnostics = [];
|
|
8626
9017
|
try {
|
|
@@ -8761,7 +9152,7 @@ function validateDistributedDuration(input) {
|
|
|
8761
9152
|
|
|
8762
9153
|
// src/services/render/stages/extractVideosStage.ts
|
|
8763
9154
|
import { existsSync as existsSync11 } from "node:fs";
|
|
8764
|
-
import { join as
|
|
9155
|
+
import { join as join18 } from "node:path";
|
|
8765
9156
|
import {
|
|
8766
9157
|
classifyVideoExtractionError,
|
|
8767
9158
|
createFrameLookupTable,
|
|
@@ -9081,14 +9472,14 @@ async function runExtractVideosStage(input) {
|
|
|
9081
9472
|
// because short boundary counts can differ by one frame.
|
|
9082
9473
|
{
|
|
9083
9474
|
fps: job.config.fps,
|
|
9084
|
-
outputDir:
|
|
9475
|
+
outputDir: join18(compiledDir, "__hyperframes_video_frames"),
|
|
9085
9476
|
format: job.config.videoFrameFormat ?? "auto",
|
|
9086
9477
|
toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
|
|
9087
9478
|
timelineEnd: composition.duration,
|
|
9088
9479
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
9089
9480
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
9090
9481
|
deferRangeExtraction,
|
|
9091
|
-
contentKeyedDirs: [
|
|
9482
|
+
contentKeyedDirs: [join18(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
9092
9483
|
urlByAbsentLocalPath
|
|
9093
9484
|
},
|
|
9094
9485
|
abortSignal,
|
|
@@ -9155,18 +9546,19 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
9155
9546
|
}
|
|
9156
9547
|
|
|
9157
9548
|
// src/services/render/stages/audioStage.ts
|
|
9158
|
-
import { join as
|
|
9159
|
-
import { rmSync as rmSync9 } from "node:fs";
|
|
9549
|
+
import { join as join20 } from "node:path";
|
|
9160
9550
|
import {
|
|
9161
9551
|
MIXED_AUDIO_FILENAME,
|
|
9162
9552
|
audioFailureDiagnostics,
|
|
9163
|
-
processCompositionAudio
|
|
9553
|
+
processCompositionAudio,
|
|
9554
|
+
removeScratch as removeScratch2,
|
|
9555
|
+
collectScratchLeftovers
|
|
9164
9556
|
} from "@hyperframes/engine";
|
|
9165
9557
|
|
|
9166
9558
|
// src/services/render/audioPadTrim.ts
|
|
9167
9559
|
import { spawn } from "node:child_process";
|
|
9168
|
-
import { mkdtempSync as mkdtempSync3
|
|
9169
|
-
import { dirname as
|
|
9560
|
+
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
9561
|
+
import { dirname as dirname8, join as join19 } from "node:path";
|
|
9170
9562
|
import {
|
|
9171
9563
|
DEFAULT_CONFIG,
|
|
9172
9564
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -9174,8 +9566,9 @@ import {
|
|
|
9174
9566
|
extractAudioMetadata as extractAudioMetadata3,
|
|
9175
9567
|
getFfprobeBinary,
|
|
9176
9568
|
ManagedChildProcess,
|
|
9177
|
-
|
|
9178
|
-
|
|
9569
|
+
removeScratch,
|
|
9570
|
+
renameWithRetry as renameWithRetry5,
|
|
9571
|
+
runFfmpeg as runFfmpeg5,
|
|
9179
9572
|
trackChildProcess
|
|
9180
9573
|
} from "@hyperframes/engine";
|
|
9181
9574
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
@@ -9185,7 +9578,7 @@ import {
|
|
|
9185
9578
|
aacSegmentCount,
|
|
9186
9579
|
describeFfmpegFailure,
|
|
9187
9580
|
extractAudioMetadata as extractAudioMetadata2,
|
|
9188
|
-
runFfmpeg as
|
|
9581
|
+
runFfmpeg as runFfmpeg4
|
|
9189
9582
|
} from "@hyperframes/engine";
|
|
9190
9583
|
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
9191
9584
|
function checkedDbfs(value) {
|
|
@@ -9209,7 +9602,7 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
9209
9602
|
else laterBlocks = Math.max(laterBlocks, block);
|
|
9210
9603
|
}
|
|
9211
9604
|
};
|
|
9212
|
-
const result = await
|
|
9605
|
+
const result = await runFfmpeg4(
|
|
9213
9606
|
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
9214
9607
|
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
9215
9608
|
);
|
|
@@ -9369,7 +9762,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9369
9762
|
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9370
9763
|
};
|
|
9371
9764
|
} finally {
|
|
9372
|
-
for (const path of plan2.cleanupPaths)
|
|
9765
|
+
for (const path of plan2.cleanupPaths) removeScratch(path);
|
|
9373
9766
|
}
|
|
9374
9767
|
let loweredDb = 0;
|
|
9375
9768
|
if (probeTruePeak) {
|
|
@@ -9416,8 +9809,8 @@ async function constrainAacTruePeak(input) {
|
|
|
9416
9809
|
async function enforceAacTruePeak(input) {
|
|
9417
9810
|
let scratchDir;
|
|
9418
9811
|
try {
|
|
9419
|
-
scratchDir = mkdtempSync3(
|
|
9420
|
-
const correctedPath =
|
|
9812
|
+
scratchDir = mkdtempSync3(join19(dirname8(input.audioPath), ".true-peak-"));
|
|
9813
|
+
const correctedPath = join19(scratchDir, "audio.m4a");
|
|
9421
9814
|
const measurements = [];
|
|
9422
9815
|
let attenuationDb = 0;
|
|
9423
9816
|
let sourcePeakDbfs;
|
|
@@ -9432,7 +9825,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9432
9825
|
}
|
|
9433
9826
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
9434
9827
|
if (measuredPath !== correctedPath) return { success: true };
|
|
9435
|
-
|
|
9828
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
9436
9829
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
9437
9830
|
}
|
|
9438
9831
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -9473,7 +9866,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9473
9866
|
)}`
|
|
9474
9867
|
};
|
|
9475
9868
|
} finally {
|
|
9476
|
-
if (scratchDir)
|
|
9869
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
9477
9870
|
}
|
|
9478
9871
|
}
|
|
9479
9872
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9550,7 +9943,7 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
9550
9943
|
};
|
|
9551
9944
|
}
|
|
9552
9945
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
9553
|
-
return toStepResult(await
|
|
9946
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
9554
9947
|
}
|
|
9555
9948
|
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
9556
9949
|
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
@@ -9618,9 +10011,17 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9618
10011
|
])
|
|
9619
10012
|
});
|
|
9620
10013
|
}
|
|
9621
|
-
|
|
10014
|
+
function runAudioStage(input) {
|
|
10015
|
+
return collectScratchLeftovers(
|
|
10016
|
+
() => runAudioStageScoped(input),
|
|
10017
|
+
(left) => {
|
|
10018
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
10019
|
+
}
|
|
10020
|
+
);
|
|
10021
|
+
}
|
|
10022
|
+
async function runAudioStageScoped(input) {
|
|
9622
10023
|
const stageStart = Date.now();
|
|
9623
|
-
const pcmSourcePath =
|
|
10024
|
+
const pcmSourcePath = join20(input.workDir, "audio-source.wav");
|
|
9624
10025
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
9625
10026
|
try {
|
|
9626
10027
|
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
@@ -9640,7 +10041,7 @@ async function runAudioStage(input) {
|
|
|
9640
10041
|
}
|
|
9641
10042
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9642
10043
|
} finally {
|
|
9643
|
-
|
|
10044
|
+
removeScratch2(pcmSourcePath);
|
|
9644
10045
|
}
|
|
9645
10046
|
}
|
|
9646
10047
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -9658,7 +10059,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9658
10059
|
assertNotAborted,
|
|
9659
10060
|
log
|
|
9660
10061
|
} = input;
|
|
9661
|
-
const audioOutputPath =
|
|
10062
|
+
const audioOutputPath = join20(workDir, MIXED_AUDIO_FILENAME);
|
|
9662
10063
|
let hasAudio = false;
|
|
9663
10064
|
let audioError;
|
|
9664
10065
|
let audioFailures;
|
|
@@ -9669,7 +10070,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9669
10070
|
audioResult = await processCompositionAudio(
|
|
9670
10071
|
audios,
|
|
9671
10072
|
projectDir,
|
|
9672
|
-
|
|
10073
|
+
join20(workDir, "audio-work"),
|
|
9673
10074
|
audioOutputPath,
|
|
9674
10075
|
duration,
|
|
9675
10076
|
abortSignal,
|
|
@@ -9738,7 +10139,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9738
10139
|
|
|
9739
10140
|
// src/services/render/stages/captureStage.ts
|
|
9740
10141
|
import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
|
|
9741
|
-
import { join as
|
|
10142
|
+
import { join as join21 } from "node:path";
|
|
9742
10143
|
import {
|
|
9743
10144
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
9744
10145
|
frameFileExtension,
|
|
@@ -9811,13 +10212,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
9811
10212
|
if (!existsSync12(workDir)) return total;
|
|
9812
10213
|
for (const attempt of readdirSync3(workDir)) {
|
|
9813
10214
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
9814
|
-
total += sampleDirectoryBytes(
|
|
10215
|
+
total += sampleDirectoryBytes(join21(workDir, attempt));
|
|
9815
10216
|
continue;
|
|
9816
10217
|
}
|
|
9817
10218
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
9818
|
-
const attemptDir =
|
|
10219
|
+
const attemptDir = join21(workDir, attempt);
|
|
9819
10220
|
for (const name of readdirSync3(attemptDir)) {
|
|
9820
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
10221
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join21(attemptDir, name));
|
|
9821
10222
|
}
|
|
9822
10223
|
}
|
|
9823
10224
|
return total;
|
|
@@ -10110,8 +10511,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
|
|
|
10110
10511
|
}
|
|
10111
10512
|
|
|
10112
10513
|
// src/services/render/stages/captureHdrStage.ts
|
|
10113
|
-
import { existsSync as existsSync14, mkdirSync as
|
|
10114
|
-
import { join as
|
|
10514
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "node:fs";
|
|
10515
|
+
import { join as join25 } from "node:path";
|
|
10115
10516
|
import {
|
|
10116
10517
|
closeCaptureSession as closeCaptureSession7,
|
|
10117
10518
|
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
@@ -10120,7 +10521,7 @@ import {
|
|
|
10120
10521
|
initializeSession as initializeSession7,
|
|
10121
10522
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
10122
10523
|
} from "@hyperframes/engine";
|
|
10123
|
-
import { fpsToNumber as
|
|
10524
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
10124
10525
|
|
|
10125
10526
|
// src/services/hdrImageTransferCache.ts
|
|
10126
10527
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -10183,7 +10584,7 @@ function createHdrImageTransferCache(options = {}) {
|
|
|
10183
10584
|
}
|
|
10184
10585
|
|
|
10185
10586
|
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
10186
|
-
import { join as
|
|
10587
|
+
import { join as join22 } from "node:path";
|
|
10187
10588
|
import {
|
|
10188
10589
|
TRANSITIONS,
|
|
10189
10590
|
crossfade
|
|
@@ -10308,7 +10709,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
10308
10709
|
);
|
|
10309
10710
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
10310
10711
|
writeFileExclusiveSync(
|
|
10311
|
-
|
|
10712
|
+
join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
10312
10713
|
normalCanvas
|
|
10313
10714
|
);
|
|
10314
10715
|
}
|
|
@@ -10340,7 +10741,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
10340
10741
|
}
|
|
10341
10742
|
|
|
10342
10743
|
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
10343
|
-
import { join as
|
|
10744
|
+
import { join as join24 } from "node:path";
|
|
10344
10745
|
import {
|
|
10345
10746
|
TRANSITIONS as TRANSITIONS2,
|
|
10346
10747
|
closeCaptureSession as closeCaptureSession6,
|
|
@@ -10354,7 +10755,7 @@ import {
|
|
|
10354
10755
|
// src/services/shaderTransitionWorkerPool.ts
|
|
10355
10756
|
import { Worker } from "node:worker_threads";
|
|
10356
10757
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
10357
|
-
import { dirname as
|
|
10758
|
+
import { dirname as dirname9, join as join23 } from "node:path";
|
|
10358
10759
|
import { createRequire } from "node:module";
|
|
10359
10760
|
import { existsSync as existsSync13 } from "node:fs";
|
|
10360
10761
|
import { cpus as cpus2 } from "node:os";
|
|
@@ -10367,10 +10768,10 @@ function resolveWorkerEntry(explicit) {
|
|
|
10367
10768
|
const isTs = override.endsWith(".ts");
|
|
10368
10769
|
return { path: override, isTs };
|
|
10369
10770
|
}
|
|
10370
|
-
const moduleDir =
|
|
10371
|
-
const jsPath =
|
|
10771
|
+
const moduleDir = dirname9(fileURLToPath3(import.meta.url));
|
|
10772
|
+
const jsPath = join23(moduleDir, "shaderTransitionWorker.js");
|
|
10372
10773
|
if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
|
|
10373
|
-
const tsPath =
|
|
10774
|
+
const tsPath = join23(moduleDir, "shaderTransitionWorker.ts");
|
|
10374
10775
|
return { path: tsPath, isTs: true };
|
|
10375
10776
|
}
|
|
10376
10777
|
function buildExecArgv(entryIsTs) {
|
|
@@ -10513,8 +10914,8 @@ async function createShaderTransitionWorkerPool(opts) {
|
|
|
10513
10914
|
if (terminated) {
|
|
10514
10915
|
throw new Error("shader-blend pool already terminated");
|
|
10515
10916
|
}
|
|
10516
|
-
return new Promise((
|
|
10517
|
-
const task = traceEnabled ? { req, resolve:
|
|
10917
|
+
return new Promise((resolve13, reject) => {
|
|
10918
|
+
const task = traceEnabled ? { req, resolve: resolve13, reject, enqueuedAtMs: Date.now(), traceId: ++nextTaskId } : { req, resolve: resolve13, reject };
|
|
10518
10919
|
const idle = slots.find((s) => !s.busy && !s.dead);
|
|
10519
10920
|
if (idle) {
|
|
10520
10921
|
queue.unshift(task);
|
|
@@ -10738,7 +11139,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
10738
11139
|
);
|
|
10739
11140
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
10740
11141
|
writeFileExclusiveSync(
|
|
10741
|
-
|
|
11142
|
+
join24(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
10742
11143
|
canvas
|
|
10743
11144
|
);
|
|
10744
11145
|
}
|
|
@@ -10794,6 +11195,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10794
11195
|
preset,
|
|
10795
11196
|
effectiveQuality,
|
|
10796
11197
|
effectiveBitrate,
|
|
11198
|
+
deliveryEncoder,
|
|
10797
11199
|
fileServer,
|
|
10798
11200
|
buildCaptureOptions,
|
|
10799
11201
|
createRenderVideoFrameInjector,
|
|
@@ -10855,7 +11257,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10855
11257
|
}
|
|
10856
11258
|
return map;
|
|
10857
11259
|
});
|
|
10858
|
-
const fpsDecimal =
|
|
11260
|
+
const fpsDecimal = fpsToNumber7(job.config.fps);
|
|
10859
11261
|
const transitionRanges = transitionMeta.map((t) => ({
|
|
10860
11262
|
...t,
|
|
10861
11263
|
startFrame: Math.floor(t.time * fpsDecimal),
|
|
@@ -10883,6 +11285,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10883
11285
|
preset: preset.preset,
|
|
10884
11286
|
quality: effectiveQuality,
|
|
10885
11287
|
bitrate: effectiveBitrate,
|
|
11288
|
+
...deliveryEncoder,
|
|
10886
11289
|
pixelFormat: preset.pixelFormat,
|
|
10887
11290
|
hdr: preset.hdr,
|
|
10888
11291
|
rawInputFormat: "rgb48le"
|
|
@@ -10926,9 +11329,9 @@ async function runCaptureHdrStage(input) {
|
|
|
10926
11329
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
10927
11330
|
}
|
|
10928
11331
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
10929
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
11332
|
+
const debugDumpDir = debugDumpEnabled ? join25(framesDir, "debug-composite") : null;
|
|
10930
11333
|
if (debugDumpDir && !existsSync14(debugDumpDir)) {
|
|
10931
|
-
|
|
11334
|
+
mkdirSync8(debugDumpDir, { recursive: true });
|
|
10932
11335
|
}
|
|
10933
11336
|
const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
|
|
10934
11337
|
const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
|
|
@@ -10942,7 +11345,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10942
11345
|
beforeCaptureHook: domSession.onBeforeCapture,
|
|
10943
11346
|
width,
|
|
10944
11347
|
height,
|
|
10945
|
-
fps:
|
|
11348
|
+
fps: fpsToNumber7(job.config.fps),
|
|
10946
11349
|
compositeTransfer,
|
|
10947
11350
|
nativeHdrImageIds,
|
|
10948
11351
|
hdrImageBuffers,
|
|
@@ -11081,24 +11484,24 @@ async function runCaptureHdrStage(input) {
|
|
|
11081
11484
|
|
|
11082
11485
|
// src/services/render/stages/encodeStage.ts
|
|
11083
11486
|
import {
|
|
11084
|
-
copyFileSync as
|
|
11487
|
+
copyFileSync as copyFileSync4,
|
|
11085
11488
|
existsSync as existsSync15,
|
|
11086
|
-
mkdirSync as
|
|
11489
|
+
mkdirSync as mkdirSync9,
|
|
11087
11490
|
readdirSync as readdirSync4,
|
|
11088
|
-
readFileSync as
|
|
11089
|
-
rmSync as
|
|
11090
|
-
statSync as
|
|
11091
|
-
writeFileSync as
|
|
11491
|
+
readFileSync as readFileSync8,
|
|
11492
|
+
rmSync as rmSync9,
|
|
11493
|
+
statSync as statSync7,
|
|
11494
|
+
writeFileSync as writeFileSync5
|
|
11092
11495
|
} from "node:fs";
|
|
11093
11496
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
11094
|
-
import { dirname as
|
|
11497
|
+
import { dirname as dirname10, join as join26 } from "node:path";
|
|
11095
11498
|
import {
|
|
11096
11499
|
encodeFramesChunkedConcat,
|
|
11097
11500
|
encodeFramesFromDir,
|
|
11098
11501
|
formatFfmpegError,
|
|
11099
11502
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
11100
11503
|
resolveConfig,
|
|
11101
|
-
runFfmpeg as
|
|
11504
|
+
runFfmpeg as runFfmpeg6,
|
|
11102
11505
|
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
11103
11506
|
} from "@hyperframes/engine";
|
|
11104
11507
|
import {
|
|
@@ -11174,17 +11577,17 @@ function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
|
11174
11577
|
}
|
|
11175
11578
|
async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
11176
11579
|
const { outputPath } = argsInput;
|
|
11177
|
-
const result = await
|
|
11580
|
+
const result = await runFfmpeg6(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
|
|
11178
11581
|
if (!result.success) {
|
|
11179
11582
|
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
11180
11583
|
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
11181
11584
|
}
|
|
11182
|
-
const gif =
|
|
11585
|
+
const gif = readFileSync8(outputPath);
|
|
11183
11586
|
if (!clearGifFramesBeforeNext(gif)) {
|
|
11184
11587
|
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
11185
11588
|
return gifEncodeFailure(startTime, outputPath, error);
|
|
11186
11589
|
}
|
|
11187
|
-
|
|
11590
|
+
writeFileSync5(outputPath, gif);
|
|
11188
11591
|
return null;
|
|
11189
11592
|
}
|
|
11190
11593
|
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
@@ -11206,17 +11609,17 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11206
11609
|
try {
|
|
11207
11610
|
const run = { signal: input.signal, timeout: input.timeout };
|
|
11208
11611
|
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
11209
|
-
const result = await
|
|
11612
|
+
const result = await runFfmpeg6(args, run);
|
|
11210
11613
|
if (!result.success) {
|
|
11211
11614
|
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
11212
11615
|
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
11213
11616
|
}
|
|
11214
11617
|
}
|
|
11215
|
-
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(
|
|
11618
|
+
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
|
|
11216
11619
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
11217
11620
|
if (failure) return failure;
|
|
11218
11621
|
}
|
|
11219
|
-
const fileSize = existsSync15(outputPath) ?
|
|
11622
|
+
const fileSize = existsSync15(outputPath) ? statSync7(outputPath).size : 0;
|
|
11220
11623
|
return {
|
|
11221
11624
|
success: true,
|
|
11222
11625
|
outputPath,
|
|
@@ -11225,7 +11628,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11225
11628
|
fileSize
|
|
11226
11629
|
};
|
|
11227
11630
|
} finally {
|
|
11228
|
-
|
|
11631
|
+
rmSync9(input.palettePath, { force: true });
|
|
11229
11632
|
}
|
|
11230
11633
|
}
|
|
11231
11634
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -11249,6 +11652,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
11249
11652
|
preset: preset.preset,
|
|
11250
11653
|
quality: input.effectiveQuality,
|
|
11251
11654
|
bitrate: input.effectiveBitrate,
|
|
11655
|
+
maxBitrate: input.maxBitrate,
|
|
11656
|
+
bufferSize: input.bufferSize,
|
|
11252
11657
|
pixelFormat: preset.pixelFormat,
|
|
11253
11658
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
11254
11659
|
useGpu: job.config.useGpu,
|
|
@@ -11297,10 +11702,10 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
11297
11702
|
const stream = await encoder;
|
|
11298
11703
|
for (let next = 0; next < totalFrames; next++) {
|
|
11299
11704
|
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
11300
|
-
await new Promise((
|
|
11705
|
+
await new Promise((resolve13) => wake = resolve13);
|
|
11301
11706
|
}
|
|
11302
11707
|
if (signal.aborted) return;
|
|
11303
|
-
const mergedPath =
|
|
11708
|
+
const mergedPath = join26(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
11304
11709
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
11305
11710
|
landed.delete(next);
|
|
11306
11711
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -11375,7 +11780,7 @@ async function runEncodeStage(input) {
|
|
|
11375
11780
|
const stage5Start = Date.now();
|
|
11376
11781
|
if (isPngSequence) {
|
|
11377
11782
|
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
11378
|
-
if (!existsSync15(outputPath))
|
|
11783
|
+
if (!existsSync15(outputPath)) mkdirSync9(outputPath, { recursive: true });
|
|
11379
11784
|
const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
11380
11785
|
if (captured.length === 0) {
|
|
11381
11786
|
throw new Error(
|
|
@@ -11383,11 +11788,11 @@ async function runEncodeStage(input) {
|
|
|
11383
11788
|
);
|
|
11384
11789
|
}
|
|
11385
11790
|
captured.forEach((name, i) => {
|
|
11386
|
-
const dst =
|
|
11387
|
-
|
|
11791
|
+
const dst = join26(outputPath, formatExportFrameName(i, "png"));
|
|
11792
|
+
copyFileSync4(join26(framesDir, name), dst);
|
|
11388
11793
|
});
|
|
11389
11794
|
if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
|
|
11390
|
-
|
|
11795
|
+
copyFileSync4(audioOutputPath, join26(outputPath, MIXED_AUDIO_FILENAME2));
|
|
11391
11796
|
log.info(
|
|
11392
11797
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
11393
11798
|
);
|
|
@@ -11407,7 +11812,7 @@ async function runEncodeStage(input) {
|
|
|
11407
11812
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
11408
11813
|
fps: job.config.fps,
|
|
11409
11814
|
loop,
|
|
11410
|
-
palettePath:
|
|
11815
|
+
palettePath: join26(dirname10(videoOnlyPath), "gif-palette.png"),
|
|
11411
11816
|
preserveAlpha: needsAlpha,
|
|
11412
11817
|
signal: abortSignal,
|
|
11413
11818
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -11617,9 +12022,9 @@ function sampleDirectoryBytes(dir) {
|
|
|
11617
12022
|
continue;
|
|
11618
12023
|
}
|
|
11619
12024
|
for (const name of entries) {
|
|
11620
|
-
const full =
|
|
12025
|
+
const full = join27(current, name);
|
|
11621
12026
|
try {
|
|
11622
|
-
const st =
|
|
12027
|
+
const st = statSync8(full);
|
|
11623
12028
|
if (st.isDirectory()) {
|
|
11624
12029
|
stack.push(full);
|
|
11625
12030
|
} else if (st.isFile()) {
|
|
@@ -11766,8 +12171,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11766
12171
|
const ranges = [];
|
|
11767
12172
|
let rangeStart = null;
|
|
11768
12173
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11769
|
-
const framePath =
|
|
11770
|
-
const missing = !existsSync16(framePath) ||
|
|
12174
|
+
const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
12175
|
+
const missing = !existsSync16(framePath) || statSync8(framePath).size <= 8;
|
|
11771
12176
|
if (missing && rangeStart === null) {
|
|
11772
12177
|
rangeStart = frameIndex;
|
|
11773
12178
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -11783,7 +12188,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11783
12188
|
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
11784
12189
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
11785
12190
|
const batches = [];
|
|
11786
|
-
const outputDirFor = (batchIndex, workerId) =>
|
|
12191
|
+
const outputDirFor = (batchIndex, workerId) => join27(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
11787
12192
|
if (stride > 1) {
|
|
11788
12193
|
const spans = /* @__PURE__ */ new Map();
|
|
11789
12194
|
for (const range of ranges) {
|
|
@@ -11843,8 +12248,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11843
12248
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11844
12249
|
}
|
|
11845
12250
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11846
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
11847
|
-
return
|
|
12251
|
+
if (platform2 === "win32" || platform2 === "darwin") return join27(systemTempDir, "hf-render-");
|
|
12252
|
+
return join27(dirname11(outputPath), `work-${jobId}-`);
|
|
11848
12253
|
}
|
|
11849
12254
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
11850
12255
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -11886,7 +12291,7 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
11886
12291
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
11887
12292
|
let captured = 0;
|
|
11888
12293
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11889
|
-
const framePath =
|
|
12294
|
+
const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11890
12295
|
if (existsSync16(framePath)) captured++;
|
|
11891
12296
|
}
|
|
11892
12297
|
return captured;
|
|
@@ -11912,7 +12317,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11912
12317
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
11913
12318
|
});
|
|
11914
12319
|
pendingTransientRetry = false;
|
|
11915
|
-
const attemptWorkDir =
|
|
12320
|
+
const attemptWorkDir = join27(options.workDir, `capture-attempt-${attempt}`);
|
|
11916
12321
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
11917
12322
|
missingRanges,
|
|
11918
12323
|
currentWorkers,
|
|
@@ -12518,18 +12923,19 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
12518
12923
|
};
|
|
12519
12924
|
}
|
|
12520
12925
|
function resolveRenderDebugDir() {
|
|
12521
|
-
const producerRoot = process.env.PRODUCER_RENDERS_DIR ?
|
|
12522
|
-
return
|
|
12926
|
+
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname11(fileURLToPath4(import.meta.url)), "../..");
|
|
12927
|
+
return join27(producerRoot, ".debug");
|
|
12523
12928
|
}
|
|
12524
12929
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12525
12930
|
validateHlsRenderConfig(job.config);
|
|
12931
|
+
validateDeliveryEncoderConfig(job.config);
|
|
12526
12932
|
const debugDir = resolveRenderDebugDir();
|
|
12527
|
-
const outputDir =
|
|
12528
|
-
if (!existsSync16(outputDir))
|
|
12529
|
-
const workDir = job.config.debug ?
|
|
12933
|
+
const outputDir = dirname11(outputPath);
|
|
12934
|
+
if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
|
|
12935
|
+
const workDir = job.config.debug ? join27(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
12530
12936
|
const pipelineStart = Date.now();
|
|
12531
12937
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
12532
|
-
const logPath = job.config.debug ?
|
|
12938
|
+
const logPath = job.config.debug ? join27(workDir, "render.log") : null;
|
|
12533
12939
|
const execution = new RenderExecutionContext({
|
|
12534
12940
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
12535
12941
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -12543,7 +12949,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12543
12949
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12544
12950
|
return;
|
|
12545
12951
|
}
|
|
12546
|
-
|
|
12952
|
+
rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12547
12953
|
});
|
|
12548
12954
|
try {
|
|
12549
12955
|
await executeRenderPipeline({
|
|
@@ -12589,7 +12995,7 @@ async function executeRenderPipeline(input) {
|
|
|
12589
12995
|
imageDecodeFailures: 0
|
|
12590
12996
|
};
|
|
12591
12997
|
let hdrPerf;
|
|
12592
|
-
const perfOutputPath =
|
|
12998
|
+
const perfOutputPath = join27(workDir, "perf-summary.json");
|
|
12593
12999
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
12594
13000
|
const observability = new RenderObservabilityRecorder({
|
|
12595
13001
|
pipelineStartMs: pipelineStart,
|
|
@@ -12601,11 +13007,14 @@ async function executeRenderPipeline(input) {
|
|
|
12601
13007
|
const isGif = outputFormat === "gif";
|
|
12602
13008
|
const isHls = outputFormat === "hls";
|
|
12603
13009
|
const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
|
|
12604
|
-
const
|
|
12605
|
-
outputFormat,
|
|
12606
|
-
job.config.fps,
|
|
12607
|
-
job.config.hlsSegmentSeconds
|
|
12608
|
-
|
|
13010
|
+
const deliveryEncoder = resolveDeliveryEncoderOptions({
|
|
13011
|
+
format: outputFormat,
|
|
13012
|
+
fps: job.config.fps,
|
|
13013
|
+
hlsSegmentSeconds: job.config.hlsSegmentSeconds,
|
|
13014
|
+
keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
|
|
13015
|
+
maxBitrate: job.config.maxBitrate,
|
|
13016
|
+
bufferSize: job.config.bufferSize
|
|
13017
|
+
});
|
|
12609
13018
|
const artifactTransaction = new ArtifactTransaction(
|
|
12610
13019
|
outputPath,
|
|
12611
13020
|
isPngSequence || isHls ? "directory" : "file",
|
|
@@ -12662,7 +13071,7 @@ async function executeRenderPipeline(input) {
|
|
|
12662
13071
|
job.startedAt = /* @__PURE__ */ new Date();
|
|
12663
13072
|
assertNotAborted();
|
|
12664
13073
|
assertConfiguredFfmpegBinariesExist();
|
|
12665
|
-
if (!existsSync16(workDir))
|
|
13074
|
+
if (!existsSync16(workDir)) mkdirSync10(workDir, { recursive: true });
|
|
12666
13075
|
if (job.config.debug) {
|
|
12667
13076
|
log.info("[Render] Debug artifacts enabled", { workDir, logPath });
|
|
12668
13077
|
}
|
|
@@ -12691,22 +13100,22 @@ async function executeRenderPipeline(input) {
|
|
|
12691
13100
|
requestedWorkers: job.config.workers ?? "auto"
|
|
12692
13101
|
});
|
|
12693
13102
|
const entryFile = job.config.entryFile || "index.html";
|
|
12694
|
-
let htmlPath =
|
|
13103
|
+
let htmlPath = join27(projectDir, entryFile);
|
|
12695
13104
|
if (!existsSync16(htmlPath)) {
|
|
12696
13105
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
12697
13106
|
}
|
|
12698
13107
|
assertNotAborted();
|
|
12699
|
-
const rawEntry =
|
|
13108
|
+
const rawEntry = readFileSync9(htmlPath, "utf-8");
|
|
12700
13109
|
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
12701
|
-
const wrapperPath =
|
|
12702
|
-
const projectIndexPath =
|
|
13110
|
+
const wrapperPath = join27(workDir, "standalone-entry.html");
|
|
13111
|
+
const projectIndexPath = join27(projectDir, "index.html");
|
|
12703
13112
|
if (!existsSync16(projectIndexPath)) {
|
|
12704
13113
|
throw new Error(
|
|
12705
13114
|
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
12706
13115
|
);
|
|
12707
13116
|
}
|
|
12708
13117
|
const standaloneHtml = extractStandaloneEntryFromIndex(
|
|
12709
|
-
|
|
13118
|
+
readFileSync9(projectIndexPath, "utf-8"),
|
|
12710
13119
|
entryFile,
|
|
12711
13120
|
rawEntry
|
|
12712
13121
|
);
|
|
@@ -12715,7 +13124,7 @@ async function executeRenderPipeline(input) {
|
|
|
12715
13124
|
`Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
|
|
12716
13125
|
);
|
|
12717
13126
|
}
|
|
12718
|
-
|
|
13127
|
+
writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
|
|
12719
13128
|
htmlPath = wrapperPath;
|
|
12720
13129
|
log.info("Extracted standalone entry from index.html host context", {
|
|
12721
13130
|
entryFile
|
|
@@ -12845,7 +13254,7 @@ async function executeRenderPipeline(input) {
|
|
|
12845
13254
|
validateRenderDuration({
|
|
12846
13255
|
duration: probeResult.duration,
|
|
12847
13256
|
totalFrames,
|
|
12848
|
-
fps:
|
|
13257
|
+
fps: fpsToNumber8(job.config.fps)
|
|
12849
13258
|
});
|
|
12850
13259
|
perfStages.browserProbeMs = probeResult.browserProbeMs;
|
|
12851
13260
|
perfStages.compileMs = Date.now() - stage1Start;
|
|
@@ -12862,7 +13271,7 @@ async function executeRenderPipeline(input) {
|
|
|
12862
13271
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
12863
13272
|
code: "extract_video"
|
|
12864
13273
|
});
|
|
12865
|
-
const compiledDir =
|
|
13274
|
+
const compiledDir = join27(workDir, "compiled");
|
|
12866
13275
|
const extractResult = await observeRenderStage(
|
|
12867
13276
|
observability,
|
|
12868
13277
|
"video_extract",
|
|
@@ -12996,7 +13405,7 @@ async function executeRenderPipeline(input) {
|
|
|
12996
13405
|
try {
|
|
12997
13406
|
fileServer = await createFileServer({
|
|
12998
13407
|
projectDir,
|
|
12999
|
-
compiledDir:
|
|
13408
|
+
compiledDir: join27(workDir, "compiled"),
|
|
13000
13409
|
port: 0,
|
|
13001
13410
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
13002
13411
|
fps: job.config.fps
|
|
@@ -13014,8 +13423,8 @@ async function executeRenderPipeline(input) {
|
|
|
13014
13423
|
if (!activeFileServer) {
|
|
13015
13424
|
throw new Error("File server failed to initialize before frame capture");
|
|
13016
13425
|
}
|
|
13017
|
-
const framesDir =
|
|
13018
|
-
if (!existsSync16(framesDir))
|
|
13426
|
+
const framesDir = join27(workDir, "captured-frames");
|
|
13427
|
+
if (!existsSync16(framesDir)) mkdirSync10(framesDir, { recursive: true });
|
|
13019
13428
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
13020
13429
|
code: "start_browsers"
|
|
13021
13430
|
});
|
|
@@ -13486,7 +13895,7 @@ async function executeRenderPipeline(input) {
|
|
|
13486
13895
|
hls: ".mp4"
|
|
13487
13896
|
};
|
|
13488
13897
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
13489
|
-
const videoOnlyPath =
|
|
13898
|
+
const videoOnlyPath = join27(workDir, `video-only${videoExt}`);
|
|
13490
13899
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
13491
13900
|
if (usePageSideCompositingForTransitions) {
|
|
13492
13901
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -13688,6 +14097,7 @@ async function executeRenderPipeline(input) {
|
|
|
13688
14097
|
preset,
|
|
13689
14098
|
effectiveQuality,
|
|
13690
14099
|
effectiveBitrate,
|
|
14100
|
+
deliveryEncoder,
|
|
13691
14101
|
fileServer: activeFileServer,
|
|
13692
14102
|
buildCaptureOptions,
|
|
13693
14103
|
createRenderVideoFrameInjector,
|
|
@@ -13712,9 +14122,7 @@ async function executeRenderPipeline(input) {
|
|
|
13712
14122
|
} else {
|
|
13713
14123
|
let streamingHandled = false;
|
|
13714
14124
|
if (capturePlan.kind === "sdr_segmented") {
|
|
13715
|
-
const segmentedPlan = capturePlan;
|
|
13716
14125
|
const captureFrameStart = Date.now();
|
|
13717
|
-
resetCaptureAttemptProgress(job);
|
|
13718
14126
|
const segmentFrames = resolveSegmentFrames(process.env);
|
|
13719
14127
|
const segmentImageFormat = captureOptions.format || "jpeg";
|
|
13720
14128
|
const segmentPlanHash = computeSegmentPlanHash({
|
|
@@ -13729,6 +14137,9 @@ async function executeRenderPipeline(input) {
|
|
|
13729
14137
|
preset: preset.preset,
|
|
13730
14138
|
quality: effectiveQuality,
|
|
13731
14139
|
bitrate: effectiveBitrate,
|
|
14140
|
+
maxBitrate: deliveryEncoder.maxBitrate,
|
|
14141
|
+
bufferSize: deliveryEncoder.bufferSize,
|
|
14142
|
+
gopSize: deliveryEncoder.gopSize,
|
|
13732
14143
|
pixelFormat: preset.pixelFormat,
|
|
13733
14144
|
imageFormat: segmentImageFormat,
|
|
13734
14145
|
useGpu: job.config.useGpu === true,
|
|
@@ -13739,7 +14150,7 @@ async function executeRenderPipeline(input) {
|
|
|
13739
14150
|
pageMotionBlur: job.config.pageMotionBlur,
|
|
13740
14151
|
captureQuality: captureOptions.quality
|
|
13741
14152
|
});
|
|
13742
|
-
const segmentDir = segmentDirFor(
|
|
14153
|
+
const segmentDir = segmentDirFor(join27(projectDir, "renders"), segmentPlanHash);
|
|
13743
14154
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
13744
14155
|
if (job.config.resumeSegments === true) {
|
|
13745
14156
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -13754,71 +14165,129 @@ async function executeRenderPipeline(input) {
|
|
|
13754
14165
|
});
|
|
13755
14166
|
}
|
|
13756
14167
|
if (completedSegments.size === 0) {
|
|
13757
|
-
|
|
14168
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13758
14169
|
}
|
|
13759
14170
|
} else {
|
|
13760
|
-
|
|
14171
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13761
14172
|
}
|
|
13762
|
-
const
|
|
14173
|
+
const emptySegmentManifest = () => ({
|
|
13763
14174
|
version: 1,
|
|
13764
14175
|
planHash: segmentPlanHash,
|
|
13765
14176
|
totalFrames,
|
|
13766
14177
|
segmentFrames,
|
|
13767
14178
|
completed: []
|
|
14179
|
+
});
|
|
14180
|
+
let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
|
|
14181
|
+
const invokeSegmented = () => {
|
|
14182
|
+
if (capturePlan.kind !== "sdr_segmented") {
|
|
14183
|
+
throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
|
|
14184
|
+
}
|
|
14185
|
+
const segmentedPlan = capturePlan;
|
|
14186
|
+
resetCaptureAttemptProgress(job);
|
|
14187
|
+
return observeRenderStage(
|
|
14188
|
+
observability,
|
|
14189
|
+
"capture_segmented",
|
|
14190
|
+
captureStageObservationData(),
|
|
14191
|
+
() => runCaptureSegmentedStage({
|
|
14192
|
+
fileServer: activeFileServer,
|
|
14193
|
+
workDir,
|
|
14194
|
+
framesDir,
|
|
14195
|
+
videoOnlyPath,
|
|
14196
|
+
job,
|
|
14197
|
+
totalFrames,
|
|
14198
|
+
cfg,
|
|
14199
|
+
plan: segmentedPlan,
|
|
14200
|
+
log,
|
|
14201
|
+
probeSession,
|
|
14202
|
+
outputFormat,
|
|
14203
|
+
streamingEncoderOptions: {
|
|
14204
|
+
fps: job.config.fps,
|
|
14205
|
+
width: captureCompositionWidth ?? width,
|
|
14206
|
+
height: captureCompositionHeight ?? height,
|
|
14207
|
+
codec: preset.codec,
|
|
14208
|
+
preset: preset.preset,
|
|
14209
|
+
quality: effectiveQuality,
|
|
14210
|
+
bitrate: effectiveBitrate,
|
|
14211
|
+
pixelFormat: preset.pixelFormat,
|
|
14212
|
+
vp9CpuUsed: cfg.vp9CpuUsed,
|
|
14213
|
+
useGpu: job.config.useGpu,
|
|
14214
|
+
imageFormat: segmentImageFormat,
|
|
14215
|
+
hdr: preset.hdr,
|
|
14216
|
+
// Each segment sets its own GOP (at most the interval); hls never reaches this route.
|
|
14217
|
+
...deliveryEncoder
|
|
14218
|
+
},
|
|
14219
|
+
buildCaptureOptions,
|
|
14220
|
+
createRenderVideoFrameInjector,
|
|
14221
|
+
abortSignal: executionSignal,
|
|
14222
|
+
assertNotAborted,
|
|
14223
|
+
onProgress,
|
|
14224
|
+
dedupPerfs,
|
|
14225
|
+
segmentFrames,
|
|
14226
|
+
segmentDir,
|
|
14227
|
+
workerCount: segmentedPlan.workerCount,
|
|
14228
|
+
browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
|
|
14229
|
+
completedSegments,
|
|
14230
|
+
onSegmentComplete: (entry) => {
|
|
14231
|
+
segmentManifest.completed = [
|
|
14232
|
+
...segmentManifest.completed.filter((e) => e.index !== entry.index),
|
|
14233
|
+
{ ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
14234
|
+
];
|
|
14235
|
+
writeSegmentManifest(segmentDir, segmentManifest);
|
|
14236
|
+
},
|
|
14237
|
+
updateCaptureObservability
|
|
14238
|
+
})
|
|
14239
|
+
);
|
|
13768
14240
|
};
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
13791
|
-
|
|
13792
|
-
|
|
13793
|
-
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
13800
|
-
|
|
13801
|
-
|
|
13802
|
-
|
|
13803
|
-
|
|
13804
|
-
|
|
13805
|
-
|
|
13806
|
-
|
|
13807
|
-
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
updateCaptureObservability
|
|
13820
|
-
})
|
|
13821
|
-
);
|
|
14241
|
+
let segmentedRes;
|
|
14242
|
+
try {
|
|
14243
|
+
segmentedRes = await invokeSegmented();
|
|
14244
|
+
} catch (err) {
|
|
14245
|
+
const isVerifyError = isDrawElementVerificationError(err);
|
|
14246
|
+
const isDeStall = isDeRendererStallError(err);
|
|
14247
|
+
if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
14248
|
+
throw err;
|
|
14249
|
+
}
|
|
14250
|
+
deSelfVerifyFallback = isVerifyError;
|
|
14251
|
+
const t = deVerifyFallbackTelemetry(err);
|
|
14252
|
+
deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
|
|
14253
|
+
deFallbackFailedDb = t.failedDb;
|
|
14254
|
+
deFallbackFrameIndex = t.frameIndex;
|
|
14255
|
+
deFallbackThresholdDb = t.thresholdDb;
|
|
14256
|
+
log.warn(
|
|
14257
|
+
"[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
|
|
14258
|
+
{
|
|
14259
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14260
|
+
}
|
|
14261
|
+
);
|
|
14262
|
+
observability.checkpoint(
|
|
14263
|
+
"capture_segmented",
|
|
14264
|
+
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14265
|
+
);
|
|
14266
|
+
if (probeSession) {
|
|
14267
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
14268
|
+
const orphaned = probeSession;
|
|
14269
|
+
probeSession = null;
|
|
14270
|
+
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14271
|
+
}
|
|
14272
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14273
|
+
completedSegments = /* @__PURE__ */ new Set();
|
|
14274
|
+
segmentManifest = emptySegmentManifest();
|
|
14275
|
+
dedupPerfs.length = 0;
|
|
14276
|
+
capturePlan = replanAfterFailure(capturePlan, {
|
|
14277
|
+
kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
|
|
14278
|
+
});
|
|
14279
|
+
syncCapturePlan();
|
|
14280
|
+
updateCaptureObservability({
|
|
14281
|
+
forceScreenshot: capturePlan.forceScreenshot,
|
|
14282
|
+
deSelfVerifyFallback,
|
|
14283
|
+
deFallbackReason,
|
|
14284
|
+
deFallbackFailedDb,
|
|
14285
|
+
deFallbackFrameIndex,
|
|
14286
|
+
deFallbackThresholdDb
|
|
14287
|
+
});
|
|
14288
|
+
segmentedRes = await invokeSegmented();
|
|
14289
|
+
observability.clearFailure("capture_segmented");
|
|
14290
|
+
}
|
|
13822
14291
|
if (segmentedRes.success) {
|
|
13823
14292
|
streamingHandled = true;
|
|
13824
14293
|
workerCount = segmentedRes.workerCount;
|
|
@@ -13838,7 +14307,7 @@ async function executeRenderPipeline(input) {
|
|
|
13838
14307
|
);
|
|
13839
14308
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
13840
14309
|
if (job.config.keepSegments !== true) {
|
|
13841
|
-
|
|
14310
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13842
14311
|
}
|
|
13843
14312
|
} else {
|
|
13844
14313
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -13891,7 +14360,7 @@ async function executeRenderPipeline(input) {
|
|
|
13891
14360
|
// assemble stage's `-c copy` segmentation cuts exactly on
|
|
13892
14361
|
// the segment boundary. Every other format leaves the
|
|
13893
14362
|
// encoder's open-GOP output untouched.
|
|
13894
|
-
...
|
|
14363
|
+
...deliveryEncoder
|
|
13895
14364
|
},
|
|
13896
14365
|
buildCaptureOptions,
|
|
13897
14366
|
createRenderVideoFrameInjector,
|
|
@@ -14054,7 +14523,7 @@ async function executeRenderPipeline(input) {
|
|
|
14054
14523
|
engineConfig: cfg,
|
|
14055
14524
|
// Same value the streaming encoder above spreads — the disk and
|
|
14056
14525
|
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
14057
|
-
...
|
|
14526
|
+
...deliveryEncoder,
|
|
14058
14527
|
abortSignal: executionSignal,
|
|
14059
14528
|
assertNotAborted,
|
|
14060
14529
|
onProgress
|
|
@@ -14110,8 +14579,8 @@ async function executeRenderPipeline(input) {
|
|
|
14110
14579
|
"capture_disk",
|
|
14111
14580
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14112
14581
|
);
|
|
14113
|
-
|
|
14114
|
-
|
|
14582
|
+
rmSync10(framesDir, { recursive: true, force: true });
|
|
14583
|
+
mkdirSync10(framesDir, { recursive: true });
|
|
14115
14584
|
resetCaptureAttemptProgress(job);
|
|
14116
14585
|
dedupPerfs.length = 0;
|
|
14117
14586
|
cfg.useDrawElement = false;
|
|
@@ -14208,7 +14677,7 @@ async function executeRenderPipeline(input) {
|
|
|
14208
14677
|
buildArtifactExpectation({
|
|
14209
14678
|
outputFormat,
|
|
14210
14679
|
durationSeconds: job.duration,
|
|
14211
|
-
fps:
|
|
14680
|
+
fps: fpsToNumber8(job.config.fps),
|
|
14212
14681
|
expectedFrames: captureTotalFrames,
|
|
14213
14682
|
width: outputWidth,
|
|
14214
14683
|
height: outputHeight
|
|
@@ -14287,7 +14756,7 @@ async function executeRenderPipeline(input) {
|
|
|
14287
14756
|
job.perfSummary = perfSummary;
|
|
14288
14757
|
if (job.config.debug) {
|
|
14289
14758
|
try {
|
|
14290
|
-
|
|
14759
|
+
writeFileSync6(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
|
|
14291
14760
|
} catch (err) {
|
|
14292
14761
|
log.debug("Failed to write perf summary", {
|
|
14293
14762
|
perfOutputPath,
|
|
@@ -14297,8 +14766,8 @@ async function executeRenderPipeline(input) {
|
|
|
14297
14766
|
}
|
|
14298
14767
|
if (job.config.debug) {
|
|
14299
14768
|
if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
|
|
14300
|
-
const debugOutput =
|
|
14301
|
-
|
|
14769
|
+
const debugOutput = join27(workDir, `output${videoExt}`);
|
|
14770
|
+
copyFileSync5(stagedOutputPath, debugOutput);
|
|
14302
14771
|
}
|
|
14303
14772
|
}
|
|
14304
14773
|
await commitArtifactTransaction(artifactTransaction, () => {
|
|
@@ -14422,30 +14891,31 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
|
14422
14891
|
// src/services/distributed/plan.ts
|
|
14423
14892
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
14424
14893
|
import {
|
|
14425
|
-
copyFileSync as
|
|
14894
|
+
copyFileSync as copyFileSync6,
|
|
14426
14895
|
cpSync as cpSync2,
|
|
14427
14896
|
existsSync as existsSync21,
|
|
14428
14897
|
linkSync,
|
|
14429
|
-
mkdirSync as
|
|
14430
|
-
rmSync as
|
|
14431
|
-
writeFileSync as
|
|
14898
|
+
mkdirSync as mkdirSync14,
|
|
14899
|
+
rmSync as rmSync12,
|
|
14900
|
+
writeFileSync as writeFileSync9
|
|
14432
14901
|
} from "node:fs";
|
|
14433
|
-
import { extname as extname6, isAbsolute as isAbsolute3, join as
|
|
14902
|
+
import { extname as extname6, isAbsolute as isAbsolute3, join as join35, relative as relative5, sep as sep2 } from "node:path";
|
|
14434
14903
|
import {
|
|
14435
|
-
fpsToNumber as
|
|
14904
|
+
fpsToNumber as fpsToNumber9,
|
|
14436
14905
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
14437
14906
|
} from "@hyperframes/core";
|
|
14438
14907
|
import {
|
|
14439
14908
|
getEncoderPreset as getEncoderPreset4,
|
|
14909
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
14440
14910
|
normalizeVp9CpuUsed,
|
|
14441
14911
|
resolveConfig as resolveConfig3,
|
|
14442
14912
|
declareContentAddressedUrl,
|
|
14443
|
-
renameWithRetry as
|
|
14913
|
+
renameWithRetry as renameWithRetry7,
|
|
14444
14914
|
declareProbeFactsUrl
|
|
14445
14915
|
} from "@hyperframes/engine";
|
|
14446
14916
|
|
|
14447
14917
|
// src/services/distributed/planFrameCost.ts
|
|
14448
|
-
import { join as
|
|
14918
|
+
import { join as join30 } from "node:path";
|
|
14449
14919
|
import {
|
|
14450
14920
|
closeCaptureSession as closeCaptureSession9,
|
|
14451
14921
|
createCaptureSession as createCaptureSession9,
|
|
@@ -14455,7 +14925,7 @@ import {
|
|
|
14455
14925
|
|
|
14456
14926
|
// src/services/distributed/chunkFrames.ts
|
|
14457
14927
|
import { existsSync as existsSync18, readdirSync as readdirSync6 } from "node:fs";
|
|
14458
|
-
import { extname as extname4, join as
|
|
14928
|
+
import { extname as extname4, join as join29 } from "node:path";
|
|
14459
14929
|
import {
|
|
14460
14930
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
14461
14931
|
createFrameLookupTable as createFrameLookupTable2,
|
|
@@ -14465,8 +14935,8 @@ import {
|
|
|
14465
14935
|
|
|
14466
14936
|
// src/services/distributed/shared.ts
|
|
14467
14937
|
import { execFile as execFileCallback } from "node:child_process";
|
|
14468
|
-
import { dirname as
|
|
14469
|
-
import { existsSync as existsSync17, readFileSync as
|
|
14938
|
+
import { dirname as dirname12, join as join28 } from "node:path";
|
|
14939
|
+
import { existsSync as existsSync17, readFileSync as readFileSync10 } from "node:fs";
|
|
14470
14940
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
14471
14941
|
import { promisify as promisify2 } from "node:util";
|
|
14472
14942
|
import {
|
|
@@ -14481,7 +14951,7 @@ function isPlanAudioArtifactPath(path) {
|
|
|
14481
14951
|
}
|
|
14482
14952
|
function resolvePlanAudioPath(planDir) {
|
|
14483
14953
|
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
14484
|
-
const candidate =
|
|
14954
|
+
const candidate = join28(planDir, name);
|
|
14485
14955
|
if (existsSync17(candidate)) return candidate;
|
|
14486
14956
|
}
|
|
14487
14957
|
return null;
|
|
@@ -14738,13 +15208,13 @@ function buildSyntheticRenderJob(input) {
|
|
|
14738
15208
|
var cachedProducerVersion = null;
|
|
14739
15209
|
function readProducerVersion() {
|
|
14740
15210
|
if (cachedProducerVersion !== null) return cachedProducerVersion;
|
|
14741
|
-
const startDir =
|
|
15211
|
+
const startDir = dirname12(fileURLToPath5(import.meta.url));
|
|
14742
15212
|
let current = startDir;
|
|
14743
15213
|
for (let i = 0; i < 10; i++) {
|
|
14744
|
-
const candidate =
|
|
15214
|
+
const candidate = join28(current, "package.json");
|
|
14745
15215
|
if (existsSync17(candidate)) {
|
|
14746
15216
|
try {
|
|
14747
|
-
const pkg = JSON.parse(
|
|
15217
|
+
const pkg = JSON.parse(readFileSync10(candidate, "utf-8"));
|
|
14748
15218
|
if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
|
|
14749
15219
|
cachedProducerVersion = pkg.version;
|
|
14750
15220
|
return pkg.version;
|
|
@@ -14752,7 +15222,7 @@ function readProducerVersion() {
|
|
|
14752
15222
|
} catch {
|
|
14753
15223
|
}
|
|
14754
15224
|
}
|
|
14755
|
-
const parent =
|
|
15225
|
+
const parent = dirname12(current);
|
|
14756
15226
|
if (parent === current) break;
|
|
14757
15227
|
current = parent;
|
|
14758
15228
|
}
|
|
@@ -14795,7 +15265,7 @@ function frameNumberFromFileName(name) {
|
|
|
14795
15265
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
14796
15266
|
const result = [];
|
|
14797
15267
|
for (const v of videos) {
|
|
14798
|
-
const outputDir =
|
|
15268
|
+
const outputDir = join29(planDir, "video-frames", v.videoId);
|
|
14799
15269
|
if (!existsSync18(outputDir)) {
|
|
14800
15270
|
throw new Error(
|
|
14801
15271
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
@@ -14814,7 +15284,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
14814
15284
|
if (!frameName) continue;
|
|
14815
15285
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
14816
15286
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
14817
|
-
framePaths.set(frameIndex,
|
|
15287
|
+
framePaths.set(frameIndex, join29(outputDir, frameName));
|
|
14818
15288
|
}
|
|
14819
15289
|
result.push({
|
|
14820
15290
|
videoId: v.videoId,
|
|
@@ -14904,14 +15374,14 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14904
15374
|
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
14905
15375
|
return Promise.all(
|
|
14906
15376
|
deferred.map(async (video) => {
|
|
14907
|
-
const outputDir =
|
|
15377
|
+
const outputDir = join29(input.outputRoot, video.videoId);
|
|
14908
15378
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
14909
15379
|
if (indexes.length === 0) {
|
|
14910
15380
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
14911
15381
|
}
|
|
14912
15382
|
const range = video.deferredRange;
|
|
14913
15383
|
const remoteUrl = remoteSources[range.sourcePath];
|
|
14914
|
-
const sourcePath = remoteUrl ??
|
|
15384
|
+
const sourcePath = remoteUrl ?? join29(input.planDir, ...range.sourcePath.split("/"));
|
|
14915
15385
|
if (remoteUrl === void 0 && !existsSync18(sourcePath)) {
|
|
14916
15386
|
throw new RenderChunkValidationError(
|
|
14917
15387
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -14960,12 +15430,12 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14960
15430
|
planVideos,
|
|
14961
15431
|
frames: sampled,
|
|
14962
15432
|
fps: fpsRatio,
|
|
14963
|
-
outputRoot:
|
|
15433
|
+
outputRoot: join30(workDir, "frame-cost-video-frames"),
|
|
14964
15434
|
cfg: input.cfg
|
|
14965
15435
|
})
|
|
14966
15436
|
];
|
|
14967
15437
|
const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
14968
|
-
const compiledDir =
|
|
15438
|
+
const compiledDir = join30(planDir, "compiled");
|
|
14969
15439
|
const fileServer = await createFileServer({
|
|
14970
15440
|
projectDir: compiledDir,
|
|
14971
15441
|
compiledDir,
|
|
@@ -14977,7 +15447,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14977
15447
|
try {
|
|
14978
15448
|
session = await createCaptureSession9(
|
|
14979
15449
|
fileServer.url,
|
|
14980
|
-
|
|
15450
|
+
join30(workDir, "frame-cost"),
|
|
14981
15451
|
buildChunkCaptureOptions({
|
|
14982
15452
|
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
14983
15453
|
encoder: input,
|
|
@@ -15074,8 +15544,8 @@ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
|
|
|
15074
15544
|
}
|
|
15075
15545
|
|
|
15076
15546
|
// src/services/render/stages/freezePlan.ts
|
|
15077
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
15078
|
-
import { join as
|
|
15547
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync11, readdirSync as readdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
15548
|
+
import { join as join31, relative as relative3, resolve as resolve9 } from "node:path";
|
|
15079
15549
|
|
|
15080
15550
|
// src/services/distributed/planProtocol.ts
|
|
15081
15551
|
var PLAN_SCHEMA_VERSION = 1;
|
|
@@ -15173,12 +15643,12 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
|
|
|
15173
15643
|
}
|
|
15174
15644
|
|
|
15175
15645
|
// src/services/render/stages/planHash.ts
|
|
15176
|
-
import { createHash as
|
|
15177
|
-
import { closeSync as
|
|
15646
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
15647
|
+
import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync5, readSync as readSync4 } from "node:fs";
|
|
15178
15648
|
var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
|
|
15179
15649
|
var FIELD_DELIMITER = Buffer.from([0]);
|
|
15180
15650
|
function computePlanHash(input) {
|
|
15181
|
-
const hash =
|
|
15651
|
+
const hash = createHash7("sha256");
|
|
15182
15652
|
hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
|
|
15183
15653
|
hash.update(input.compositionHtml);
|
|
15184
15654
|
hash.update(FIELD_DELIMITER);
|
|
@@ -15225,27 +15695,27 @@ function canonicalJsonStringify(value) {
|
|
|
15225
15695
|
throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
|
|
15226
15696
|
}
|
|
15227
15697
|
function sha256Hex(bytes) {
|
|
15228
|
-
const h =
|
|
15698
|
+
const h = createHash7("sha256");
|
|
15229
15699
|
h.update(bytes);
|
|
15230
15700
|
return h.digest("hex");
|
|
15231
15701
|
}
|
|
15232
15702
|
function sha256File(path) {
|
|
15233
|
-
const hash =
|
|
15703
|
+
const hash = createHash7("sha256");
|
|
15234
15704
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
15235
|
-
const fd =
|
|
15705
|
+
const fd = openSync5(path, "r");
|
|
15236
15706
|
try {
|
|
15237
15707
|
let bytesRead = 0;
|
|
15238
15708
|
do {
|
|
15239
|
-
bytesRead =
|
|
15709
|
+
bytesRead = readSync4(fd, buffer, 0, buffer.byteLength, null);
|
|
15240
15710
|
if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
|
|
15241
15711
|
} while (bytesRead > 0);
|
|
15242
15712
|
} finally {
|
|
15243
|
-
|
|
15713
|
+
closeSync5(fd);
|
|
15244
15714
|
}
|
|
15245
15715
|
return hash.digest("hex");
|
|
15246
15716
|
}
|
|
15247
15717
|
async function sha256FileAsync(path) {
|
|
15248
|
-
const hash =
|
|
15718
|
+
const hash = createHash7("sha256");
|
|
15249
15719
|
for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
|
|
15250
15720
|
hash.update(chunk);
|
|
15251
15721
|
}
|
|
@@ -15289,11 +15759,11 @@ function stripUndefined(value) {
|
|
|
15289
15759
|
}
|
|
15290
15760
|
function listPlanFiles(planDir) {
|
|
15291
15761
|
const results = [];
|
|
15292
|
-
const rootResolved =
|
|
15762
|
+
const rootResolved = resolve9(planDir);
|
|
15293
15763
|
function walk(dir) {
|
|
15294
15764
|
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
15295
15765
|
for (const entry of entries) {
|
|
15296
|
-
const full =
|
|
15766
|
+
const full = join31(dir, entry.name);
|
|
15297
15767
|
if (entry.isDirectory()) {
|
|
15298
15768
|
walk(full);
|
|
15299
15769
|
} else if (entry.isFile()) {
|
|
@@ -15314,7 +15784,7 @@ function collectPlanAssetShas(planDir, digests) {
|
|
|
15314
15784
|
const assets = [];
|
|
15315
15785
|
for (const file of files) {
|
|
15316
15786
|
if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
|
|
15317
|
-
compositionHtml =
|
|
15787
|
+
compositionHtml = readFileSync11(file.absolutePath);
|
|
15318
15788
|
continue;
|
|
15319
15789
|
}
|
|
15320
15790
|
if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
|
|
@@ -15334,23 +15804,23 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
|
15334
15804
|
function writePlanRemoteFiles(planDir, files) {
|
|
15335
15805
|
if (files.length === 0) return;
|
|
15336
15806
|
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
15337
|
-
|
|
15807
|
+
writeFileSync7(join31(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
15338
15808
|
}
|
|
15339
15809
|
function readPlanRemoteFiles(planDir) {
|
|
15340
|
-
const path =
|
|
15341
|
-
return existsSync19(path) ? JSON.parse(
|
|
15810
|
+
const path = join31(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
15811
|
+
return existsSync19(path) ? JSON.parse(readFileSync11(path, "utf-8")) : [];
|
|
15342
15812
|
}
|
|
15343
15813
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
15344
|
-
const planJsonPath =
|
|
15345
|
-
const encoderJsonPath =
|
|
15814
|
+
const planJsonPath = join31(planDir, "plan.json");
|
|
15815
|
+
const encoderJsonPath = join31(planDir, "meta", "encoder.json");
|
|
15346
15816
|
if (!existsSync19(planJsonPath)) {
|
|
15347
15817
|
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
15348
15818
|
}
|
|
15349
15819
|
if (!existsSync19(encoderJsonPath)) {
|
|
15350
15820
|
throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
|
|
15351
15821
|
}
|
|
15352
|
-
const planJson = JSON.parse(
|
|
15353
|
-
const encoderConfigCanonicalJson =
|
|
15822
|
+
const planJson = JSON.parse(readFileSync11(planJsonPath, "utf-8"));
|
|
15823
|
+
const encoderConfigCanonicalJson = readFileSync11(encoderJsonPath, "utf-8");
|
|
15354
15824
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
15355
15825
|
return computePlanHash({
|
|
15356
15826
|
compositionHtml,
|
|
@@ -15379,18 +15849,18 @@ async function freezePlan(input) {
|
|
|
15379
15849
|
if (!existsSync19(planDir)) {
|
|
15380
15850
|
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
15381
15851
|
}
|
|
15382
|
-
const metaDir =
|
|
15383
|
-
if (!existsSync19(metaDir))
|
|
15384
|
-
|
|
15385
|
-
|
|
15852
|
+
const metaDir = join31(planDir, "meta");
|
|
15853
|
+
if (!existsSync19(metaDir)) mkdirSync11(metaDir, { recursive: true });
|
|
15854
|
+
writeFileSync7(
|
|
15855
|
+
join31(metaDir, "composition.json"),
|
|
15386
15856
|
`${JSON.stringify(composition, null, 2)}
|
|
15387
15857
|
`,
|
|
15388
15858
|
"utf-8"
|
|
15389
15859
|
);
|
|
15390
15860
|
const encoderForCanonical = stripUndefined(encoder);
|
|
15391
15861
|
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
15392
|
-
|
|
15393
|
-
|
|
15862
|
+
writeFileSync7(join31(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
15863
|
+
writeFileSync7(join31(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
15394
15864
|
`, "utf-8");
|
|
15395
15865
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
15396
15866
|
const planHash = computePlanHash({
|
|
@@ -15414,8 +15884,8 @@ async function freezePlan(input) {
|
|
|
15414
15884
|
duration: durationSeconds,
|
|
15415
15885
|
hasAudio
|
|
15416
15886
|
};
|
|
15417
|
-
const planJsonPath =
|
|
15418
|
-
|
|
15887
|
+
const planJsonPath = join31(planDir, "plan.json");
|
|
15888
|
+
writeFileSync7(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
15419
15889
|
`, "utf-8");
|
|
15420
15890
|
return { planJsonPath, planHash };
|
|
15421
15891
|
}
|
|
@@ -15456,16 +15926,16 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
15456
15926
|
}
|
|
15457
15927
|
|
|
15458
15928
|
// src/services/distributed/chunkAudio.ts
|
|
15459
|
-
import { closeSync as
|
|
15460
|
-
import { writeFileSync as
|
|
15461
|
-
import { join as
|
|
15462
|
-
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as
|
|
15929
|
+
import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync13, openSync as openSync6, readFileSync as readFileSync12 } from "node:fs";
|
|
15930
|
+
import { writeFileSync as writeFileSync8, writeSync } from "node:fs";
|
|
15931
|
+
import { join as join33 } from "node:path";
|
|
15932
|
+
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
|
|
15463
15933
|
|
|
15464
15934
|
// src/services/distributed/mp4Stitch.ts
|
|
15465
|
-
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as
|
|
15935
|
+
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync12, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
|
|
15466
15936
|
import { open } from "node:fs/promises";
|
|
15467
|
-
import { renameWithRetry as
|
|
15468
|
-
import { dirname as
|
|
15937
|
+
import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
|
|
15938
|
+
import { dirname as dirname13, join as join32 } from "node:path";
|
|
15469
15939
|
import { finished, pipeline } from "node:stream/promises";
|
|
15470
15940
|
function boxAt(src, off, at, limit) {
|
|
15471
15941
|
let size = src.readUInt32BE(off);
|
|
@@ -15478,16 +15948,16 @@ function boxAt(src, off, at, limit) {
|
|
|
15478
15948
|
return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
|
|
15479
15949
|
}
|
|
15480
15950
|
function childBoxes(buf, parent) {
|
|
15481
|
-
const
|
|
15482
|
-
for (let at = parent.bodyStart; at < parent.end; at =
|
|
15483
|
-
|
|
15951
|
+
const boxes2 = [];
|
|
15952
|
+
for (let at = parent.bodyStart; at < parent.end; at = boxes2.at(-1).end) {
|
|
15953
|
+
boxes2.push(boxAt(buf, at, at, parent.end));
|
|
15484
15954
|
}
|
|
15485
|
-
return
|
|
15955
|
+
return boxes2;
|
|
15486
15956
|
}
|
|
15487
15957
|
function findChild(buf, parent, type) {
|
|
15488
15958
|
return childBoxes(buf, parent).find((box2) => box2.type === type);
|
|
15489
15959
|
}
|
|
15490
|
-
function
|
|
15960
|
+
function child2(buf, parent, type) {
|
|
15491
15961
|
const found = findChild(buf, parent, type);
|
|
15492
15962
|
if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
|
|
15493
15963
|
return found;
|
|
@@ -15539,7 +16009,7 @@ function rows(buf, table, columns, countAt = 4) {
|
|
|
15539
16009
|
function readMediaTime(moov, trak) {
|
|
15540
16010
|
const edts = findChild(moov, trak, "edts");
|
|
15541
16011
|
if (!edts) return 0;
|
|
15542
|
-
const elst =
|
|
16012
|
+
const elst = child2(moov, edts, "elst");
|
|
15543
16013
|
const wide = moov[elst.bodyStart] === 1;
|
|
15544
16014
|
const entries = moov.readUInt32BE(elst.bodyStart + 4);
|
|
15545
16015
|
for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
|
|
@@ -15554,7 +16024,7 @@ function readSizes(moov, stsz) {
|
|
|
15554
16024
|
if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
|
|
15555
16025
|
return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
|
|
15556
16026
|
}
|
|
15557
|
-
function
|
|
16027
|
+
function readSync5(moov, stbl, sampleCount) {
|
|
15558
16028
|
const stss = findChild(moov, stbl, "stss");
|
|
15559
16029
|
if (!stss) return null;
|
|
15560
16030
|
const sync = Array(sampleCount).fill(false);
|
|
@@ -15564,10 +16034,10 @@ function readSync4(moov, stbl, sampleCount) {
|
|
|
15564
16034
|
function readChunkOffsets(moov, stbl) {
|
|
15565
16035
|
const stco = findChild(moov, stbl, "stco");
|
|
15566
16036
|
if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
|
|
15567
|
-
return rows(moov,
|
|
16037
|
+
return rows(moov, child2(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
|
|
15568
16038
|
}
|
|
15569
16039
|
function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
15570
|
-
const stsc = rows(moov,
|
|
16040
|
+
const stsc = rows(moov, child2(moov, stbl, "stsc"), 3);
|
|
15571
16041
|
return stsc.flatMap(([firstChunk, perChunk, description], i) => {
|
|
15572
16042
|
if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
|
|
15573
16043
|
const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
|
|
@@ -15575,28 +16045,28 @@ function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
|
15575
16045
|
});
|
|
15576
16046
|
}
|
|
15577
16047
|
function readTrack(moov, trak) {
|
|
15578
|
-
const mdia =
|
|
15579
|
-
const hdlr =
|
|
15580
|
-
const stbl =
|
|
16048
|
+
const mdia = child2(moov, trak, "mdia");
|
|
16049
|
+
const hdlr = child2(moov, mdia, "hdlr");
|
|
16050
|
+
const stbl = child2(moov, child2(moov, mdia, "minf"), "stbl");
|
|
15581
16051
|
if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
|
|
15582
|
-
const stts = rows(moov,
|
|
16052
|
+
const stts = rows(moov, child2(moov, stbl, "stts"), 2);
|
|
15583
16053
|
const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
|
|
15584
|
-
const sizes = readSizes(moov,
|
|
16054
|
+
const sizes = readSizes(moov, child2(moov, stbl, "stsz"));
|
|
15585
16055
|
const chunkOffsets = readChunkOffsets(moov, stbl);
|
|
15586
16056
|
const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
|
|
15587
16057
|
if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
|
|
15588
16058
|
throw new Error("[mp4Stitch] sample tables disagree on the sample count");
|
|
15589
16059
|
}
|
|
15590
|
-
const stsd =
|
|
16060
|
+
const stsd = child2(moov, stbl, "stsd");
|
|
15591
16061
|
return {
|
|
15592
16062
|
trak,
|
|
15593
16063
|
handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
|
|
15594
|
-
timescale: timescaleOf(moov,
|
|
16064
|
+
timescale: timescaleOf(moov, child2(moov, mdia, "mdhd")),
|
|
15595
16065
|
mediaTime: readMediaTime(moov, trak),
|
|
15596
16066
|
stsd: moov.subarray(stsd.start, stsd.end),
|
|
15597
16067
|
durations,
|
|
15598
16068
|
sizes,
|
|
15599
|
-
sync:
|
|
16069
|
+
sync: readSync5(moov, stbl, sizes.length),
|
|
15600
16070
|
samplesPerChunk,
|
|
15601
16071
|
chunkOffsets
|
|
15602
16072
|
};
|
|
@@ -15737,9 +16207,9 @@ function buildStitchedHeader(layouts, audio) {
|
|
|
15737
16207
|
}
|
|
15738
16208
|
const template = layouts[0].moov;
|
|
15739
16209
|
const root = boxAt(template, 0, 0, template.length);
|
|
15740
|
-
const mvhd =
|
|
16210
|
+
const mvhd = child2(template, root, "mvhd");
|
|
15741
16211
|
const movieTimescale = timescaleOf(template, mvhd);
|
|
15742
|
-
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template,
|
|
16212
|
+
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child2(template, track.trak, "tkhd")));
|
|
15743
16213
|
const audioTrackId = Math.max(...chunkTrackIds) + 1;
|
|
15744
16214
|
const buildMoov = (dataStart2) => {
|
|
15745
16215
|
const traks = plan2.map((track, t) => {
|
|
@@ -15829,18 +16299,18 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
15829
16299
|
const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
|
|
15830
16300
|
const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
|
|
15831
16301
|
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
15832
|
-
|
|
16302
|
+
mkdirSync12(dirname13(outputPath), { recursive: true });
|
|
15833
16303
|
const workDir = mkdtempSync4(`${outputPath}.stitch-`);
|
|
15834
|
-
const out = createWriteStream(
|
|
16304
|
+
const out = createWriteStream(join32(workDir, "out.mp4"));
|
|
15835
16305
|
try {
|
|
15836
16306
|
out.write(header);
|
|
15837
16307
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
15838
16308
|
out.end();
|
|
15839
16309
|
await finished(out);
|
|
15840
|
-
|
|
16310
|
+
renameWithRetry6(join32(workDir, "out.mp4"), outputPath);
|
|
15841
16311
|
} finally {
|
|
15842
16312
|
out.destroy();
|
|
15843
|
-
|
|
16313
|
+
rmSync11(workDir, { recursive: true, force: true });
|
|
15844
16314
|
}
|
|
15845
16315
|
}
|
|
15846
16316
|
|
|
@@ -15929,9 +16399,9 @@ async function writeChunkAudioSlices(input) {
|
|
|
15929
16399
|
offset += track.sizes[offsets.length - 1];
|
|
15930
16400
|
}
|
|
15931
16401
|
});
|
|
15932
|
-
const bytes =
|
|
16402
|
+
const bytes = readFileSync12(input.audioPath);
|
|
15933
16403
|
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
15934
|
-
|
|
16404
|
+
mkdirSync13(join33(input.planDir, SLICE_DIR), { recursive: true });
|
|
15935
16405
|
input.chunks.forEach((_, index) => {
|
|
15936
16406
|
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
15937
16407
|
if (!cut) return;
|
|
@@ -15939,18 +16409,18 @@ async function writeChunkAudioSlices(input) {
|
|
|
15939
16409
|
const start = offsets[cut.firstPacket];
|
|
15940
16410
|
layout.partStarts[index] = start;
|
|
15941
16411
|
const end = offsets[last] + track.sizes[last];
|
|
15942
|
-
|
|
16412
|
+
writeFileSync8(join33(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
15943
16413
|
});
|
|
15944
16414
|
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
15945
|
-
|
|
15946
|
-
|
|
16415
|
+
writeFileSync8(join33(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
16416
|
+
writeFileSync8(join33(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
15947
16417
|
}
|
|
15948
16418
|
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
15949
|
-
const layoutPath =
|
|
16419
|
+
const layoutPath = join33(planDir, SLICE_LAYOUT);
|
|
15950
16420
|
if (!existsSync20(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
15951
|
-
const layout = JSON.parse(
|
|
15952
|
-
const frame =
|
|
15953
|
-
const fd =
|
|
16421
|
+
const layout = JSON.parse(readFileSync12(layoutPath, "utf-8"));
|
|
16422
|
+
const frame = readFileSync12(join33(planDir, SLICE_FRAME));
|
|
16423
|
+
const fd = openSync6(scratchPath, "w");
|
|
15954
16424
|
try {
|
|
15955
16425
|
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
15956
16426
|
writeSync(
|
|
@@ -15962,12 +16432,12 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
|
15962
16432
|
);
|
|
15963
16433
|
const start = layout.partStarts[chunkIndex];
|
|
15964
16434
|
if (start !== void 0) {
|
|
15965
|
-
const part =
|
|
16435
|
+
const part = readFileSync12(join33(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
15966
16436
|
writeSync(fd, part, 0, part.length, start);
|
|
15967
16437
|
}
|
|
15968
16438
|
ftruncateSync(fd, layout.sizeBytes);
|
|
15969
16439
|
} finally {
|
|
15970
|
-
|
|
16440
|
+
closeSync6(fd);
|
|
15971
16441
|
}
|
|
15972
16442
|
return scratchPath;
|
|
15973
16443
|
}
|
|
@@ -15987,7 +16457,7 @@ async function muxChunkWithAudio(input) {
|
|
|
15987
16457
|
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
15988
16458
|
appendRenderProvenanceArgs(args, input.outputPath);
|
|
15989
16459
|
args.push("-y", input.outputPath);
|
|
15990
|
-
const result = await
|
|
16460
|
+
const result = await runFfmpeg7(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
15991
16461
|
if (!result.success) {
|
|
15992
16462
|
throw new Error(
|
|
15993
16463
|
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
|
|
@@ -16016,9 +16486,9 @@ async function muxChunkWithAudio(input) {
|
|
|
16016
16486
|
}
|
|
16017
16487
|
|
|
16018
16488
|
// src/services/distributed/planSize.ts
|
|
16019
|
-
import { createHash as
|
|
16489
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
16020
16490
|
import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
|
|
16021
|
-
import { extname as extname5, join as
|
|
16491
|
+
import { extname as extname5, join as join34, relative as relative4 } from "node:path";
|
|
16022
16492
|
var MAX_TOP_COMPONENTS = 10;
|
|
16023
16493
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
16024
16494
|
".aac",
|
|
@@ -16046,7 +16516,7 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
16046
16516
|
"plan.json": "metadata"
|
|
16047
16517
|
};
|
|
16048
16518
|
function hashComponent(value) {
|
|
16049
|
-
return
|
|
16519
|
+
return createHash8("sha256").update(value).digest("hex").slice(0, 12);
|
|
16050
16520
|
}
|
|
16051
16521
|
function classifyFile(segments, rootKind) {
|
|
16052
16522
|
const rootName = segments[0] ?? "";
|
|
@@ -16130,7 +16600,7 @@ function walkRegularFiles(dir, visit) {
|
|
|
16130
16600
|
return;
|
|
16131
16601
|
}
|
|
16132
16602
|
for (const entry of entries) {
|
|
16133
|
-
const full =
|
|
16603
|
+
const full = join34(dir, entry.name);
|
|
16134
16604
|
if (entry.isDirectory()) {
|
|
16135
16605
|
walkRegularFiles(full, visit);
|
|
16136
16606
|
} else if (entry.isFile()) {
|
|
@@ -16342,13 +16812,13 @@ function assertPositiveInteger2(name, value) {
|
|
|
16342
16812
|
}
|
|
16343
16813
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
16344
16814
|
"plan.json",
|
|
16345
|
-
|
|
16346
|
-
|
|
16347
|
-
|
|
16815
|
+
join35("meta", "composition.json"),
|
|
16816
|
+
join35("meta", "encoder.json"),
|
|
16817
|
+
join35("meta", "chunks.json")
|
|
16348
16818
|
];
|
|
16349
16819
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
16350
16820
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
16351
|
-
|
|
16821
|
+
rmSync12(join35(planDir, relativePath), { force: true });
|
|
16352
16822
|
}
|
|
16353
16823
|
}
|
|
16354
16824
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -16408,6 +16878,9 @@ function buildLockedRenderConfig(input) {
|
|
|
16408
16878
|
preset,
|
|
16409
16879
|
crf: config.crf,
|
|
16410
16880
|
bitrate: config.bitrate,
|
|
16881
|
+
maxBitrate: config.maxBitrate,
|
|
16882
|
+
bufferSize: config.bufferSize,
|
|
16883
|
+
keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
|
|
16411
16884
|
...locksVp9CpuUsed,
|
|
16412
16885
|
// GOP === chunkSize so every chunk's first frame is an IDR keyframe and
|
|
16413
16886
|
// ffmpeg concat-copy round-trips losslessly.
|
|
@@ -16493,23 +16966,23 @@ function createVideoSourceShipper(input) {
|
|
|
16493
16966
|
const known = shipped.get(srcPath);
|
|
16494
16967
|
if (known) return known;
|
|
16495
16968
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
16496
|
-
const current = stagedRel === null ? srcPath :
|
|
16969
|
+
const current = stagedRel === null ? srcPath : join35(input.planDir, "video-frames", stagedRel);
|
|
16497
16970
|
let result = null;
|
|
16498
16971
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
16499
16972
|
const rel = pathInside(root, srcPath);
|
|
16500
|
-
if (rel !== null && (existsSync21(
|
|
16973
|
+
if (rel !== null && (existsSync21(join35(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
16501
16974
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
16502
16975
|
break;
|
|
16503
16976
|
}
|
|
16504
16977
|
}
|
|
16505
16978
|
if (result === null) {
|
|
16506
16979
|
result = `video-sources/${shipped.size}${extname6(current)}`;
|
|
16507
|
-
const destination =
|
|
16508
|
-
|
|
16980
|
+
const destination = join35(input.planDir, ...result.split("/"));
|
|
16981
|
+
mkdirSync14(join35(input.planDir, "video-sources"), { recursive: true });
|
|
16509
16982
|
try {
|
|
16510
16983
|
linkSync(current, destination);
|
|
16511
16984
|
} catch {
|
|
16512
|
-
|
|
16985
|
+
copyFileSync6(current, destination);
|
|
16513
16986
|
}
|
|
16514
16987
|
}
|
|
16515
16988
|
shipped.set(srcPath, result);
|
|
@@ -16525,7 +16998,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
16525
16998
|
useGpu: false,
|
|
16526
16999
|
browserGpuMode: "software"
|
|
16527
17000
|
});
|
|
16528
|
-
if (!existsSync21(planDir))
|
|
17001
|
+
if (!existsSync21(planDir)) mkdirSync14(planDir, { recursive: true });
|
|
16529
17002
|
const log = config.logger ?? defaultLogger;
|
|
16530
17003
|
const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
|
|
16531
17004
|
const abortSignal = config.abortSignal;
|
|
@@ -16554,15 +17027,15 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
16554
17027
|
variables: config.variables
|
|
16555
17028
|
});
|
|
16556
17029
|
const entryFile = config.entryFile ?? "index.html";
|
|
16557
|
-
const htmlPath =
|
|
17030
|
+
const htmlPath = join35(projectDir, entryFile);
|
|
16558
17031
|
if (!existsSync21(htmlPath)) {
|
|
16559
17032
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
16560
17033
|
}
|
|
16561
|
-
const workDir =
|
|
16562
|
-
|
|
16563
|
-
|
|
16564
|
-
const compiledDir =
|
|
16565
|
-
|
|
17034
|
+
const workDir = join35(planDir, ".plan-work");
|
|
17035
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
17036
|
+
mkdirSync14(workDir, { recursive: true });
|
|
17037
|
+
const compiledDir = join35(workDir, "compiled");
|
|
17038
|
+
mkdirSync14(compiledDir, { recursive: true });
|
|
16566
17039
|
let stageStarted = Date.now();
|
|
16567
17040
|
cpSync2(projectDir, compiledDir, {
|
|
16568
17041
|
recursive: true,
|
|
@@ -16575,10 +17048,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
16575
17048
|
}
|
|
16576
17049
|
});
|
|
16577
17050
|
logPlanStage(log, "copy", stageStarted);
|
|
16578
|
-
const finalCompiledDir =
|
|
17051
|
+
const finalCompiledDir = join35(planDir, "compiled");
|
|
16579
17052
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
16580
17053
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
16581
|
-
const remoteLocalPath = (rel) =>
|
|
17054
|
+
const remoteLocalPath = (rel) => join35(projectDir, ...rel.split("/"));
|
|
16582
17055
|
for (const [, file] of remoteFiles) {
|
|
16583
17056
|
declareContentAddressedUrl(file.url, file.sha256);
|
|
16584
17057
|
if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
|
|
@@ -16652,7 +17125,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
16652
17125
|
validateDistributedDuration({
|
|
16653
17126
|
duration: probeResult.duration,
|
|
16654
17127
|
totalFrames,
|
|
16655
|
-
fps:
|
|
17128
|
+
fps: fpsToNumber9(job.config.fps)
|
|
16656
17129
|
});
|
|
16657
17130
|
if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
|
|
16658
17131
|
if (probeResult.probeSession) {
|
|
@@ -16764,18 +17237,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16764
17237
|
let stageStarted = Date.now();
|
|
16765
17238
|
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
16766
17239
|
logPlanStage(log, "extract", stageStarted);
|
|
16767
|
-
const stagedVideoFrames =
|
|
16768
|
-
const videoFramesDst =
|
|
16769
|
-
if (existsSync21(videoFramesDst))
|
|
16770
|
-
const videoSourcesDir =
|
|
16771
|
-
|
|
17240
|
+
const stagedVideoFrames = join35(compiledDir, "__hyperframes_video_frames");
|
|
17241
|
+
const videoFramesDst = join35(planDir, "video-frames");
|
|
17242
|
+
if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
|
|
17243
|
+
const videoSourcesDir = join35(planDir, "video-sources");
|
|
17244
|
+
rmSync12(videoSourcesDir, { recursive: true, force: true });
|
|
16772
17245
|
if (existsSync21(stagedVideoFrames)) {
|
|
16773
|
-
|
|
17246
|
+
renameWithRetry7(stagedVideoFrames, videoFramesDst);
|
|
16774
17247
|
} else {
|
|
16775
|
-
|
|
17248
|
+
mkdirSync14(videoFramesDst, { recursive: true });
|
|
16776
17249
|
}
|
|
16777
|
-
if (existsSync21(finalCompiledDir))
|
|
16778
|
-
|
|
17250
|
+
if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
|
|
17251
|
+
renameWithRetry7(compiledDir, finalCompiledDir);
|
|
16779
17252
|
const shipVideoSource = createVideoSourceShipper({
|
|
16780
17253
|
planDir,
|
|
16781
17254
|
projectDir,
|
|
@@ -16804,7 +17277,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16804
17277
|
};
|
|
16805
17278
|
})
|
|
16806
17279
|
});
|
|
16807
|
-
|
|
17280
|
+
mkdirSync14(join35(planDir, "meta"), { recursive: true });
|
|
16808
17281
|
writePlanRemoteFiles(
|
|
16809
17282
|
planDir,
|
|
16810
17283
|
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
@@ -16813,8 +17286,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16813
17286
|
sizeBytes
|
|
16814
17287
|
}))
|
|
16815
17288
|
);
|
|
16816
|
-
|
|
16817
|
-
|
|
17289
|
+
writeFileSync9(
|
|
17290
|
+
join35(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
16818
17291
|
JSON.stringify(planVideosJson, null, 2),
|
|
16819
17292
|
"utf-8"
|
|
16820
17293
|
);
|
|
@@ -16893,9 +17366,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16893
17366
|
if (audioResult?.audioError) {
|
|
16894
17367
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
16895
17368
|
}
|
|
16896
|
-
const planAudioPath =
|
|
17369
|
+
const planAudioPath = join35(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
16897
17370
|
if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
|
|
16898
|
-
|
|
17371
|
+
renameWithRetry7(audioResult.audioOutputPath, planAudioPath);
|
|
16899
17372
|
}
|
|
16900
17373
|
if (chunksCarryAudio && existsSync21(planAudioPath)) {
|
|
16901
17374
|
if (options.sliceChunkAudio) {
|
|
@@ -16942,7 +17415,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16942
17415
|
format: config.format
|
|
16943
17416
|
};
|
|
16944
17417
|
try {
|
|
16945
|
-
|
|
17418
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
16946
17419
|
} catch (err) {
|
|
16947
17420
|
log.warn("[plan] failed to remove temp work dir", {
|
|
16948
17421
|
workDir,
|
|
@@ -17093,6 +17566,20 @@ function validateDistributedRenderConfig(config) {
|
|
|
17093
17566
|
`must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
|
|
17094
17567
|
);
|
|
17095
17568
|
}
|
|
17569
|
+
try {
|
|
17570
|
+
validateDeliveryEncoderConfig({
|
|
17571
|
+
format: config.format,
|
|
17572
|
+
fps: { num: config.fps, den: 1 },
|
|
17573
|
+
keyframeIntervalSeconds: config.keyframeIntervalSeconds,
|
|
17574
|
+
maxBitrate: config.maxBitrate,
|
|
17575
|
+
bufferSize: config.bufferSize
|
|
17576
|
+
});
|
|
17577
|
+
} catch (error) {
|
|
17578
|
+
throw new InvalidConfigError(
|
|
17579
|
+
"config.maxBitrate",
|
|
17580
|
+
error instanceof Error ? error.message : String(error)
|
|
17581
|
+
);
|
|
17582
|
+
}
|
|
17096
17583
|
if (config.chunkSize !== void 0) {
|
|
17097
17584
|
if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
|
|
17098
17585
|
throw new InvalidConfigError(
|
|
@@ -17272,6 +17759,12 @@ function assertOptionalInteger(options, field, min = 0) {
|
|
|
17272
17759
|
throw new Error(`Render request ${field} must be an integer >= ${min}`);
|
|
17273
17760
|
}
|
|
17274
17761
|
}
|
|
17762
|
+
function assertOptionalPositiveNumber(options, field) {
|
|
17763
|
+
const value = options[field];
|
|
17764
|
+
if (value !== void 0 && !(typeof value === "number" && value > 0)) {
|
|
17765
|
+
throw new Error(`Render request ${field} must be a positive number`);
|
|
17766
|
+
}
|
|
17767
|
+
}
|
|
17275
17768
|
function assertOptionalString(options, field) {
|
|
17276
17769
|
if (options[field] !== void 0 && typeof options[field] !== "string") {
|
|
17277
17770
|
throw new Error(`Render request ${field} must be a string`);
|
|
@@ -17319,10 +17812,11 @@ function assertRequestOptionScalars(options) {
|
|
|
17319
17812
|
assertOptionalInteger(options, "workers", 1);
|
|
17320
17813
|
assertOptionalInteger(options, "maxRenderMs");
|
|
17321
17814
|
assertOptionalInteger(options, "crf");
|
|
17815
|
+
assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
|
|
17322
17816
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
17323
17817
|
assertOptionalBoolean(options, field);
|
|
17324
17818
|
}
|
|
17325
|
-
for (const field of ["entryFile", "videoBitrate"]) {
|
|
17819
|
+
for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
|
|
17326
17820
|
assertOptionalString(options, field);
|
|
17327
17821
|
}
|
|
17328
17822
|
assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
|
|
@@ -17425,6 +17919,9 @@ function distributedConfigFromRequest(request, runtime = {}) {
|
|
|
17425
17919
|
crf: options.crf,
|
|
17426
17920
|
pageMotionBlur: options.pageMotionBlur,
|
|
17427
17921
|
bitrate: options.videoBitrate,
|
|
17922
|
+
keyframeIntervalSeconds: options.keyframeIntervalSeconds,
|
|
17923
|
+
maxBitrate: options.maxBitrate,
|
|
17924
|
+
bufferSize: options.bufferSize,
|
|
17428
17925
|
videoFrameFormat: options.videoFrameFormat,
|
|
17429
17926
|
outputResolution: options.outputResolution,
|
|
17430
17927
|
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
|
|
@@ -17472,6 +17969,9 @@ function renderRequestFromDistributedConfig(input) {
|
|
|
17472
17969
|
...optionalProperty("crf", config.crf),
|
|
17473
17970
|
...optionalProperty("pageMotionBlur", config.pageMotionBlur),
|
|
17474
17971
|
...optionalProperty("videoBitrate", config.bitrate),
|
|
17972
|
+
...optionalProperty("keyframeIntervalSeconds", config.keyframeIntervalSeconds),
|
|
17973
|
+
...optionalProperty("maxBitrate", config.maxBitrate),
|
|
17974
|
+
...optionalProperty("bufferSize", config.bufferSize),
|
|
17475
17975
|
...optionalProperty("videoFrameFormat", config.videoFrameFormat),
|
|
17476
17976
|
...optionalProperty("outputResolution", config.outputResolution),
|
|
17477
17977
|
...optionalProperty("outputResolutionAspectAgnostic", config.outputResolutionAspectAgnostic),
|
|
@@ -17513,14 +18013,14 @@ import {
|
|
|
17513
18013
|
// src/server.ts
|
|
17514
18014
|
import {
|
|
17515
18015
|
existsSync as existsSync24,
|
|
17516
|
-
mkdirSync as
|
|
17517
|
-
statSync as
|
|
18016
|
+
mkdirSync as mkdirSync15,
|
|
18017
|
+
statSync as statSync10,
|
|
17518
18018
|
mkdtempSync as mkdtempSync5,
|
|
17519
|
-
writeFileSync as
|
|
17520
|
-
rmSync as
|
|
18019
|
+
writeFileSync as writeFileSync10,
|
|
18020
|
+
rmSync as rmSync13,
|
|
17521
18021
|
createReadStream as createReadStream5
|
|
17522
18022
|
} from "node:fs";
|
|
17523
|
-
import { resolve as
|
|
18023
|
+
import { resolve as resolve11, dirname as dirname15, join as join38 } from "node:path";
|
|
17524
18024
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
17525
18025
|
import { parseArgs } from "node:util";
|
|
17526
18026
|
import crypto from "node:crypto";
|
|
@@ -17532,14 +18032,14 @@ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engi
|
|
|
17532
18032
|
// src/services/hyperframeLint.ts
|
|
17533
18033
|
import {
|
|
17534
18034
|
existsSync as existsSync22,
|
|
17535
|
-
readFileSync as
|
|
17536
|
-
statSync as
|
|
17537
|
-
openSync as
|
|
18035
|
+
readFileSync as readFileSync13,
|
|
18036
|
+
statSync as statSync9,
|
|
18037
|
+
openSync as openSync7,
|
|
17538
18038
|
fstatSync as fstatSync3,
|
|
17539
|
-
closeSync as
|
|
18039
|
+
closeSync as closeSync7,
|
|
17540
18040
|
constants as constants3
|
|
17541
18041
|
} from "node:fs";
|
|
17542
|
-
import { resolve as
|
|
18042
|
+
import { resolve as resolve10, join as join36, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
|
|
17543
18043
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
17544
18044
|
function isStringRecord(value) {
|
|
17545
18045
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -17569,30 +18069,30 @@ function pickEntryFile(files, preferredEntryFile) {
|
|
|
17569
18069
|
function readEntryHtml(filePath) {
|
|
17570
18070
|
let fd;
|
|
17571
18071
|
try {
|
|
17572
|
-
fd =
|
|
18072
|
+
fd = openSync7(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
|
|
17573
18073
|
} catch (error) {
|
|
17574
18074
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
17575
18075
|
return null;
|
|
17576
|
-
if (!
|
|
18076
|
+
if (!statSync9(filePath, { throwIfNoEntry: false })?.isFile()) return null;
|
|
17577
18077
|
throw error;
|
|
17578
18078
|
}
|
|
17579
18079
|
try {
|
|
17580
18080
|
if (!fstatSync3(fd).isFile()) return null;
|
|
17581
|
-
return
|
|
18081
|
+
return readFileSync13(fd, "utf-8");
|
|
17582
18082
|
} finally {
|
|
17583
|
-
|
|
18083
|
+
closeSync7(fd);
|
|
17584
18084
|
}
|
|
17585
18085
|
}
|
|
17586
18086
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
17587
|
-
const absProjectDir =
|
|
17588
|
-
if (!existsSync22(absProjectDir) || !
|
|
18087
|
+
const absProjectDir = resolve10(projectDir);
|
|
18088
|
+
if (!existsSync22(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
|
|
17589
18089
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
17590
18090
|
}
|
|
17591
18091
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
17592
18092
|
(value) => typeof value === "string" && value.trim().length > 0
|
|
17593
18093
|
);
|
|
17594
18094
|
for (const entryFile of entryCandidates) {
|
|
17595
|
-
const absoluteEntryPath =
|
|
18095
|
+
const absoluteEntryPath = resolve10(absProjectDir, entryFile);
|
|
17596
18096
|
const entryRelativePath = relative6(absProjectDir, absoluteEntryPath);
|
|
17597
18097
|
if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep3}`) || isAbsolute4(entryRelativePath)) {
|
|
17598
18098
|
return { error: `Entry file must stay inside project directory: ${entryFile}` };
|
|
@@ -17607,7 +18107,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
17607
18107
|
}
|
|
17608
18108
|
}
|
|
17609
18109
|
return {
|
|
17610
|
-
error: `No HTML entry file found in project directory: ${
|
|
18110
|
+
error: `No HTML entry file found in project directory: ${join36(absProjectDir, preferredEntryFile || "index.html")}`
|
|
17611
18111
|
};
|
|
17612
18112
|
}
|
|
17613
18113
|
function prepareHyperframeLintBody(body) {
|
|
@@ -17653,7 +18153,7 @@ async function runHyperframeLint(prepared) {
|
|
|
17653
18153
|
// src/services/healthWorker.ts
|
|
17654
18154
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
17655
18155
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
17656
|
-
import { dirname as
|
|
18156
|
+
import { dirname as dirname14, join as join37 } from "node:path";
|
|
17657
18157
|
import { existsSync as existsSync23 } from "node:fs";
|
|
17658
18158
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
17659
18159
|
async function startHealthWorker(options = {}) {
|
|
@@ -17672,12 +18172,12 @@ async function startHealthWorker(options = {}) {
|
|
|
17672
18172
|
stdout: false,
|
|
17673
18173
|
stderr: false
|
|
17674
18174
|
});
|
|
17675
|
-
await new Promise((
|
|
18175
|
+
await new Promise((resolve13, reject) => {
|
|
17676
18176
|
const onMessage = (msg) => {
|
|
17677
18177
|
if (msg?.type === "listening") {
|
|
17678
18178
|
worker.off("error", onError);
|
|
17679
18179
|
worker.off("message", onMessage);
|
|
17680
|
-
|
|
18180
|
+
resolve13();
|
|
17681
18181
|
} else if (msg?.type === "listen-error") {
|
|
17682
18182
|
worker.off("error", onError);
|
|
17683
18183
|
worker.off("message", onMessage);
|
|
@@ -17728,8 +18228,8 @@ function defaultLogger2() {
|
|
|
17728
18228
|
};
|
|
17729
18229
|
}
|
|
17730
18230
|
function resolveWorkerEntry2() {
|
|
17731
|
-
const here =
|
|
17732
|
-
const candidates = [
|
|
18231
|
+
const here = dirname14(fileURLToPath6(import.meta.url));
|
|
18232
|
+
const candidates = [join37(here, "healthWorkerThread.js"), join37(here, "healthWorkerThread.ts")];
|
|
17733
18233
|
for (const candidate of candidates) {
|
|
17734
18234
|
if (existsSync23(candidate)) return candidate;
|
|
17735
18235
|
}
|
|
@@ -17747,6 +18247,8 @@ var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
|
|
|
17747
18247
|
var SSE_KEEPALIVE_MS = 3e4;
|
|
17748
18248
|
var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
17749
18249
|
"ASSET_MEDIA_TYPE_MISMATCH",
|
|
18250
|
+
"HEIF_IMAGE_CONVERSION_FAILED",
|
|
18251
|
+
"HEIF_TOOL_UNAVAILABLE",
|
|
17750
18252
|
"NOT_MEDIA_PAYLOAD",
|
|
17751
18253
|
"INVALID_VIDEO_METADATA",
|
|
17752
18254
|
"VIDEO_SOURCE_UNRENDERABLE",
|
|
@@ -17821,7 +18323,7 @@ function parseRenderOptions(body) {
|
|
|
17821
18323
|
const format = parseServerFormat(body.format);
|
|
17822
18324
|
const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
|
|
17823
18325
|
const encoder = parseEncoderOptions(body, format ?? "mp4");
|
|
17824
|
-
const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
|
|
18326
|
+
const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
|
|
17825
18327
|
const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
|
|
17826
18328
|
return {
|
|
17827
18329
|
outputPath,
|
|
@@ -17831,6 +18333,9 @@ function parseRenderOptions(body) {
|
|
|
17831
18333
|
maxRenderMs,
|
|
17832
18334
|
crf,
|
|
17833
18335
|
videoBitrate,
|
|
18336
|
+
keyframeIntervalSeconds,
|
|
18337
|
+
maxBitrate,
|
|
18338
|
+
bufferSize,
|
|
17834
18339
|
useGpu,
|
|
17835
18340
|
debug,
|
|
17836
18341
|
strictness,
|
|
@@ -17865,6 +18370,9 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
17865
18370
|
maxRenderMs: input.maxRenderMs,
|
|
17866
18371
|
crf: input.crf,
|
|
17867
18372
|
videoBitrate: input.videoBitrate,
|
|
18373
|
+
keyframeIntervalSeconds: input.keyframeIntervalSeconds,
|
|
18374
|
+
maxBitrate: input.maxBitrate,
|
|
18375
|
+
bufferSize: input.bufferSize,
|
|
17868
18376
|
useGpu: input.useGpu,
|
|
17869
18377
|
debug: input.debug,
|
|
17870
18378
|
strictness: input.strictness,
|
|
@@ -17882,8 +18390,8 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
17882
18390
|
function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
17883
18391
|
const { input, cleanupProjectDir } = prepared;
|
|
17884
18392
|
const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
|
|
17885
|
-
const outputDir =
|
|
17886
|
-
if (!existsSync24(outputDir))
|
|
18393
|
+
const outputDir = dirname15(absoluteOutputPath);
|
|
18394
|
+
if (!existsSync24(outputDir)) mkdirSync15(outputDir, { recursive: true });
|
|
17887
18395
|
return { input, cleanupProjectDir, absoluteOutputPath };
|
|
17888
18396
|
}
|
|
17889
18397
|
function validateRenderOverrides(body) {
|
|
@@ -17923,12 +18431,12 @@ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
|
|
|
17923
18431
|
function prepareProjectDirectory(projectDir, options) {
|
|
17924
18432
|
const candidate = nonEmptyString(projectDir);
|
|
17925
18433
|
if (!candidate) return null;
|
|
17926
|
-
const absProjectDir =
|
|
17927
|
-
if (!existsSync24(absProjectDir) || !
|
|
18434
|
+
const absProjectDir = resolve11(candidate);
|
|
18435
|
+
if (!existsSync24(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
|
|
17928
18436
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
17929
18437
|
}
|
|
17930
18438
|
const entry = options.entryFile || "index.html";
|
|
17931
|
-
if (!existsSync24(
|
|
18439
|
+
if (!existsSync24(resolve11(absProjectDir, entry))) {
|
|
17932
18440
|
return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
|
|
17933
18441
|
}
|
|
17934
18442
|
return { prepared: { input: { projectDir: absProjectDir, ...options } } };
|
|
@@ -17949,8 +18457,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
17949
18457
|
}
|
|
17950
18458
|
function materializeInlineProject(html, options) {
|
|
17951
18459
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
17952
|
-
const tempProjectDir = mkdtempSync5(
|
|
17953
|
-
|
|
18460
|
+
const tempProjectDir = mkdtempSync5(join38(tempRoot, "producer-project-"));
|
|
18461
|
+
writeFileSync10(join38(tempProjectDir, "index.html"), html, "utf-8");
|
|
17954
18462
|
return {
|
|
17955
18463
|
prepared: {
|
|
17956
18464
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -17975,7 +18483,7 @@ function resolveOutputPath(projectDir, outputCandidate, rendersDir, log) {
|
|
|
17975
18483
|
try {
|
|
17976
18484
|
return resolveRenderPaths(projectDir, outputCandidate, rendersDir).absoluteOutputPath;
|
|
17977
18485
|
} catch (error) {
|
|
17978
|
-
const fallbackPath =
|
|
18486
|
+
const fallbackPath = resolve11(rendersDir, `producer-fallback-${Date.now()}.mp4`);
|
|
17979
18487
|
log.warn("Failed to resolve output path, using fallback", {
|
|
17980
18488
|
fallback: fallbackPath,
|
|
17981
18489
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -18011,7 +18519,7 @@ function createArtifactStore(ttlMs) {
|
|
|
18011
18519
|
function cleanupTempDir(dir, log) {
|
|
18012
18520
|
if (!dir) return;
|
|
18013
18521
|
try {
|
|
18014
|
-
|
|
18522
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
18015
18523
|
} catch (error) {
|
|
18016
18524
|
log.warn("Failed to cleanup temp project dir", {
|
|
18017
18525
|
cleanupProjectDir: dir,
|
|
@@ -18020,7 +18528,7 @@ function cleanupTempDir(dir, log) {
|
|
|
18020
18528
|
}
|
|
18021
18529
|
}
|
|
18022
18530
|
function outputFileSize(path) {
|
|
18023
|
-
return existsSync24(path) ?
|
|
18531
|
+
return existsSync24(path) ? statSync10(path).size : 0;
|
|
18024
18532
|
}
|
|
18025
18533
|
function createBlockingProgressReporter(log, requestId) {
|
|
18026
18534
|
let lastLoggedPct = -10;
|
|
@@ -18326,7 +18834,7 @@ function createRenderHandlers(options = {}) {
|
|
|
18326
18834
|
store.delete(token);
|
|
18327
18835
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
18328
18836
|
}
|
|
18329
|
-
const stats =
|
|
18837
|
+
const stats = statSync10(artifact.path);
|
|
18330
18838
|
return new Response(createReadStream5(artifact.path), {
|
|
18331
18839
|
headers: {
|
|
18332
18840
|
"content-type": "video/mp4",
|
|
@@ -18393,7 +18901,7 @@ function startServer(options = {}) {
|
|
|
18393
18901
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
18394
18902
|
return server;
|
|
18395
18903
|
}
|
|
18396
|
-
var entryScript = process.argv[1] ?
|
|
18904
|
+
var entryScript = process.argv[1] ? resolve11(process.argv[1]) : "";
|
|
18397
18905
|
var isPublicServerEntry = entryScript.endsWith("/public-server.js") || entryScript.endsWith("/src/server.ts");
|
|
18398
18906
|
if (isPublicServerEntry) {
|
|
18399
18907
|
const { values } = parseArgs({
|
|
@@ -18418,35 +18926,35 @@ import { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes
|
|
|
18418
18926
|
|
|
18419
18927
|
// src/services/distributed/planV2.ts
|
|
18420
18928
|
import {
|
|
18421
|
-
copyFileSync as
|
|
18929
|
+
copyFileSync as copyFileSync8,
|
|
18422
18930
|
existsSync as existsSync26,
|
|
18423
18931
|
lstatSync as lstatSync3,
|
|
18424
|
-
mkdirSync as
|
|
18932
|
+
mkdirSync as mkdirSync17,
|
|
18425
18933
|
mkdtempSync as mkdtempSync7,
|
|
18426
|
-
readFileSync as
|
|
18934
|
+
readFileSync as readFileSync14,
|
|
18427
18935
|
readdirSync as readdirSync9,
|
|
18428
|
-
rmSync as
|
|
18429
|
-
statSync as
|
|
18430
|
-
writeFileSync as
|
|
18936
|
+
rmSync as rmSync15,
|
|
18937
|
+
statSync as statSync12,
|
|
18938
|
+
writeFileSync as writeFileSync12
|
|
18431
18939
|
} from "node:fs";
|
|
18432
18940
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
18433
|
-
import { dirname as
|
|
18941
|
+
import { dirname as dirname17, isAbsolute as isAbsolute5, join as join41, relative as relative7, resolve as resolve12, sep as sep4 } from "node:path";
|
|
18434
18942
|
import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
|
|
18435
|
-
import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as
|
|
18943
|
+
import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry9 } from "@hyperframes/engine";
|
|
18436
18944
|
|
|
18437
18945
|
// src/services/distributed/planV2Publisher.ts
|
|
18438
18946
|
import {
|
|
18439
|
-
copyFileSync as
|
|
18947
|
+
copyFileSync as copyFileSync7,
|
|
18440
18948
|
existsSync as existsSync25,
|
|
18441
18949
|
linkSync as linkSync2,
|
|
18442
|
-
mkdirSync as
|
|
18950
|
+
mkdirSync as mkdirSync16,
|
|
18443
18951
|
mkdtempSync as mkdtempSync6,
|
|
18444
|
-
rmSync as
|
|
18445
|
-
statSync as
|
|
18446
|
-
writeFileSync as
|
|
18952
|
+
rmSync as rmSync14,
|
|
18953
|
+
statSync as statSync11,
|
|
18954
|
+
writeFileSync as writeFileSync11
|
|
18447
18955
|
} from "node:fs";
|
|
18448
|
-
import { renameWithRetry as
|
|
18449
|
-
import { dirname as
|
|
18956
|
+
import { renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
|
|
18957
|
+
import { dirname as dirname16, join as join40 } from "node:path";
|
|
18450
18958
|
|
|
18451
18959
|
// src/services/distributed/planV2Errors.ts
|
|
18452
18960
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -18461,7 +18969,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
18461
18969
|
};
|
|
18462
18970
|
|
|
18463
18971
|
// src/services/distributed/planV2Layout.ts
|
|
18464
|
-
import { join as
|
|
18972
|
+
import { join as join39 } from "node:path";
|
|
18465
18973
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
18466
18974
|
function assertPlanV2Sha256(value, field) {
|
|
18467
18975
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -18471,7 +18979,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
18471
18979
|
}
|
|
18472
18980
|
function planV2BlobPath(root, sha256) {
|
|
18473
18981
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
18474
|
-
return
|
|
18982
|
+
return join39(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
18475
18983
|
}
|
|
18476
18984
|
|
|
18477
18985
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -18511,12 +19019,12 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18511
19019
|
}
|
|
18512
19020
|
this.destinationDir = destinationDir;
|
|
18513
19021
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
18514
|
-
|
|
18515
|
-
this.temporaryDir = mkdtempSync6(
|
|
19022
|
+
mkdirSync16(dirname16(destinationDir), { recursive: true });
|
|
19023
|
+
this.temporaryDir = mkdtempSync6(join40(dirname16(destinationDir), ".plan-v2-publish-"));
|
|
18516
19024
|
}
|
|
18517
19025
|
async putBlob(blob) {
|
|
18518
19026
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
18519
|
-
const sourceSize =
|
|
19027
|
+
const sourceSize = statSync11(blob.sourcePath).size;
|
|
18520
19028
|
if (sourceSize !== blob.sizeBytes) {
|
|
18521
19029
|
throw new PlanV2IntegrityError(
|
|
18522
19030
|
`published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
|
|
@@ -18524,19 +19032,19 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18524
19032
|
}
|
|
18525
19033
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
18526
19034
|
if (existsSync25(destinationPath)) return;
|
|
18527
|
-
|
|
18528
|
-
const stagingDir = mkdtempSync6(
|
|
18529
|
-
const temporaryPath =
|
|
19035
|
+
mkdirSync16(dirname16(destinationPath), { recursive: true });
|
|
19036
|
+
const stagingDir = mkdtempSync6(join40(dirname16(destinationPath), ".plan-v2-blob-"));
|
|
19037
|
+
const temporaryPath = join40(stagingDir, "blob");
|
|
18530
19038
|
try {
|
|
18531
19039
|
try {
|
|
18532
19040
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
18533
19041
|
} catch (error) {
|
|
18534
19042
|
if (!canFallbackToCopy(error)) throw error;
|
|
18535
|
-
|
|
19043
|
+
copyFileSync7(blob.sourcePath, temporaryPath);
|
|
18536
19044
|
}
|
|
18537
|
-
|
|
19045
|
+
renameWithRetry8(temporaryPath, destinationPath);
|
|
18538
19046
|
} finally {
|
|
18539
|
-
|
|
19047
|
+
rmSync14(stagingDir, { recursive: true, force: true });
|
|
18540
19048
|
}
|
|
18541
19049
|
}
|
|
18542
19050
|
async commitManifest(manifestBytes) {
|
|
@@ -18547,13 +19055,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18547
19055
|
);
|
|
18548
19056
|
}
|
|
18549
19057
|
}
|
|
18550
|
-
|
|
18551
|
-
|
|
19058
|
+
writeFileSync11(join40(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
19059
|
+
renameWithRetry8(this.temporaryDir, this.destinationDir);
|
|
18552
19060
|
this.#committed = true;
|
|
18553
19061
|
}
|
|
18554
19062
|
async abort() {
|
|
18555
19063
|
if (!this.#committed) {
|
|
18556
|
-
|
|
19064
|
+
rmSync14(this.temporaryDir, { recursive: true, force: true });
|
|
18557
19065
|
}
|
|
18558
19066
|
}
|
|
18559
19067
|
};
|
|
@@ -18572,7 +19080,7 @@ function isNonNegativeIntegerArray(value) {
|
|
|
18572
19080
|
return Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0);
|
|
18573
19081
|
}
|
|
18574
19082
|
function readJsonFile(path, label) {
|
|
18575
|
-
const contents =
|
|
19083
|
+
const contents = readFileSync14(path, "utf-8");
|
|
18576
19084
|
try {
|
|
18577
19085
|
const parsed = JSON.parse(contents);
|
|
18578
19086
|
return parsed;
|
|
@@ -18589,10 +19097,10 @@ function assertSafeRelativePath(path) {
|
|
|
18589
19097
|
}
|
|
18590
19098
|
function listFiles(root) {
|
|
18591
19099
|
const files = [];
|
|
18592
|
-
const rootResolved =
|
|
19100
|
+
const rootResolved = resolve12(root);
|
|
18593
19101
|
function walk(dir) {
|
|
18594
19102
|
for (const entry of readdirSync9(dir, { withFileTypes: true })) {
|
|
18595
|
-
const absolutePath =
|
|
19103
|
+
const absolutePath = join41(dir, entry.name);
|
|
18596
19104
|
if (entry.isDirectory()) {
|
|
18597
19105
|
walk(absolutePath);
|
|
18598
19106
|
} else if (entry.isFile()) {
|
|
@@ -18619,13 +19127,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
18619
19127
|
}
|
|
18620
19128
|
}
|
|
18621
19129
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
18622
|
-
const videoRoot =
|
|
19130
|
+
const videoRoot = join41(executionPlanDir, "video-frames");
|
|
18623
19131
|
if (!existsSync26(videoRoot)) return;
|
|
18624
19132
|
for (const videoEntry of readdirSync9(videoRoot, { withFileTypes: true })) {
|
|
18625
19133
|
if (!videoEntry.isDirectory()) continue;
|
|
18626
|
-
const videoDir =
|
|
19134
|
+
const videoDir = join41(videoRoot, videoEntry.name);
|
|
18627
19135
|
if (readdirSync9(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
18628
|
-
const sentinelPath =
|
|
19136
|
+
const sentinelPath = join41(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
18629
19137
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
18630
19138
|
}
|
|
18631
19139
|
}
|
|
@@ -18635,8 +19143,8 @@ function isExtractionCacheCompleteSentinelPath(path) {
|
|
|
18635
19143
|
return segments.length === 3 && segments[0] === "video-frames" && segments[1] !== "" && segments[2] === EXTRACTION_CACHE_COMPLETE_SENTINEL;
|
|
18636
19144
|
}
|
|
18637
19145
|
function resolveExtractedVideoOutputDir(planDir, videoId) {
|
|
18638
|
-
const videoRoot =
|
|
18639
|
-
const outputDir =
|
|
19146
|
+
const videoRoot = resolve12(planDir, "video-frames");
|
|
19147
|
+
const outputDir = resolve12(videoRoot, videoId);
|
|
18640
19148
|
if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep4}`)) {
|
|
18641
19149
|
throw new PlanV2IntegrityError(`unsafe extracted video id: ${JSON.stringify(videoId)}`);
|
|
18642
19150
|
}
|
|
@@ -18672,7 +19180,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
18672
19180
|
if (video.deferredRange) {
|
|
18673
19181
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
18674
19182
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
18675
|
-
framePaths2.set(index,
|
|
19183
|
+
framePaths2.set(index, join41(outputDir, String(index)));
|
|
18676
19184
|
}
|
|
18677
19185
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
18678
19186
|
}
|
|
@@ -18680,7 +19188,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
18680
19188
|
const framePaths = /* @__PURE__ */ new Map();
|
|
18681
19189
|
for (const frameName of frameNames) {
|
|
18682
19190
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
18683
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
19191
|
+
assertValidExtractionCacheCompleteSentinel(join41(outputDir, frameName));
|
|
18684
19192
|
continue;
|
|
18685
19193
|
}
|
|
18686
19194
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -18699,7 +19207,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
18699
19207
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
18700
19208
|
);
|
|
18701
19209
|
}
|
|
18702
|
-
framePaths.set(frameIndex,
|
|
19210
|
+
framePaths.set(frameIndex, join41(outputDir, frameName));
|
|
18703
19211
|
}
|
|
18704
19212
|
return {
|
|
18705
19213
|
...video,
|
|
@@ -18720,11 +19228,11 @@ function parsePlanVideosJson2(value) {
|
|
|
18720
19228
|
}
|
|
18721
19229
|
}
|
|
18722
19230
|
function materializeExtractedVideoDirectories(planDir) {
|
|
18723
|
-
const videosPath =
|
|
19231
|
+
const videosPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18724
19232
|
if (!existsSync26(videosPath)) return;
|
|
18725
19233
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
18726
19234
|
for (const video of videos.extracted) {
|
|
18727
|
-
|
|
19235
|
+
mkdirSync17(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
|
|
18728
19236
|
}
|
|
18729
19237
|
}
|
|
18730
19238
|
function parseChunkSlices(value) {
|
|
@@ -18754,13 +19262,13 @@ function parseChunkSlices(value) {
|
|
|
18754
19262
|
});
|
|
18755
19263
|
}
|
|
18756
19264
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
18757
|
-
const videoRoot =
|
|
19265
|
+
const videoRoot = join41(executionPlanDir, "video-frames");
|
|
18758
19266
|
const hasExtractedFrames = existsSync26(videoRoot) && listFiles(videoRoot).length > 0;
|
|
18759
|
-
const videosPath =
|
|
19267
|
+
const videosPath = join41(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18760
19268
|
if (!existsSync26(videosPath)) {
|
|
18761
19269
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
18762
19270
|
}
|
|
18763
|
-
const chunksPath =
|
|
19271
|
+
const chunksPath = join41(executionPlanDir, "meta", "chunks.json");
|
|
18764
19272
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18765
19273
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
18766
19274
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -18797,7 +19305,7 @@ function collectChunkVideoDependencies(input) {
|
|
|
18797
19305
|
for (let frame = chunk.startFrame; frame < chunk.endFrame; frame++) {
|
|
18798
19306
|
const globalTime = frame * input.fpsDen / input.fpsNum;
|
|
18799
19307
|
for (const [videoId, payload] of input.table.getActiveFramePayloads(globalTime)) {
|
|
18800
|
-
const path = input.deferredSources.get(videoId) ?? relative7(
|
|
19308
|
+
const path = input.deferredSources.get(videoId) ?? relative7(resolve12(input.executionPlanDir), payload.framePath).split(sep4).join("/");
|
|
18801
19309
|
assertSafeRelativePath(path);
|
|
18802
19310
|
const indexes = mutable.get(path) ?? /* @__PURE__ */ new Set();
|
|
18803
19311
|
indexes.add(chunk.index);
|
|
@@ -18815,21 +19323,21 @@ function computeManifestHash(manifest) {
|
|
|
18815
19323
|
}
|
|
18816
19324
|
function writeBlob(sourcePath, destinationPath) {
|
|
18817
19325
|
if (existsSync26(destinationPath)) return;
|
|
18818
|
-
|
|
18819
|
-
const temporaryDir = mkdtempSync7(
|
|
18820
|
-
const temporaryPath =
|
|
19326
|
+
mkdirSync17(dirname17(destinationPath), { recursive: true });
|
|
19327
|
+
const temporaryDir = mkdtempSync7(join41(dirname17(destinationPath), ".plan-v2-blob-"));
|
|
19328
|
+
const temporaryPath = join41(temporaryDir, "blob");
|
|
18821
19329
|
try {
|
|
18822
|
-
|
|
18823
|
-
|
|
19330
|
+
copyFileSync8(sourcePath, temporaryPath);
|
|
19331
|
+
renameWithRetry9(temporaryPath, destinationPath);
|
|
18824
19332
|
} finally {
|
|
18825
|
-
|
|
19333
|
+
rmSync15(temporaryDir, { recursive: true, force: true });
|
|
18826
19334
|
}
|
|
18827
19335
|
}
|
|
18828
19336
|
function publishBlobFor(absolutePath, digests) {
|
|
18829
19337
|
return {
|
|
18830
19338
|
sourcePath: absolutePath,
|
|
18831
19339
|
sha256: planFileSha256(absolutePath, digests),
|
|
18832
|
-
sizeBytes:
|
|
19340
|
+
sizeBytes: statSync12(absolutePath).size
|
|
18833
19341
|
};
|
|
18834
19342
|
}
|
|
18835
19343
|
async function putBlobs(publisher, blobs) {
|
|
@@ -18853,7 +19361,7 @@ async function putTasks(tasks) {
|
|
|
18853
19361
|
if (failure) throw failure.reason;
|
|
18854
19362
|
}
|
|
18855
19363
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
18856
|
-
const executionPlanPath =
|
|
19364
|
+
const executionPlanPath = join41(executionPlanDir, "plan.json");
|
|
18857
19365
|
if (!existsSync26(executionPlanPath)) {
|
|
18858
19366
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
18859
19367
|
}
|
|
@@ -18881,7 +19389,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
18881
19389
|
}
|
|
18882
19390
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
18883
19391
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
18884
|
-
const encoder = readJsonFile(
|
|
19392
|
+
const encoder = readJsonFile(join41(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
18885
19393
|
const files = listFiles(executionPlanDir);
|
|
18886
19394
|
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
18887
19395
|
for (const file of files) {
|
|
@@ -18941,21 +19449,21 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
18941
19449
|
if (publication.storedBlobs.length > 0) {
|
|
18942
19450
|
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
18943
19451
|
}
|
|
18944
|
-
|
|
18945
|
-
const tempDir = mkdtempSync7(
|
|
19452
|
+
mkdirSync17(dirname17(planV2Dir), { recursive: true });
|
|
19453
|
+
const tempDir = mkdtempSync7(join41(dirname17(planV2Dir), ".plan-v2-build-"));
|
|
18946
19454
|
try {
|
|
18947
19455
|
for (const blob of publication.blobs) {
|
|
18948
19456
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
18949
19457
|
}
|
|
18950
|
-
|
|
18951
|
-
|
|
19458
|
+
writeFileSync12(
|
|
19459
|
+
join41(tempDir, "plan.json"),
|
|
18952
19460
|
canonicalJsonStringify(publication.manifest),
|
|
18953
19461
|
"utf-8"
|
|
18954
19462
|
);
|
|
18955
|
-
|
|
19463
|
+
renameWithRetry9(tempDir, planV2Dir);
|
|
18956
19464
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
18957
19465
|
} catch (error) {
|
|
18958
|
-
|
|
19466
|
+
rmSync15(tempDir, { recursive: true, force: true });
|
|
18959
19467
|
throw error;
|
|
18960
19468
|
}
|
|
18961
19469
|
}
|
|
@@ -18995,7 +19503,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
|
18995
19503
|
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
18996
19504
|
}
|
|
18997
19505
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
18998
|
-
const stagingRoot = mkdtempSync7(
|
|
19506
|
+
const stagingRoot = mkdtempSync7(join41(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
|
|
18999
19507
|
const digests = /* @__PURE__ */ new Map();
|
|
19000
19508
|
try {
|
|
19001
19509
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -19006,7 +19514,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
19006
19514
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
19007
19515
|
whileAudioMixes: () => putBlobs(
|
|
19008
19516
|
publisher,
|
|
19009
|
-
listFiles(
|
|
19517
|
+
listFiles(join41(stagingRoot, "compiled")).map(
|
|
19010
19518
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
19011
19519
|
)
|
|
19012
19520
|
),
|
|
@@ -19022,7 +19530,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
19022
19530
|
}
|
|
19023
19531
|
throw error;
|
|
19024
19532
|
} finally {
|
|
19025
|
-
|
|
19533
|
+
rmSync15(stagingRoot, { recursive: true, force: true });
|
|
19026
19534
|
}
|
|
19027
19535
|
}
|
|
19028
19536
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -19031,14 +19539,14 @@ async function planV2(projectDir, config, planV2Dir) {
|
|
|
19031
19539
|
}
|
|
19032
19540
|
const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
|
|
19033
19541
|
const manifest = await planV2WithPublisher(projectDir, config, publisher, {
|
|
19034
|
-
stagingParentDir:
|
|
19542
|
+
stagingParentDir: dirname17(planV2Dir)
|
|
19035
19543
|
});
|
|
19036
19544
|
return resultFromManifest(planV2Dir, manifest);
|
|
19037
19545
|
}
|
|
19038
19546
|
function resultFromManifest(planV2Dir, manifest) {
|
|
19039
19547
|
return {
|
|
19040
19548
|
planDir: planV2Dir,
|
|
19041
|
-
manifestPath:
|
|
19549
|
+
manifestPath: join41(planV2Dir, "plan.json"),
|
|
19042
19550
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
19043
19551
|
planHash: manifest.planHash,
|
|
19044
19552
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -19174,7 +19682,7 @@ function parsePlanV2Manifest(value) {
|
|
|
19174
19682
|
return parsed;
|
|
19175
19683
|
}
|
|
19176
19684
|
function readPlanV2Manifest(planV2Dir) {
|
|
19177
|
-
const manifestPath =
|
|
19685
|
+
const manifestPath = join41(planV2Dir, "plan.json");
|
|
19178
19686
|
if (!existsSync26(manifestPath)) {
|
|
19179
19687
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
19180
19688
|
}
|
|
@@ -19197,7 +19705,7 @@ function verifyBlob(planV2Dir, artifact) {
|
|
|
19197
19705
|
if (!existsSync26(sourcePath)) {
|
|
19198
19706
|
throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
|
|
19199
19707
|
}
|
|
19200
|
-
const stats =
|
|
19708
|
+
const stats = statSync12(sourcePath);
|
|
19201
19709
|
if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
|
|
19202
19710
|
throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
|
|
19203
19711
|
}
|
|
@@ -19219,17 +19727,17 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19219
19727
|
artifact,
|
|
19220
19728
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
19221
19729
|
}));
|
|
19222
|
-
|
|
19223
|
-
const tempDir = mkdtempSync7(
|
|
19730
|
+
mkdirSync17(dirname17(destinationDir), { recursive: true });
|
|
19731
|
+
const tempDir = mkdtempSync7(join41(dirname17(destinationDir), ".plan-v2-materialize-"));
|
|
19224
19732
|
try {
|
|
19225
19733
|
for (const { artifact, sourcePath } of verified) {
|
|
19226
|
-
const destinationPath =
|
|
19227
|
-
|
|
19228
|
-
|
|
19734
|
+
const destinationPath = join41(tempDir, ...artifact.path.split("/"));
|
|
19735
|
+
mkdirSync17(dirname17(destinationPath), { recursive: true });
|
|
19736
|
+
copyFileSync8(sourcePath, destinationPath);
|
|
19229
19737
|
}
|
|
19230
19738
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
19231
|
-
|
|
19232
|
-
|
|
19739
|
+
writeFileSync12(
|
|
19740
|
+
join41(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
19233
19741
|
canonicalJsonStringify({
|
|
19234
19742
|
manifest,
|
|
19235
19743
|
target,
|
|
@@ -19237,9 +19745,9 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19237
19745
|
}),
|
|
19238
19746
|
"utf-8"
|
|
19239
19747
|
);
|
|
19240
|
-
|
|
19748
|
+
renameWithRetry9(tempDir, destinationDir);
|
|
19241
19749
|
} catch (error) {
|
|
19242
|
-
|
|
19750
|
+
rmSync15(tempDir, { recursive: true, force: true });
|
|
19243
19751
|
throw error;
|
|
19244
19752
|
}
|
|
19245
19753
|
return {
|
|
@@ -19252,7 +19760,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19252
19760
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
19253
19761
|
// before the container change carries the legacy name, and materializing it
|
|
19254
19762
|
// under the new one would point at a file that was never written.
|
|
19255
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
19763
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join41(destinationDir, audioArtifact.path) : null : null
|
|
19256
19764
|
};
|
|
19257
19765
|
}
|
|
19258
19766
|
function assertRemoteSources(remoteSources, artifacts) {
|
|
@@ -19271,7 +19779,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
19271
19779
|
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
19272
19780
|
}
|
|
19273
19781
|
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
19274
|
-
const markerPath =
|
|
19782
|
+
const markerPath = join41(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
19275
19783
|
if (!existsSync26(markerPath)) return null;
|
|
19276
19784
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
19277
19785
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
@@ -19289,8 +19797,8 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
19289
19797
|
assertRemoteSources(remoteSources, required);
|
|
19290
19798
|
for (const artifact of required) {
|
|
19291
19799
|
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
19292
|
-
const path =
|
|
19293
|
-
if (!existsSync26(path) ||
|
|
19800
|
+
const path = join41(planDir, ...artifact.path.split("/"));
|
|
19801
|
+
if (!existsSync26(path) || statSync12(path).size !== artifact.sizeBytes) {
|
|
19294
19802
|
throw new PlanV2IntegrityError(
|
|
19295
19803
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
19296
19804
|
);
|
|
@@ -19303,33 +19811,34 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
19303
19811
|
}
|
|
19304
19812
|
|
|
19305
19813
|
// src/services/distributed/planV2Execution.ts
|
|
19306
|
-
import { mkdtempSync as mkdtempSync9, rmSync as
|
|
19814
|
+
import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
|
|
19307
19815
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
19308
|
-
import { join as
|
|
19816
|
+
import { join as join44 } from "node:path";
|
|
19309
19817
|
|
|
19310
19818
|
// src/services/distributed/assemble.ts
|
|
19311
19819
|
import {
|
|
19312
19820
|
cpSync as cpSync3,
|
|
19313
19821
|
existsSync as existsSync27,
|
|
19314
|
-
mkdirSync as
|
|
19822
|
+
mkdirSync as mkdirSync18,
|
|
19315
19823
|
mkdtempSync as mkdtempSync8,
|
|
19316
|
-
readFileSync as
|
|
19824
|
+
readFileSync as readFileSync15,
|
|
19317
19825
|
readdirSync as readdirSync10,
|
|
19318
|
-
rmSync as
|
|
19319
|
-
statSync as
|
|
19320
|
-
writeFileSync as
|
|
19826
|
+
rmSync as rmSync16,
|
|
19827
|
+
statSync as statSync13,
|
|
19828
|
+
writeFileSync as writeFileSync13
|
|
19321
19829
|
} from "node:fs";
|
|
19322
|
-
import { dirname as
|
|
19830
|
+
import { dirname as dirname18, join as join42 } from "node:path";
|
|
19323
19831
|
import {
|
|
19832
|
+
appendRateCapArgs as appendRateCapArgs2,
|
|
19324
19833
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
19325
19834
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
19326
19835
|
getEncoderPreset as getEncoderPreset5,
|
|
19327
19836
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
19328
19837
|
muxVideoWithAudio as muxVideoWithAudio2,
|
|
19329
19838
|
resolveConfig as resolveConfig6,
|
|
19330
|
-
runFfmpeg as
|
|
19839
|
+
runFfmpeg as runFfmpeg8,
|
|
19331
19840
|
appendSoftwareEncoderThreadsArg,
|
|
19332
|
-
renameWithRetry as
|
|
19841
|
+
renameWithRetry as renameWithRetry10
|
|
19333
19842
|
} from "@hyperframes/engine";
|
|
19334
19843
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
19335
19844
|
function cfrRateControlArgs(locked) {
|
|
@@ -19355,13 +19864,13 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
19355
19864
|
outputPath,
|
|
19356
19865
|
durationMs: Date.now() - start,
|
|
19357
19866
|
framesEncoded: plan2.totalFrames,
|
|
19358
|
-
fileSize:
|
|
19867
|
+
fileSize: statSync13(outputPath).size
|
|
19359
19868
|
};
|
|
19360
19869
|
}
|
|
19361
19870
|
const format = plan2.dimensions.format;
|
|
19362
19871
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
19363
|
-
if (!existsSync27(
|
|
19364
|
-
|
|
19872
|
+
if (!existsSync27(dirname18(outputPath))) {
|
|
19873
|
+
mkdirSync18(dirname18(outputPath), { recursive: true });
|
|
19365
19874
|
}
|
|
19366
19875
|
const workDir = mkdtempSync8(`${outputPath}.assemble-work-`);
|
|
19367
19876
|
const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
@@ -19383,10 +19892,10 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
19383
19892
|
};
|
|
19384
19893
|
try {
|
|
19385
19894
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
19386
|
-
const finalPath =
|
|
19895
|
+
const finalPath = join42(workDir, `output.${format}`);
|
|
19387
19896
|
const audio = audioPath !== null && existsSync27(audioPath) ? audioPath : null;
|
|
19388
19897
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
19389
|
-
|
|
19898
|
+
renameWithRetry10(finalPath, outputPath);
|
|
19390
19899
|
log.info("[assemble] stage timings", {
|
|
19391
19900
|
...stageMs,
|
|
19392
19901
|
chunks: chunkPaths.length,
|
|
@@ -19394,7 +19903,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
19394
19903
|
});
|
|
19395
19904
|
} finally {
|
|
19396
19905
|
try {
|
|
19397
|
-
|
|
19906
|
+
rmSync16(workDir, { recursive: true, force: true });
|
|
19398
19907
|
} catch (err) {
|
|
19399
19908
|
log.warn("[assemble] failed to remove work dir", {
|
|
19400
19909
|
workDir,
|
|
@@ -19402,7 +19911,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
19402
19911
|
});
|
|
19403
19912
|
}
|
|
19404
19913
|
}
|
|
19405
|
-
const fileSize = existsSync27(outputPath) ?
|
|
19914
|
+
const fileSize = existsSync27(outputPath) ? statSync13(outputPath).size : 0;
|
|
19406
19915
|
return {
|
|
19407
19916
|
outputPath,
|
|
19408
19917
|
durationMs: Date.now() - start,
|
|
@@ -19411,18 +19920,18 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
19411
19920
|
};
|
|
19412
19921
|
}
|
|
19413
19922
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
19414
|
-
const planJsonPath =
|
|
19415
|
-
const chunksJsonPath =
|
|
19923
|
+
const planJsonPath = join42(planDir, "plan.json");
|
|
19924
|
+
const chunksJsonPath = join42(planDir, "meta", "chunks.json");
|
|
19416
19925
|
if (!existsSync27(planJsonPath)) {
|
|
19417
19926
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
19418
19927
|
}
|
|
19419
|
-
const plan2 = JSON.parse(
|
|
19928
|
+
const plan2 = JSON.parse(readFileSync15(planJsonPath, "utf-8"));
|
|
19420
19929
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
|
|
19421
19930
|
validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
|
|
19422
19931
|
if (!existsSync27(chunksJsonPath)) {
|
|
19423
19932
|
throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
|
|
19424
19933
|
}
|
|
19425
|
-
const chunks = JSON.parse(
|
|
19934
|
+
const chunks = JSON.parse(readFileSync15(chunksJsonPath, "utf-8"));
|
|
19426
19935
|
if (chunkPaths.length !== chunks.length) {
|
|
19427
19936
|
throw new Error(
|
|
19428
19937
|
`[assemble] chunkPaths length (${chunkPaths.length}) does not match chunks.json length (${chunks.length}). Adapters must pass one path per chunk, ordered by index.`
|
|
@@ -19456,11 +19965,14 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
19456
19965
|
"-r",
|
|
19457
19966
|
fpsArg
|
|
19458
19967
|
];
|
|
19968
|
+
if (encoder.keyframeIntervalFrames !== void 0)
|
|
19969
|
+
args.push("-g", String(encoder.keyframeIntervalFrames));
|
|
19970
|
+
appendRateCapArgs2(args, encoder);
|
|
19459
19971
|
appendSoftwareEncoderThreadsArg(args, "libx264");
|
|
19460
19972
|
return args;
|
|
19461
19973
|
}
|
|
19462
19974
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
19463
|
-
const cfrOutputPath =
|
|
19975
|
+
const cfrOutputPath = join42(ctx.workDir, `cfr.${ctx.format}`);
|
|
19464
19976
|
const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
|
|
19465
19977
|
appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
|
|
19466
19978
|
cfrArgs.push("-y", cfrOutputPath);
|
|
@@ -19487,7 +19999,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
19487
19999
|
);
|
|
19488
20000
|
return;
|
|
19489
20001
|
}
|
|
19490
|
-
const paddedAudioPath =
|
|
20002
|
+
const paddedAudioPath = join42(ctx.workDir, "audio-padded.m4a");
|
|
19491
20003
|
const padTrimResult = await ctx.timed(
|
|
19492
20004
|
"audioMs",
|
|
19493
20005
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -19527,9 +20039,9 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
19527
20039
|
}
|
|
19528
20040
|
}
|
|
19529
20041
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
19530
|
-
const listPath =
|
|
20042
|
+
const listPath = join42(workDir, "concat-list.txt");
|
|
19531
20043
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
19532
|
-
|
|
20044
|
+
writeFileSync13(listPath, `${body}
|
|
19533
20045
|
`, "utf-8");
|
|
19534
20046
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
19535
20047
|
}
|
|
@@ -19544,7 +20056,7 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
19544
20056
|
};
|
|
19545
20057
|
}
|
|
19546
20058
|
async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
19547
|
-
const result = await
|
|
20059
|
+
const result = await runFfmpeg8(args, { signal, timeout });
|
|
19548
20060
|
if (!result.success) {
|
|
19549
20061
|
throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
|
|
19550
20062
|
error: describeFfmpegFailure4(result, timeout),
|
|
@@ -19565,13 +20077,13 @@ function stitchesSeparateAudio(planDir) {
|
|
|
19565
20077
|
return lockedAudio(planDir).separateAudio === true;
|
|
19566
20078
|
}
|
|
19567
20079
|
function planFormat(planDir) {
|
|
19568
|
-
const plan2 = JSON.parse(
|
|
20080
|
+
const plan2 = JSON.parse(readFileSync15(join42(planDir, "plan.json"), "utf-8"));
|
|
19569
20081
|
return plan2.dimensions.format;
|
|
19570
20082
|
}
|
|
19571
20083
|
function lockedAudio(planDir) {
|
|
19572
|
-
const encoderJsonPath =
|
|
20084
|
+
const encoderJsonPath = join42(planDir, "meta", "encoder.json");
|
|
19573
20085
|
if (!existsSync27(encoderJsonPath)) return {};
|
|
19574
|
-
return JSON.parse(
|
|
20086
|
+
return JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
|
|
19575
20087
|
}
|
|
19576
20088
|
function readCfrEncoder(planDir, format) {
|
|
19577
20089
|
if (format !== "mp4") {
|
|
@@ -19579,11 +20091,11 @@ function readCfrEncoder(planDir, format) {
|
|
|
19579
20091
|
`[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.`
|
|
19580
20092
|
);
|
|
19581
20093
|
}
|
|
19582
|
-
const encoderJsonPath =
|
|
20094
|
+
const encoderJsonPath = join42(planDir, "meta", "encoder.json");
|
|
19583
20095
|
if (!existsSync27(encoderJsonPath)) {
|
|
19584
20096
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
19585
20097
|
}
|
|
19586
|
-
const encoderJson = JSON.parse(
|
|
20098
|
+
const encoderJson = JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
|
|
19587
20099
|
if (encoderJson.encoder === "libx265-software") {
|
|
19588
20100
|
throw new Error(
|
|
19589
20101
|
`[assemble] cfr=true is not yet supported with codec: "h265". The cfr re-encode pass uses libx264 and would silently transcode the h265 chunks. Either disable cfr or render with codec: "h264".`
|
|
@@ -19592,11 +20104,11 @@ function readCfrEncoder(planDir, format) {
|
|
|
19592
20104
|
return encoderJson;
|
|
19593
20105
|
}
|
|
19594
20106
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
19595
|
-
if (existsSync27(outputPath))
|
|
19596
|
-
|
|
20107
|
+
if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
|
|
20108
|
+
mkdirSync18(outputPath, { recursive: true });
|
|
19597
20109
|
let globalIdx = 0;
|
|
19598
20110
|
for (const chunkDir of chunkPaths) {
|
|
19599
|
-
if (!
|
|
20111
|
+
if (!statSync13(chunkDir).isDirectory()) {
|
|
19600
20112
|
throw new Error(
|
|
19601
20113
|
`[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
|
|
19602
20114
|
);
|
|
@@ -19606,8 +20118,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
19606
20118
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
19607
20119
|
}
|
|
19608
20120
|
for (const frame of frames) {
|
|
19609
|
-
const dst =
|
|
19610
|
-
cpSync3(
|
|
20121
|
+
const dst = join42(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
20122
|
+
cpSync3(join42(chunkDir, frame), dst);
|
|
19611
20123
|
globalIdx += 1;
|
|
19612
20124
|
}
|
|
19613
20125
|
}
|
|
@@ -19617,13 +20129,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
19617
20129
|
);
|
|
19618
20130
|
}
|
|
19619
20131
|
if (audioPath !== null && existsSync27(audioPath)) {
|
|
19620
|
-
const sidecar =
|
|
20132
|
+
const sidecar = join42(outputPath, MIXED_AUDIO_FILENAME4);
|
|
19621
20133
|
cpSync3(audioPath, sidecar);
|
|
19622
20134
|
}
|
|
19623
20135
|
let fileSize = 0;
|
|
19624
20136
|
for (const name of readdirSync10(outputPath)) {
|
|
19625
20137
|
try {
|
|
19626
|
-
fileSize +=
|
|
20138
|
+
fileSize += statSync13(join42(outputPath, name)).size;
|
|
19627
20139
|
} catch {
|
|
19628
20140
|
}
|
|
19629
20141
|
}
|
|
@@ -19637,9 +20149,9 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
19637
20149
|
|
|
19638
20150
|
// src/services/distributed/renderChunk.ts
|
|
19639
20151
|
import { randomBytes } from "node:crypto";
|
|
19640
|
-
import { existsSync as existsSync28, mkdirSync as
|
|
20152
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync16, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync14 } from "node:fs";
|
|
19641
20153
|
import { cpus as cpus3 } from "node:os";
|
|
19642
|
-
import { join as
|
|
20154
|
+
import { join as join43 } from "node:path";
|
|
19643
20155
|
import {
|
|
19644
20156
|
assertSwiftShader,
|
|
19645
20157
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -19758,10 +20270,10 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
|
|
|
19758
20270
|
return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
|
|
19759
20271
|
}
|
|
19760
20272
|
function hashChunkOutput(outputPath, kind) {
|
|
19761
|
-
if (kind === "file") return sha256Hex(
|
|
20273
|
+
if (kind === "file") return sha256Hex(readFileSync16(outputPath));
|
|
19762
20274
|
const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
19763
20275
|
const lines = entries.map(
|
|
19764
|
-
(name) => `${name}\0${sha256Hex(
|
|
20276
|
+
(name) => `${name}\0${sha256Hex(readFileSync16(join43(outputPath, name)))}`
|
|
19765
20277
|
);
|
|
19766
20278
|
return sha256Hex(lines.join("\0"));
|
|
19767
20279
|
}
|
|
@@ -19778,16 +20290,16 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
19778
20290
|
async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
19779
20291
|
const start = Date.now();
|
|
19780
20292
|
const log = defaultLogger;
|
|
19781
|
-
const planJsonPath =
|
|
19782
|
-
const encoderJsonPath =
|
|
19783
|
-
const chunksJsonPath =
|
|
20293
|
+
const planJsonPath = join43(planDir, "plan.json");
|
|
20294
|
+
const encoderJsonPath = join43(planDir, "meta", "encoder.json");
|
|
20295
|
+
const chunksJsonPath = join43(planDir, "meta", "chunks.json");
|
|
19784
20296
|
if (!existsSync28(planJsonPath)) {
|
|
19785
20297
|
throw new RenderChunkValidationError(
|
|
19786
20298
|
MISSING_PLAN_ARTIFACT,
|
|
19787
20299
|
`[renderChunk] planDir is missing required artifact: ${planJsonPath}`
|
|
19788
20300
|
);
|
|
19789
20301
|
}
|
|
19790
|
-
const plan2 = JSON.parse(
|
|
20302
|
+
const plan2 = JSON.parse(readFileSync16(planJsonPath, "utf-8"));
|
|
19791
20303
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk);
|
|
19792
20304
|
const planHashStarted = Date.now();
|
|
19793
20305
|
const v2Materialization = readValidatedPlanV2Materialization(planDir, {
|
|
@@ -19804,13 +20316,13 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19804
20316
|
);
|
|
19805
20317
|
}
|
|
19806
20318
|
}
|
|
19807
|
-
const encoder = JSON.parse(
|
|
19808
|
-
const chunks = JSON.parse(
|
|
19809
|
-
const videosJsonPath =
|
|
20319
|
+
const encoder = JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
|
|
20320
|
+
const chunks = JSON.parse(readFileSync16(chunksJsonPath, "utf-8"));
|
|
20321
|
+
const videosJsonPath = join43(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
19810
20322
|
let planVideos = null;
|
|
19811
20323
|
if (existsSync28(videosJsonPath)) {
|
|
19812
20324
|
try {
|
|
19813
|
-
planVideos = validatePlanVideosForChunk(JSON.parse(
|
|
20325
|
+
planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync16(videosJsonPath, "utf-8")));
|
|
19814
20326
|
} catch (err) {
|
|
19815
20327
|
if (err instanceof RenderChunkValidationError) throw err;
|
|
19816
20328
|
throw new RenderChunkValidationError(
|
|
@@ -19838,7 +20350,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19838
20350
|
}
|
|
19839
20351
|
const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
|
|
19840
20352
|
const framesInChunk = range.endFrame - range.startFrame;
|
|
19841
|
-
const compiledDir =
|
|
20353
|
+
const compiledDir = join43(planDir, "compiled");
|
|
19842
20354
|
if (!existsSync28(compiledDir)) {
|
|
19843
20355
|
throw new RenderChunkValidationError(
|
|
19844
20356
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -19895,9 +20407,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19895
20407
|
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
|
|
19896
20408
|
};
|
|
19897
20409
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
19898
|
-
|
|
19899
|
-
const framesDir =
|
|
19900
|
-
|
|
20410
|
+
mkdirSync19(workDir, { recursive: true });
|
|
20411
|
+
const framesDir = join43(workDir, "captured-frames");
|
|
20412
|
+
mkdirSync19(framesDir, { recursive: true });
|
|
19901
20413
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
19902
20414
|
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
19903
20415
|
const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
@@ -19921,7 +20433,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19921
20433
|
dimensions: plan2.dimensions,
|
|
19922
20434
|
encoder,
|
|
19923
20435
|
videoCount: planVideos?.videos.length ?? 0,
|
|
19924
|
-
html:
|
|
20436
|
+
html: readFileSync16(join43(compiledDir, "index.html"), "utf-8"),
|
|
19925
20437
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
19926
20438
|
skipReadinessVideoIds: deferredVideos.filter(
|
|
19927
20439
|
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
@@ -20050,10 +20562,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20050
20562
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
20051
20563
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
20052
20564
|
if (isPngSequence) {
|
|
20053
|
-
if (!existsSync28(outputChunkPath))
|
|
20565
|
+
if (!existsSync28(outputChunkPath)) mkdirSync19(outputChunkPath, { recursive: true });
|
|
20054
20566
|
} else {
|
|
20055
|
-
const outDir =
|
|
20056
|
-
if (!existsSync28(outDir))
|
|
20567
|
+
const outDir = join43(outputChunkPath, "..");
|
|
20568
|
+
if (!existsSync28(outDir)) mkdirSync19(outDir, { recursive: true });
|
|
20057
20569
|
}
|
|
20058
20570
|
const encodeInput = {
|
|
20059
20571
|
job,
|
|
@@ -20092,7 +20604,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20092
20604
|
// first frame is an IDR keyframe and concat-copy at assemble time
|
|
20093
20605
|
// round-trips losslessly.
|
|
20094
20606
|
lockGopForChunkConcat: !isPngSequence,
|
|
20095
|
-
gopSize: framesInChunk
|
|
20607
|
+
gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
|
|
20608
|
+
maxBitrate: encoder.maxBitrate,
|
|
20609
|
+
bufferSize: encoder.bufferSize
|
|
20096
20610
|
};
|
|
20097
20611
|
const captureStarted = Date.now();
|
|
20098
20612
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
@@ -20164,8 +20678,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20164
20678
|
return observedChunkCaptureMode(capturePerfs);
|
|
20165
20679
|
},
|
|
20166
20680
|
resetForScreenshotRetry: () => {
|
|
20167
|
-
|
|
20168
|
-
|
|
20681
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20682
|
+
mkdirSync19(framesDir, { recursive: true });
|
|
20169
20683
|
capturePerfs.length = 0;
|
|
20170
20684
|
captureAttempts.length = 0;
|
|
20171
20685
|
job.framesRendered = 0;
|
|
@@ -20207,8 +20721,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20207
20721
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
20208
20722
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
20209
20723
|
});
|
|
20210
|
-
|
|
20211
|
-
|
|
20724
|
+
rmSync17(videoOnlyPath, { force: true });
|
|
20725
|
+
rmSync17(audioScratchPath, { force: true });
|
|
20212
20726
|
}
|
|
20213
20727
|
encodeStageMs = Date.now() - encodeStarted;
|
|
20214
20728
|
} finally {
|
|
@@ -20255,11 +20769,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20255
20769
|
producerVersion: plan2.producerVersion,
|
|
20256
20770
|
ffmpegVersion
|
|
20257
20771
|
};
|
|
20258
|
-
|
|
20772
|
+
writeFileSync14(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
20259
20773
|
`, "utf-8");
|
|
20260
20774
|
try {
|
|
20261
|
-
|
|
20262
|
-
|
|
20775
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20776
|
+
rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20263
20777
|
} catch (err) {
|
|
20264
20778
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
20265
20779
|
workDir,
|
|
@@ -20288,18 +20802,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
20288
20802
|
|
|
20289
20803
|
// src/services/distributed/planV2Execution.ts
|
|
20290
20804
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
20291
|
-
const workRoot = mkdtempSync9(
|
|
20292
|
-
const materializedPlanDir =
|
|
20805
|
+
const workRoot = mkdtempSync9(join44(tmpdir5(), "hf-plan-v2-chunk-"));
|
|
20806
|
+
const materializedPlanDir = join44(workRoot, "plan");
|
|
20293
20807
|
try {
|
|
20294
20808
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
20295
20809
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
20296
20810
|
} finally {
|
|
20297
|
-
|
|
20811
|
+
rmSync18(workRoot, { recursive: true, force: true });
|
|
20298
20812
|
}
|
|
20299
20813
|
}
|
|
20300
20814
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
20301
|
-
const workRoot = mkdtempSync9(
|
|
20302
|
-
const materializedPlanDir =
|
|
20815
|
+
const workRoot = mkdtempSync9(join44(tmpdir5(), "hf-plan-v2-assembler-"));
|
|
20816
|
+
const materializedPlanDir = join44(workRoot, "plan");
|
|
20303
20817
|
try {
|
|
20304
20818
|
const materialized = materializePlanV2Target(
|
|
20305
20819
|
planV2Dir,
|
|
@@ -20314,7 +20828,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
20314
20828
|
options
|
|
20315
20829
|
);
|
|
20316
20830
|
} finally {
|
|
20317
|
-
|
|
20831
|
+
rmSync18(workRoot, { recursive: true, force: true });
|
|
20318
20832
|
}
|
|
20319
20833
|
}
|
|
20320
20834
|
|
|
@@ -20322,7 +20836,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
20322
20836
|
var READ_CHUNK_BYTES = 256 * 1024;
|
|
20323
20837
|
|
|
20324
20838
|
// src/services/distributed/audioSidecar.ts
|
|
20325
|
-
import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as
|
|
20839
|
+
import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg9 } from "@hyperframes/engine";
|
|
20326
20840
|
var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
|
|
20327
20841
|
|
|
20328
20842
|
// src/services/distributed/probeFacts.ts
|
|
@@ -20386,6 +20900,7 @@ export {
|
|
|
20386
20900
|
localizeRemoteImageSources,
|
|
20387
20901
|
localizeRemoteMediaSources,
|
|
20388
20902
|
materializePlanV2Target,
|
|
20903
|
+
mediaProfileMatchesElementType,
|
|
20389
20904
|
normalizeErrorMessage,
|
|
20390
20905
|
parseRenderRequest,
|
|
20391
20906
|
plan,
|
|
@@ -20393,6 +20908,7 @@ export {
|
|
|
20393
20908
|
planV2WithPublisher,
|
|
20394
20909
|
prepareAnimatedGifInputs,
|
|
20395
20910
|
prepareCaptureSessionForReuse4 as prepareCaptureSessionForReuse,
|
|
20911
|
+
prepareHeifImageInputs,
|
|
20396
20912
|
prepareHyperframeLintBody,
|
|
20397
20913
|
publishPlanV2FromExecutionPlan,
|
|
20398
20914
|
publishPlanV2FromV1,
|