@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/public-server.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import {
|
|
5
5
|
existsSync as existsSync19,
|
|
6
|
-
mkdirSync as
|
|
7
|
-
statSync as
|
|
6
|
+
mkdirSync as mkdirSync11,
|
|
7
|
+
statSync as statSync10,
|
|
8
8
|
mkdtempSync as mkdtempSync4,
|
|
9
|
-
writeFileSync as
|
|
10
|
-
rmSync as
|
|
9
|
+
writeFileSync as writeFileSync7,
|
|
10
|
+
rmSync as rmSync11,
|
|
11
11
|
createReadStream as createReadStream3
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { resolve as
|
|
13
|
+
import { resolve as resolve10, dirname as dirname13, join as join31 } from "node:path";
|
|
14
14
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
16
|
import crypto from "node:crypto";
|
|
@@ -23,19 +23,19 @@ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engi
|
|
|
23
23
|
import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
|
|
24
24
|
import {
|
|
25
25
|
existsSync as existsSync16,
|
|
26
|
-
mkdirSync as
|
|
27
|
-
readFileSync as
|
|
26
|
+
mkdirSync as mkdirSync10,
|
|
27
|
+
readFileSync as readFileSync9,
|
|
28
28
|
readdirSync as readdirSync5,
|
|
29
|
-
rmSync as
|
|
30
|
-
statSync as
|
|
31
|
-
writeFileSync as
|
|
32
|
-
copyFileSync as
|
|
29
|
+
rmSync as rmSync10,
|
|
30
|
+
statSync as statSync8,
|
|
31
|
+
writeFileSync as writeFileSync6,
|
|
32
|
+
copyFileSync as copyFileSync5,
|
|
33
33
|
appendFileSync
|
|
34
34
|
} from "fs";
|
|
35
35
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
36
36
|
import {
|
|
37
37
|
chainCapture,
|
|
38
|
-
fpsToNumber as
|
|
38
|
+
fpsToNumber as fpsToNumber8,
|
|
39
39
|
HF_COLOR_GRADING_ATTR,
|
|
40
40
|
HF_VFX_ATTR,
|
|
41
41
|
parseVfxChain,
|
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
augmentProtocolTimeoutError,
|
|
78
78
|
augmentPageNavigationTimeoutError
|
|
79
79
|
} from "@hyperframes/engine";
|
|
80
|
-
import { join as
|
|
80
|
+
import { join as join27, dirname as dirname11, resolve as resolve8 } from "path";
|
|
81
81
|
import { totalmem as totalmem2 } from "node:os";
|
|
82
82
|
import { randomUUID } from "crypto";
|
|
83
83
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -101,6 +101,7 @@ import { Readable } from "node:stream";
|
|
|
101
101
|
import { join, extname, resolve as resolve2, sep } from "node:path";
|
|
102
102
|
import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
|
|
103
103
|
import { fpsToNumber } from "@hyperframes/core";
|
|
104
|
+
import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
|
|
104
105
|
|
|
105
106
|
// src/services/hyperframeRuntimeLoader.ts
|
|
106
107
|
import { createHash } from "node:crypto";
|
|
@@ -217,11 +218,11 @@ var defaultLogger = createConsoleLogger("info");
|
|
|
217
218
|
|
|
218
219
|
// src/services/fileServer.ts
|
|
219
220
|
var readFileAsync = promisify(readFile);
|
|
220
|
-
function isPathInside(
|
|
221
|
+
function isPathInside(child2, parent, options = {}) {
|
|
221
222
|
const { resolveSymlinks = false, pathModule } = options;
|
|
222
223
|
const resolveFn = pathModule?.resolve ?? resolve2;
|
|
223
224
|
const separator = pathModule?.sep ?? sep;
|
|
224
|
-
const resolvedChild = resolveFn(
|
|
225
|
+
const resolvedChild = resolveFn(child2);
|
|
225
226
|
const resolvedParent = resolveFn(parent);
|
|
226
227
|
const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
|
|
227
228
|
const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
|
|
@@ -741,6 +742,18 @@ function createFileServer(options) {
|
|
|
741
742
|
html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
|
|
742
743
|
return c.text(html, 200, { "Content-Type": contentType });
|
|
743
744
|
}
|
|
745
|
+
if (ext === ".png") {
|
|
746
|
+
const untagged = stripSdrPngColourTags(await readFileAsync(fd));
|
|
747
|
+
if (untagged) {
|
|
748
|
+
const headers = {
|
|
749
|
+
"Content-Type": contentType,
|
|
750
|
+
"Content-Length": String(untagged.length)
|
|
751
|
+
};
|
|
752
|
+
return new Response(c.req.method === "HEAD" ? null : new Uint8Array(untagged), {
|
|
753
|
+
headers
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
}
|
|
744
757
|
const totalSize = stat.size;
|
|
745
758
|
const rangeHeader = c.req.header("range");
|
|
746
759
|
const rangeRequest = parseRangeHeader(rangeHeader, totalSize);
|
|
@@ -768,7 +781,8 @@ function createFileServer(options) {
|
|
|
768
781
|
const stream = createReadStream(filePath, {
|
|
769
782
|
fd,
|
|
770
783
|
autoClose: true,
|
|
771
|
-
|
|
784
|
+
start: range?.start ?? 0,
|
|
785
|
+
...range ? { end: range.end } : {}
|
|
772
786
|
});
|
|
773
787
|
streamOwnsFd = true;
|
|
774
788
|
try {
|
|
@@ -782,10 +796,10 @@ function createFileServer(options) {
|
|
|
782
796
|
if (!streamOwnsFd) closeSync(fd);
|
|
783
797
|
}
|
|
784
798
|
});
|
|
785
|
-
return new Promise((
|
|
799
|
+
return new Promise((resolve11) => {
|
|
786
800
|
const connections = /* @__PURE__ */ new Set();
|
|
787
801
|
const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
|
|
788
|
-
|
|
802
|
+
resolve11({
|
|
789
803
|
url: `http://localhost:${info.port}`,
|
|
790
804
|
port: info.port,
|
|
791
805
|
addPreHeadScript: (script) => {
|
|
@@ -817,7 +831,7 @@ function outputUsesH264Pipeline(format) {
|
|
|
817
831
|
}
|
|
818
832
|
|
|
819
833
|
// src/services/render/hlsConfig.ts
|
|
820
|
-
import {
|
|
834
|
+
import { keyframeIntervalToGopSize } from "@hyperframes/engine";
|
|
821
835
|
var DEFAULT_HLS_SEGMENT_SECONDS = 4;
|
|
822
836
|
function validateHlsRenderConfig(config) {
|
|
823
837
|
if (config.format !== "hls") return;
|
|
@@ -842,7 +856,7 @@ function resolveHlsSegmentSeconds(config) {
|
|
|
842
856
|
return config.hlsSegmentSeconds ?? DEFAULT_HLS_SEGMENT_SECONDS;
|
|
843
857
|
}
|
|
844
858
|
function resolveHlsGopSize(fps, segmentSeconds) {
|
|
845
|
-
return
|
|
859
|
+
return keyframeIntervalToGopSize(fps, segmentSeconds);
|
|
846
860
|
}
|
|
847
861
|
function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
848
862
|
if (outputFormat !== "hls") return { lockGopForChunkConcat: false, gopSize: void 0 };
|
|
@@ -852,6 +866,38 @@ function resolveHlsEncoderGopLock(outputFormat, fps, segmentSeconds) {
|
|
|
852
866
|
};
|
|
853
867
|
}
|
|
854
868
|
|
|
869
|
+
// src/services/render/deliveryEncoder.ts
|
|
870
|
+
import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
|
|
871
|
+
function validateDeliveryEncoderConfig(config) {
|
|
872
|
+
const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
|
|
873
|
+
if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
|
|
874
|
+
return;
|
|
875
|
+
if (keyframeIntervalSeconds !== void 0) {
|
|
876
|
+
if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
if (format === "hls") {
|
|
882
|
+
throw new Error(
|
|
883
|
+
"keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (format !== void 0 && format !== "mp4") {
|
|
888
|
+
throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
|
|
889
|
+
}
|
|
890
|
+
appendRateCapArgs([], { maxBitrate, bufferSize });
|
|
891
|
+
}
|
|
892
|
+
function resolveDeliveryEncoderOptions(config) {
|
|
893
|
+
const { maxBitrate, bufferSize, keyframeIntervalSeconds } = config;
|
|
894
|
+
const gop = keyframeIntervalSeconds === void 0 ? resolveHlsEncoderGopLock(config.format ?? "mp4", config.fps, config.hlsSegmentSeconds) : {
|
|
895
|
+
lockGopForChunkConcat: true,
|
|
896
|
+
gopSize: keyframeIntervalToGopSize2(config.fps, keyframeIntervalSeconds)
|
|
897
|
+
};
|
|
898
|
+
return { maxBitrate, bufferSize, ...gop };
|
|
899
|
+
}
|
|
900
|
+
|
|
855
901
|
// src/services/render/shared.ts
|
|
856
902
|
import {
|
|
857
903
|
copyFileSync,
|
|
@@ -1292,8 +1338,8 @@ var OrderedRenderEventPublisher = class {
|
|
|
1292
1338
|
let timer;
|
|
1293
1339
|
const drained = await Promise.race([
|
|
1294
1340
|
this.tail.then(() => true),
|
|
1295
|
-
new Promise((
|
|
1296
|
-
timer = setTimeout(() =>
|
|
1341
|
+
new Promise((resolve11) => {
|
|
1342
|
+
timer = setTimeout(() => resolve11(false), FLUSH_TIMEOUT_MS);
|
|
1297
1343
|
})
|
|
1298
1344
|
]);
|
|
1299
1345
|
clearTimeout(timer);
|
|
@@ -1870,7 +1916,7 @@ function replanSegmentedAfterFailure(plan, failure) {
|
|
|
1870
1916
|
const keepSegmenting = failure.kind === "draw_element_verification" || failure.kind === "draw_element_capture";
|
|
1871
1917
|
return createCapturePlan({
|
|
1872
1918
|
...plan,
|
|
1873
|
-
forceScreenshot: keepSegmenting || failure.kind === "capture_failure",
|
|
1919
|
+
forceScreenshot: plan.forceScreenshot || keepSegmenting || failure.kind === "capture_failure",
|
|
1874
1920
|
useStreamingEncode: true,
|
|
1875
1921
|
useLayeredComposite: false,
|
|
1876
1922
|
useSegmentedCapture: keepSegmenting,
|
|
@@ -1940,7 +1986,7 @@ import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
|
1940
1986
|
|
|
1941
1987
|
// src/services/render/perfSummary.ts
|
|
1942
1988
|
import { arch, cpus, platform, totalmem } from "node:os";
|
|
1943
|
-
import { fpsToNumber as
|
|
1989
|
+
import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
|
|
1944
1990
|
|
|
1945
1991
|
// src/services/render/hdrPerf.ts
|
|
1946
1992
|
function createHdrPerfCollector() {
|
|
@@ -2211,7 +2257,7 @@ function buildRenderPerfSummary(input) {
|
|
|
2211
2257
|
// payloads (CLI telemetry, regression-harness reports) where a single
|
|
2212
2258
|
// number is friendlier than `{num,den}`. Callers needing the rational
|
|
2213
2259
|
// back can read `job.config.fps`.
|
|
2214
|
-
fps:
|
|
2260
|
+
fps: fpsToNumber2(input.job.config.fps),
|
|
2215
2261
|
quality: input.job.config.quality,
|
|
2216
2262
|
workers: input.workerCount,
|
|
2217
2263
|
workerSizing: input.workerSizing,
|
|
@@ -2796,7 +2842,7 @@ import {
|
|
|
2796
2842
|
resolveVideoExtractionWindow,
|
|
2797
2843
|
runFfmpeg
|
|
2798
2844
|
} from "@hyperframes/engine";
|
|
2799
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
2845
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
2800
2846
|
|
|
2801
2847
|
// src/services/hdrFrameWindows.ts
|
|
2802
2848
|
import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
|
|
@@ -3099,7 +3145,7 @@ async function extractHdrVideoFrames(args) {
|
|
|
3099
3145
|
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
|
|
3100
3146
|
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
|
|
3101
3147
|
const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
|
|
3102
|
-
const fps =
|
|
3148
|
+
const fps = fpsToNumber3(job.config.fps);
|
|
3103
3149
|
const out = /* @__PURE__ */ new Map();
|
|
3104
3150
|
mkdirSync2(framesDir, { recursive: true });
|
|
3105
3151
|
let scratchBytes = 0;
|
|
@@ -3636,7 +3682,7 @@ var SequentialCaptureStallError = class extends Error {
|
|
|
3636
3682
|
}
|
|
3637
3683
|
};
|
|
3638
3684
|
function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
3639
|
-
return new Promise((
|
|
3685
|
+
return new Promise((resolve11, reject) => {
|
|
3640
3686
|
const timer = setTimeout(
|
|
3641
3687
|
() => {
|
|
3642
3688
|
reject(
|
|
@@ -3655,7 +3701,7 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
|
|
|
3655
3701
|
promise.then(
|
|
3656
3702
|
(value) => {
|
|
3657
3703
|
clearTimeout(timer);
|
|
3658
|
-
|
|
3704
|
+
resolve11(value);
|
|
3659
3705
|
},
|
|
3660
3706
|
(error) => {
|
|
3661
3707
|
clearTimeout(timer);
|
|
@@ -3937,7 +3983,6 @@ async function runCaptureStreamingStage(input) {
|
|
|
3937
3983
|
const reorderBuffer = createFrameReorderBuffer(0, totalFrames);
|
|
3938
3984
|
if (workerCount > 1) {
|
|
3939
3985
|
const deParallelStream = forceParallelStream === true || process.env.HF_DE_PARALLEL_STREAM === "true";
|
|
3940
|
-
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames, workerCount, workDir) : distributeFrames(totalFrames, workerCount, workDir);
|
|
3941
3986
|
const parallelStats = {
|
|
3942
3987
|
verifyChecked: 0,
|
|
3943
3988
|
blankSuspects: 0,
|
|
@@ -3950,143 +3995,158 @@ async function runCaptureStreamingStage(input) {
|
|
|
3950
3995
|
frameTime: (i) => i * job.config.fps.den / job.config.fps.num
|
|
3951
3996
|
});
|
|
3952
3997
|
let parallelGuardRan = false;
|
|
3953
|
-
let
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3998
|
+
let writtenFrames = 0;
|
|
3999
|
+
let resumeFrom = 0;
|
|
4000
|
+
let resumedAfterStall = false;
|
|
4001
|
+
let workerResults;
|
|
4002
|
+
for (; ; ) {
|
|
4003
|
+
const parallelReorder = createFrameReorderBuffer(resumeFrom, totalFrames);
|
|
4004
|
+
const tasks = deParallelStream ? distributeFramesInterleaved(totalFrames - resumeFrom, workerCount, workDir, resumeFrom) : distributeFrames(totalFrames - resumeFrom, workerCount, workDir, resumeFrom);
|
|
4005
|
+
let parallelDrainError = null;
|
|
4006
|
+
const onFrameBuffer = async (frameIndex, buffer, workerSession) => {
|
|
4007
|
+
try {
|
|
4008
|
+
if (deParallelStream && workerSession.captureMode === "drawelement") {
|
|
4009
|
+
parallelGuardRan = true;
|
|
4010
|
+
buffer = await parallelGuard(workerSession, frameIndex, buffer);
|
|
4011
|
+
}
|
|
4012
|
+
await parallelReorder.waitForFrame(frameIndex);
|
|
4013
|
+
ensureFrameWritten(await currentEncoder.writeFrame(buffer), frameIndex, currentEncoder);
|
|
4014
|
+
parallelReorder.advanceTo(frameIndex + 1);
|
|
4015
|
+
writtenFrames = frameIndex + 1;
|
|
4016
|
+
} catch (err) {
|
|
4017
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
4018
|
+
if (!parallelDrainError) {
|
|
4019
|
+
parallelDrainError = e;
|
|
4020
|
+
parallelReorder.abort(e);
|
|
4021
|
+
}
|
|
4022
|
+
throw e;
|
|
3968
4023
|
}
|
|
3969
|
-
|
|
4024
|
+
};
|
|
4025
|
+
const captureLabel = input.parallelCaptureLabel ?? "worker";
|
|
4026
|
+
let watchdogArmed = false;
|
|
4027
|
+
const stallController = new AbortController();
|
|
4028
|
+
const forwardParentAbort = () => stallController.abort();
|
|
4029
|
+
if (abortSignal) {
|
|
4030
|
+
if (abortSignal.aborted) stallController.abort();
|
|
4031
|
+
else abortSignal.addEventListener("abort", forwardParentAbort, { once: true });
|
|
3970
4032
|
}
|
|
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
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4033
|
+
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4034
|
+
let lastCapturedFrames = 0;
|
|
4035
|
+
let lastProgressAt = Date.now();
|
|
4036
|
+
const workerPhases = /* @__PURE__ */ new Map();
|
|
4037
|
+
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}`;
|
|
4038
|
+
const phaseSummary = () => [...workerPhases.entries()].sort(([left], [right]) => left - right).map(([workerId, detail]) => `worker=${workerId} ${detail}`).join("; ");
|
|
4039
|
+
let stalled = false;
|
|
4040
|
+
const stallTimer = setInterval(
|
|
4041
|
+
() => {
|
|
4042
|
+
if (!watchdogArmed || Date.now() - lastProgressAt <= stallTimeoutMs) return;
|
|
4043
|
+
stalled = true;
|
|
4044
|
+
const stallErr = new ParallelCaptureStallError({
|
|
4045
|
+
variant: "drain",
|
|
4046
|
+
captureLabel,
|
|
4047
|
+
stallTimeoutMs,
|
|
4048
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4049
|
+
totalFrames,
|
|
4050
|
+
phaseSummary: phaseSummary()
|
|
4051
|
+
});
|
|
4052
|
+
parallelReorder.abort(stallErr);
|
|
4053
|
+
stallController.abort();
|
|
4054
|
+
},
|
|
4055
|
+
Math.min(stallTimeoutMs, DE_STALL_POLL_MS)
|
|
4056
|
+
);
|
|
4057
|
+
try {
|
|
4058
|
+
workerResults = await executeParallelCapture(
|
|
4059
|
+
fileServer.url,
|
|
4060
|
+
workDir,
|
|
4061
|
+
tasks,
|
|
4062
|
+
buildCaptureOptions(),
|
|
4063
|
+
createRenderVideoFrameInjector,
|
|
4064
|
+
stallController.signal,
|
|
4065
|
+
(progress) => {
|
|
4066
|
+
if (progress.latestWorkerPhase) {
|
|
4067
|
+
const phase = progress.latestWorkerPhase;
|
|
4068
|
+
workerPhases.set(phase.workerId, phaseDetail(phase));
|
|
4069
|
+
if (phase.phase === "frame_capture" && !watchdogArmed) {
|
|
4070
|
+
watchdogArmed = true;
|
|
4071
|
+
lastProgressAt = Date.now();
|
|
4072
|
+
}
|
|
4073
|
+
log.info("[Render] Parallel capture worker phase", {
|
|
4074
|
+
workerId: phase.workerId,
|
|
4075
|
+
phase: phase.phase,
|
|
4076
|
+
frameIndex: phase.frameIndex,
|
|
4077
|
+
browserExecutable: phase.browserExecutable,
|
|
4078
|
+
browserVersion: phase.browserVersion,
|
|
4079
|
+
canvasDrawElement: phase.canvasDrawElement,
|
|
4080
|
+
gpuBackend: phase.gpuBackend
|
|
4081
|
+
});
|
|
4082
|
+
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4086
|
+
lastCapturedFrames = progress.capturedFrames;
|
|
4019
4087
|
lastProgressAt = Date.now();
|
|
4088
|
+
watchdogArmed = true;
|
|
4089
|
+
}
|
|
4090
|
+
const doneFrames = resumeFrom + progress.capturedFrames;
|
|
4091
|
+
job.framesRendered = doneFrames;
|
|
4092
|
+
const frameProgress = doneFrames / totalFrames;
|
|
4093
|
+
reportFrameProgress(
|
|
4094
|
+
job,
|
|
4095
|
+
`Streaming frame ${doneFrames}/${totalFrames} (${workerCount} workers)`,
|
|
4096
|
+
Math.round(25 + frameProgress * 55),
|
|
4097
|
+
onProgress,
|
|
4098
|
+
doneFrames,
|
|
4099
|
+
totalFrames
|
|
4100
|
+
);
|
|
4101
|
+
},
|
|
4102
|
+
onFrameBuffer,
|
|
4103
|
+
// Interleaved DE workers each get their own browser process: co-tenant pages starve
|
|
4104
|
+
// each other's paint events.
|
|
4105
|
+
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4106
|
+
{
|
|
4107
|
+
// A worker died: release parked peers with its original failure, not the stall the
|
|
4108
|
+
// watchdog raises later.
|
|
4109
|
+
onWorkerOperation: (phase) => workerPhases.set(phase.workerId, phaseDetail(phase)),
|
|
4110
|
+
onWorkerFailure: (failure) => {
|
|
4111
|
+
if (parallelDrainError) return;
|
|
4112
|
+
parallelDrainError = failure;
|
|
4113
|
+
parallelReorder.abort(failure);
|
|
4020
4114
|
}
|
|
4021
|
-
log.info("[Render] Parallel capture worker phase", {
|
|
4022
|
-
workerId: phase.workerId,
|
|
4023
|
-
phase: phase.phase,
|
|
4024
|
-
frameIndex: phase.frameIndex,
|
|
4025
|
-
browserExecutable: phase.browserExecutable,
|
|
4026
|
-
browserVersion: phase.browserVersion,
|
|
4027
|
-
canvasDrawElement: phase.canvasDrawElement,
|
|
4028
|
-
gpuBackend: phase.gpuBackend
|
|
4029
|
-
});
|
|
4030
|
-
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
4031
|
-
return;
|
|
4032
|
-
}
|
|
4033
|
-
if (progress.capturedFrames > lastCapturedFrames) {
|
|
4034
|
-
lastCapturedFrames = progress.capturedFrames;
|
|
4035
|
-
lastProgressAt = Date.now();
|
|
4036
|
-
watchdogArmed = true;
|
|
4037
4115
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4116
|
+
);
|
|
4117
|
+
} catch (err) {
|
|
4118
|
+
if (stalled && abortSignal?.aborted !== true && captureLabel === "BeginFrame" && !resumedAfterStall && writtenFrames > resumeFrom) {
|
|
4119
|
+
resumedAfterStall = true;
|
|
4120
|
+
log.warn(
|
|
4121
|
+
"[Render] Parallel BeginFrame capture stalled; resuming from the first unwritten frame",
|
|
4122
|
+
{
|
|
4123
|
+
resumeFrom: writtenFrames,
|
|
4124
|
+
totalFrames,
|
|
4125
|
+
workerPhases: phaseSummary()
|
|
4126
|
+
}
|
|
4047
4127
|
);
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
// Interleaved DE workers each need their own browser PROCESS:
|
|
4051
|
-
// pages co-tenant in one browser share one compositor, whose
|
|
4052
|
-
// internal frame scheduling deprioritizes non-active pages — their
|
|
4053
|
-
// canvas `paint` events starve and the drawElement paint-wait slows
|
|
4054
|
-
// ~3x (measured: 86s vs 30s on a 3,245-frame rAF comp). This is
|
|
4055
|
-
// Chromium's internal frame-production scheduling on ALL platforms,
|
|
4056
|
-
// NOT the Linux-only HeadlessExperimental.beginFrame capture mode.
|
|
4057
|
-
deParallelStream ? { ...captureCfg, enableBrowserPool: false } : captureCfg,
|
|
4058
|
-
{
|
|
4059
|
-
// A worker died (Target closed, Page crashed, launch failure).
|
|
4060
|
-
// Its frames are gone and peers are parked in waitForFrame on the
|
|
4061
|
-
// first of them; release them with the ORIGINAL failure so the
|
|
4062
|
-
// orchestrator sees a transient browser error it knows how to
|
|
4063
|
-
// retry, not the stall the watchdog would have raised a minute
|
|
4064
|
-
// later. First failure wins, same as the drain guard above.
|
|
4065
|
-
onWorkerFailure: (failure) => {
|
|
4066
|
-
if (parallelDrainError) return;
|
|
4067
|
-
parallelDrainError = failure;
|
|
4068
|
-
reorderBuffer.abort(failure);
|
|
4069
|
-
}
|
|
4128
|
+
resumeFrom = writtenFrames;
|
|
4129
|
+
continue;
|
|
4070
4130
|
}
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4131
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4132
|
+
if (stalled && abortSignal?.aborted !== true) {
|
|
4133
|
+
throw new ParallelCaptureStallError({
|
|
4134
|
+
variant: "pool",
|
|
4135
|
+
captureLabel,
|
|
4136
|
+
stallTimeoutMs,
|
|
4137
|
+
lastCapturedFrames: resumeFrom + lastCapturedFrames,
|
|
4138
|
+
totalFrames,
|
|
4139
|
+
phaseSummary: phaseSummary()
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
throw poolFailure(err, probeSession);
|
|
4143
|
+
} finally {
|
|
4144
|
+
clearInterval(stallTimer);
|
|
4145
|
+
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4083
4146
|
}
|
|
4084
|
-
throw poolFailure(
|
|
4085
|
-
|
|
4086
|
-
clearInterval(stallTimer);
|
|
4087
|
-
abortSignal?.removeEventListener("abort", forwardParentAbort);
|
|
4147
|
+
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4148
|
+
break;
|
|
4088
4149
|
}
|
|
4089
|
-
if (parallelDrainError) throw poolFailure(parallelDrainError, probeSession);
|
|
4090
4150
|
pushWorkerDedupPerfs(workerResults, dedupPerfs);
|
|
4091
4151
|
const workerPageErrors = [];
|
|
4092
4152
|
pushWorkerPageErrors(workerResults, workerPageErrors);
|
|
@@ -4247,7 +4307,7 @@ function segmentEncoderOptions(base, segment) {
|
|
|
4247
4307
|
return {
|
|
4248
4308
|
...base,
|
|
4249
4309
|
lockGopForChunkConcat: true,
|
|
4250
|
-
gopSize: segment.endFrame - segment.startFrame
|
|
4310
|
+
gopSize: Math.min(base.gopSize ?? Infinity, segment.endFrame - segment.startFrame)
|
|
4251
4311
|
};
|
|
4252
4312
|
}
|
|
4253
4313
|
var SegmentEncoderSpawnError = class extends Error {
|
|
@@ -4341,6 +4401,12 @@ function defaultSessionFactory(input) {
|
|
|
4341
4401
|
let probeSession = input.probeSession;
|
|
4342
4402
|
return {
|
|
4343
4403
|
create: async () => {
|
|
4404
|
+
const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
|
|
4405
|
+
if (probeSession && probeCannotCaptureEveryFrame) {
|
|
4406
|
+
await closeCaptureSession2(probeSession).catch(() => {
|
|
4407
|
+
});
|
|
4408
|
+
probeSession = null;
|
|
4409
|
+
}
|
|
4344
4410
|
const session = probeSession ?? await openSegmentedSession(input);
|
|
4345
4411
|
if (probeSession) {
|
|
4346
4412
|
openSegmentedSessionReuse(input, probeSession);
|
|
@@ -4671,8 +4737,10 @@ function computeSegmentPlanHash(input) {
|
|
|
4671
4737
|
input.motionBlur,
|
|
4672
4738
|
input.pageMotionBlur ?? "",
|
|
4673
4739
|
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4674
|
-
]
|
|
4675
|
-
|
|
4740
|
+
];
|
|
4741
|
+
const delivery = [input.maxBitrate, input.bufferSize, input.gopSize].map((v) => v ?? "");
|
|
4742
|
+
if (delivery.some((v) => v !== "")) ordered.push(...delivery.map(String));
|
|
4743
|
+
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
4676
4744
|
}
|
|
4677
4745
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
4678
4746
|
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
@@ -4819,7 +4887,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
4819
4887
|
|
|
4820
4888
|
// src/services/render/captureCost.ts
|
|
4821
4889
|
import { join as join12 } from "node:path";
|
|
4822
|
-
import { fpsToNumber as
|
|
4890
|
+
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
4823
4891
|
import {
|
|
4824
4892
|
calculateOptimalWorkers,
|
|
4825
4893
|
captureFrameToBuffer as captureFrameToBuffer3,
|
|
@@ -5023,7 +5091,7 @@ async function runCaptureCalibration(input) {
|
|
|
5023
5091
|
let probeSession = input.probeSession;
|
|
5024
5092
|
let forceScreenshot = input.forceScreenshot;
|
|
5025
5093
|
let lastBrowserConsole = [];
|
|
5026
|
-
const fps =
|
|
5094
|
+
const fps = fpsToNumber4(job.config.fps);
|
|
5027
5095
|
const sessionRef = { current: null };
|
|
5028
5096
|
const runOneCalibration = async (sessionDir, sessionCfg) => {
|
|
5029
5097
|
log.info("Launching browser for capture calibration...");
|
|
@@ -5483,7 +5551,7 @@ function emitFallbackCaptureProfile(recorder, perfSummaries, env = process.env)
|
|
|
5483
5551
|
// src/services/render/videoFrameCoverage.ts
|
|
5484
5552
|
import { parseHTML } from "linkedom";
|
|
5485
5553
|
import {
|
|
5486
|
-
fpsToNumber as
|
|
5554
|
+
fpsToNumber as fpsToNumber5,
|
|
5487
5555
|
normalizeRateSpec,
|
|
5488
5556
|
sourceTimeAt,
|
|
5489
5557
|
toFps
|
|
@@ -5518,7 +5586,7 @@ function resolveVideoCoverageThreshold(envValue = process.env.HF_VIDEO_COVERAGE_
|
|
|
5518
5586
|
return parsed;
|
|
5519
5587
|
}
|
|
5520
5588
|
function expectedFramesForClip(start, end, fps, rounding = "ceil") {
|
|
5521
|
-
const fpsValue =
|
|
5589
|
+
const fpsValue = fpsToNumber5(toFps(fps));
|
|
5522
5590
|
if (!Number.isFinite(start) || !Number.isFinite(end) || !Number.isFinite(fpsValue)) return 0;
|
|
5523
5591
|
if (fpsValue <= 0) return 0;
|
|
5524
5592
|
const duration = Math.max(0, end - start);
|
|
@@ -5583,15 +5651,15 @@ function countAuthoredTimedClips(html) {
|
|
|
5583
5651
|
}
|
|
5584
5652
|
|
|
5585
5653
|
// src/services/render/stages/compileStage.ts
|
|
5586
|
-
import { join as
|
|
5654
|
+
import { join as join16 } from "node:path";
|
|
5587
5655
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
5588
5656
|
|
|
5589
5657
|
// src/services/htmlCompiler.ts
|
|
5590
5658
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
5591
|
-
import { createHash as
|
|
5592
|
-
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as
|
|
5593
|
-
import { join as
|
|
5594
|
-
import { parseHTML as
|
|
5659
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
5660
|
+
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
|
|
5661
|
+
import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
|
|
5662
|
+
import { parseHTML as parseHTML5 } from "linkedom";
|
|
5595
5663
|
import {
|
|
5596
5664
|
COMPOSITION_SOURCE_URL,
|
|
5597
5665
|
compileTimingAttrs,
|
|
@@ -5867,7 +5935,7 @@ function readLoopOverride(el) {
|
|
|
5867
5935
|
}
|
|
5868
5936
|
return null;
|
|
5869
5937
|
}
|
|
5870
|
-
function
|
|
5938
|
+
function resolveLocalImageSourcePath(src, options) {
|
|
5871
5939
|
const trimmed = src.trim();
|
|
5872
5940
|
if (!trimmed || trimmed.startsWith("data:")) return null;
|
|
5873
5941
|
const { basePath } = splitUrlSuffix(trimmed);
|
|
@@ -6090,7 +6158,7 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6090
6158
|
const img = images[i];
|
|
6091
6159
|
const src = (img.getAttribute("src") || "").trim();
|
|
6092
6160
|
if (!hasGifExtension(src)) continue;
|
|
6093
|
-
const sourcePath =
|
|
6161
|
+
const sourcePath = resolveLocalImageSourcePath(src, options);
|
|
6094
6162
|
if (!sourcePath) continue;
|
|
6095
6163
|
const bytes = readFileSync5(sourcePath);
|
|
6096
6164
|
const metadata = parseAnimatedGifMetadata(bytes);
|
|
@@ -6138,19 +6206,74 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6138
6206
|
};
|
|
6139
6207
|
}
|
|
6140
6208
|
|
|
6141
|
-
// src/services/
|
|
6142
|
-
import {
|
|
6143
|
-
import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
|
|
6144
|
-
|
|
6145
|
-
// src/services/assetMediaType.ts
|
|
6146
|
-
import { existsSync as existsSync9 } from "node:fs";
|
|
6209
|
+
// src/services/heifImagePrep.ts
|
|
6210
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
6147
6211
|
import {
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6212
|
+
closeSync as closeSync4,
|
|
6213
|
+
copyFileSync as copyFileSync3,
|
|
6214
|
+
mkdirSync as mkdirSync6,
|
|
6215
|
+
openSync as openSync4,
|
|
6216
|
+
readFileSync as readFileSync6,
|
|
6217
|
+
readSync as readSync3,
|
|
6218
|
+
rmSync as rmSync8,
|
|
6219
|
+
statSync as statSync6,
|
|
6220
|
+
writeFileSync as writeFileSync4
|
|
6221
|
+
} from "node:fs";
|
|
6222
|
+
import { dirname as dirname6, join as join14, resolve as resolve6 } from "node:path";
|
|
6223
|
+
import { parseHTML as parseHTML4 } from "linkedom";
|
|
6224
|
+
import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
|
|
6225
|
+
|
|
6226
|
+
// src/services/heifRotation.ts
|
|
6227
|
+
function* boxes(buf, start, end) {
|
|
6228
|
+
let at = start;
|
|
6229
|
+
while (at + 8 <= end) {
|
|
6230
|
+
let size = buf.readUInt32BE(at);
|
|
6231
|
+
let header = 8;
|
|
6232
|
+
if (size === 1 && at + 16 <= end) {
|
|
6233
|
+
size = Number(buf.readBigUInt64BE(at + 8));
|
|
6234
|
+
header = 16;
|
|
6235
|
+
} else if (size === 0) {
|
|
6236
|
+
size = end - at;
|
|
6237
|
+
}
|
|
6238
|
+
if (size < header || at + size > end) return;
|
|
6239
|
+
yield { type: buf.toString("latin1", at + 4, at + 8), start: at + header, end: at + size };
|
|
6240
|
+
at += size;
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
function child(buf, parent, type, skip = 0) {
|
|
6244
|
+
return [...boxes(buf, parent.start + skip, parent.end)].filter((box) => box.type === type);
|
|
6245
|
+
}
|
|
6246
|
+
function readHeifPrimaryRotation(buf) {
|
|
6247
|
+
try {
|
|
6248
|
+
const root = { type: "", start: 0, end: buf.length };
|
|
6249
|
+
const meta = child(buf, root, "meta")[0];
|
|
6250
|
+
const pitm = meta && child(buf, meta, "pitm", 4)[0];
|
|
6251
|
+
const iprp = meta && child(buf, meta, "iprp", 4)[0];
|
|
6252
|
+
const ipco = iprp && child(buf, iprp, "ipco")[0];
|
|
6253
|
+
if (!pitm || !ipco || !iprp) return 0;
|
|
6254
|
+
const primaryId = buf[pitm.start] === 0 ? buf.readUInt16BE(pitm.start + 4) : buf.readUInt32BE(pitm.start + 4);
|
|
6255
|
+
const props = [...boxes(buf, ipco.start, ipco.end)];
|
|
6256
|
+
for (const ipma of child(buf, iprp, "ipma")) {
|
|
6257
|
+
const version = buf[ipma.start];
|
|
6258
|
+
const wide = (buf[ipma.start + 3] & 1) === 1;
|
|
6259
|
+
let at = ipma.start + 8;
|
|
6260
|
+
for (let n = buf.readUInt32BE(ipma.start + 4); n > 0 && at < ipma.end; n--) {
|
|
6261
|
+
const id = version < 1 ? buf.readUInt16BE(at) : buf.readUInt32BE(at);
|
|
6262
|
+
at += version < 1 ? 2 : 4;
|
|
6263
|
+
const count = buf[at++];
|
|
6264
|
+
for (let i = 0; i < count; i++) {
|
|
6265
|
+
const index = wide ? buf.readUInt16BE(at) & 32767 : buf[at] & 127;
|
|
6266
|
+
at += wide ? 2 : 1;
|
|
6267
|
+
const prop = props[index - 1];
|
|
6268
|
+
if (id === primaryId && prop?.type === "irot")
|
|
6269
|
+
return (buf[prop.start] & 3) * 90;
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
} catch {
|
|
6274
|
+
}
|
|
6275
|
+
return 0;
|
|
6276
|
+
}
|
|
6154
6277
|
|
|
6155
6278
|
// src/utils/semaphore.ts
|
|
6156
6279
|
var Semaphore = class {
|
|
@@ -6165,10 +6288,10 @@ var Semaphore = class {
|
|
|
6165
6288
|
this.active++;
|
|
6166
6289
|
return () => this.release();
|
|
6167
6290
|
}
|
|
6168
|
-
return new Promise((
|
|
6291
|
+
return new Promise((resolve11) => {
|
|
6169
6292
|
this.queue.push(() => {
|
|
6170
6293
|
this.active++;
|
|
6171
|
-
|
|
6294
|
+
resolve11(() => this.release());
|
|
6172
6295
|
});
|
|
6173
6296
|
});
|
|
6174
6297
|
}
|
|
@@ -6199,7 +6322,229 @@ async function withMediaProbeSlot(operation) {
|
|
|
6199
6322
|
}
|
|
6200
6323
|
}
|
|
6201
6324
|
|
|
6325
|
+
// src/services/heifImagePrep.ts
|
|
6326
|
+
var PREPARED_HEIF_SUBDIR = "_heif_images";
|
|
6327
|
+
var CACHE_SCHEMA2 = "hfheif-v1";
|
|
6328
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
6329
|
+
var HEIF_IMAGE_CONVERSION_FAILED = "HEIF_IMAGE_CONVERSION_FAILED";
|
|
6330
|
+
var HEIF_TOOL_UNAVAILABLE = "HEIF_TOOL_UNAVAILABLE";
|
|
6331
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
|
|
6332
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
6333
|
+
var DATA_URL_RE = /^data:image\/(?:heic|heif)(?:-sequence)?;base64,(.*)$/is;
|
|
6334
|
+
var HeifImageConversionError = class extends Error {
|
|
6335
|
+
code = HEIF_IMAGE_CONVERSION_FAILED;
|
|
6336
|
+
owner = "user";
|
|
6337
|
+
retryable = false;
|
|
6338
|
+
constructor(label, reason) {
|
|
6339
|
+
super(
|
|
6340
|
+
`Could not convert HEIC/HEIF image ${label} to JPEG: ${reason}. Re-export the photo as JPEG or PNG and update its src.`
|
|
6341
|
+
);
|
|
6342
|
+
this.name = "HeifImageConversionError";
|
|
6343
|
+
}
|
|
6344
|
+
};
|
|
6345
|
+
var HeifToolUnavailableError = class extends Error {
|
|
6346
|
+
code = HEIF_TOOL_UNAVAILABLE;
|
|
6347
|
+
owner = "system";
|
|
6348
|
+
retryable = false;
|
|
6349
|
+
constructor(label, reason) {
|
|
6350
|
+
super(
|
|
6351
|
+
`Could not convert HEIC/HEIF image ${label}: ${reason}. Converting HEIC needs ffmpeg and ffprobe 7.0 or newer with the hevc decoder.`
|
|
6352
|
+
);
|
|
6353
|
+
this.name = "HeifToolUnavailableError";
|
|
6354
|
+
}
|
|
6355
|
+
};
|
|
6356
|
+
var TOOL_FAILURE_RE = /(?:ffmpeg|ffprobe) not found|ENOENT|Option not found|Unrecognized option|Unknown decoder|Decoder \S+ not found/i;
|
|
6357
|
+
function isHeifBytes(head) {
|
|
6358
|
+
const bytes = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
|
|
6359
|
+
if (bytes.length < 16 || bytes.toString("ascii", 4, 8) !== "ftyp") return false;
|
|
6360
|
+
const boxEnd = Math.min(bytes.readUInt32BE(0), bytes.length);
|
|
6361
|
+
if (boxEnd < 16) return false;
|
|
6362
|
+
const brands = [bytes.toString("ascii", 8, 12)];
|
|
6363
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
|
|
6364
|
+
brands.push(bytes.toString("ascii", offset, offset + 4));
|
|
6365
|
+
}
|
|
6366
|
+
return !brands.some((b) => AVIF_BRANDS.has(b)) && brands.some((b) => HEIF_BRANDS.has(b));
|
|
6367
|
+
}
|
|
6368
|
+
function readHead(path) {
|
|
6369
|
+
let fd;
|
|
6370
|
+
try {
|
|
6371
|
+
fd = openSync4(path, "r");
|
|
6372
|
+
const buf = Buffer.alloc(4096);
|
|
6373
|
+
return buf.subarray(0, readSync3(fd, buf, 0, buf.length, 0));
|
|
6374
|
+
} catch {
|
|
6375
|
+
return null;
|
|
6376
|
+
} finally {
|
|
6377
|
+
if (fd !== void 0) closeSync4(fd);
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
async function probeJson(path, withGroups) {
|
|
6381
|
+
const stdout = await runFfprobe(
|
|
6382
|
+
path,
|
|
6383
|
+
withGroups ? ["-show_stream_groups", "-show_streams", "-of", "json"] : ["-show_streams", "-of", "json"]
|
|
6384
|
+
);
|
|
6385
|
+
return JSON.parse(stdout);
|
|
6386
|
+
}
|
|
6387
|
+
var ROTATION_FILTER = {
|
|
6388
|
+
0: "",
|
|
6389
|
+
90: "transpose=2",
|
|
6390
|
+
180: "hflip,vflip",
|
|
6391
|
+
270: "transpose=1"
|
|
6392
|
+
};
|
|
6393
|
+
function primaryGrid(probed) {
|
|
6394
|
+
const groups = probed.stream_groups ?? [];
|
|
6395
|
+
return (groups.find((g) => g.disposition?.default === 1) ?? groups[0])?.components?.[0];
|
|
6396
|
+
}
|
|
6397
|
+
async function convertHeifToJpeg(request) {
|
|
6398
|
+
const probed = await probeJson(request.inputPath, true);
|
|
6399
|
+
const grid = primaryGrid(probed);
|
|
6400
|
+
const rotation = readHeifPrimaryRotation(readFileSync6(request.inputPath));
|
|
6401
|
+
const filters = [];
|
|
6402
|
+
let inputs = [];
|
|
6403
|
+
let size = probed.streams?.[0];
|
|
6404
|
+
if (grid?.subcomponents?.length) {
|
|
6405
|
+
size = grid;
|
|
6406
|
+
const layout = grid.subcomponents.map(
|
|
6407
|
+
(t) => `${t.tile_horizontal_offset}_${t.tile_vertical_offset}`
|
|
6408
|
+
);
|
|
6409
|
+
inputs = grid.subcomponents.map((t) => `[0:${t.stream_index}]`);
|
|
6410
|
+
filters.push(
|
|
6411
|
+
`${inputs.join("")}xstack=inputs=${inputs.length}:layout=${layout.join("|")}`,
|
|
6412
|
+
`crop=${grid.width}:${grid.height}:${grid.horizontal_offset ?? 0}:${grid.vertical_offset ?? 0}`
|
|
6413
|
+
);
|
|
6414
|
+
}
|
|
6415
|
+
if (!size?.width || !size.height) throw new Error("no declared image size");
|
|
6416
|
+
if (rotation && grid) filters.push(ROTATION_FILTER[rotation]);
|
|
6417
|
+
const mapArgs = filters.length > 0 ? ["-filter_complex", `${filters.join(",")}[v]`, "-map", "[v]"] : [];
|
|
6418
|
+
const result = await runFfmpeg3(
|
|
6419
|
+
[
|
|
6420
|
+
"-hide_banner",
|
|
6421
|
+
"-loglevel",
|
|
6422
|
+
"error",
|
|
6423
|
+
"-i",
|
|
6424
|
+
request.inputPath,
|
|
6425
|
+
...mapArgs,
|
|
6426
|
+
"-frames:v",
|
|
6427
|
+
"1",
|
|
6428
|
+
"-q:v",
|
|
6429
|
+
"2",
|
|
6430
|
+
"-f",
|
|
6431
|
+
"mjpeg",
|
|
6432
|
+
"-y",
|
|
6433
|
+
request.outputPath
|
|
6434
|
+
],
|
|
6435
|
+
{ timeout: request.timeoutMs ?? DEFAULT_TIMEOUT_MS }
|
|
6436
|
+
);
|
|
6437
|
+
if (!result.success) {
|
|
6438
|
+
throw result.error ?? new Error(`ffmpeg exited ${result.exitCode}: ${result.stderr.slice(-300)}`);
|
|
6439
|
+
}
|
|
6440
|
+
const out = (await probeJson(request.outputPath, false)).streams?.[0];
|
|
6441
|
+
const same = out?.width === size.width && out.height === size.height;
|
|
6442
|
+
const swapped = out?.width === size.height && out.height === size.width;
|
|
6443
|
+
if (!same && !swapped) {
|
|
6444
|
+
throw new Error(
|
|
6445
|
+
`wrong size: decoded ${out?.width}x${out?.height}, file declares ${size.width}x${size.height}`
|
|
6446
|
+
);
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
function resolveImageSource(src, options) {
|
|
6450
|
+
const data = DATA_URL_RE.exec(src);
|
|
6451
|
+
if (data) {
|
|
6452
|
+
const bytes = Buffer.from(data[1], "base64");
|
|
6453
|
+
return isHeifBytes(bytes.subarray(0, 4096)) ? { path: null, bytes, label: '"inline image"' } : null;
|
|
6454
|
+
}
|
|
6455
|
+
const path = resolveLocalImageSourcePath(src, options);
|
|
6456
|
+
const head = path ? readHead(path) : null;
|
|
6457
|
+
if (!path || !head || !isHeifBytes(head)) return null;
|
|
6458
|
+
return { path, bytes: readFileSync6(path), label: `"${splitUrlSuffix(src).basePath}"` };
|
|
6459
|
+
}
|
|
6460
|
+
function isUsableFile2(path) {
|
|
6461
|
+
try {
|
|
6462
|
+
return statSync6(path).size > 0;
|
|
6463
|
+
} catch {
|
|
6464
|
+
return false;
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
6468
|
+
mkdirSync6(dirname6(cachePath), { recursive: true });
|
|
6469
|
+
mkdirSync6(dirname6(outputPath), { recursive: true });
|
|
6470
|
+
if (!isUsableFile2(cachePath)) {
|
|
6471
|
+
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
6472
|
+
const inlinePath = `${tmpPath}.heic`;
|
|
6473
|
+
const inputPath = source.path ?? inlinePath;
|
|
6474
|
+
try {
|
|
6475
|
+
if (!source.path) writeFileSync4(inlinePath, source.bytes);
|
|
6476
|
+
await (options.transcode ?? convertHeifToJpeg)({
|
|
6477
|
+
inputPath,
|
|
6478
|
+
outputPath: tmpPath,
|
|
6479
|
+
timeoutMs: options.timeoutMs
|
|
6480
|
+
});
|
|
6481
|
+
if (!isUsableFile2(tmpPath)) throw new Error("conversion produced an empty file");
|
|
6482
|
+
renameWithRetry4(tmpPath, cachePath);
|
|
6483
|
+
} catch (error) {
|
|
6484
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
6485
|
+
throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
|
|
6486
|
+
} finally {
|
|
6487
|
+
rmSync8(tmpPath, { force: true });
|
|
6488
|
+
rmSync8(inlinePath, { force: true });
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
|
|
6492
|
+
copyFileSync3(cachePath, outputPath);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
async function prepareHeifImageInputs(html, options) {
|
|
6496
|
+
const outputDir = options.outputDir ?? join14(options.downloadDir, PREPARED_HEIF_SUBDIR);
|
|
6497
|
+
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
|
|
6498
|
+
const cacheDir = options.cacheDir ?? outputDir;
|
|
6499
|
+
const preparedAssets = /* @__PURE__ */ new Map();
|
|
6500
|
+
const preparedImages = [];
|
|
6501
|
+
if (!/<img\b/i.test(html)) return { html, preparedAssets, preparedImages };
|
|
6502
|
+
const { document: document2 } = parseHTML4(html);
|
|
6503
|
+
const found = [];
|
|
6504
|
+
for (const img of Array.from(document2.querySelectorAll("img[src]"))) {
|
|
6505
|
+
const src = (img.getAttribute("src") || "").trim();
|
|
6506
|
+
const source = src ? resolveImageSource(src, options) : null;
|
|
6507
|
+
if (!source) continue;
|
|
6508
|
+
const hash = createHash5("sha256").update(CACHE_SCHEMA2).update("\0").update(source.bytes).digest("hex");
|
|
6509
|
+
found.push({ img, src, source, filename: `${CACHE_SCHEMA2}-${hash.slice(0, 24)}.jpg` });
|
|
6510
|
+
}
|
|
6511
|
+
if (found.length === 0) return { html, preparedAssets, preparedImages };
|
|
6512
|
+
const unique = new Map(found.map((item) => [item.filename, item.source]));
|
|
6513
|
+
const failed = /* @__PURE__ */ new Set();
|
|
6514
|
+
await Promise.all(
|
|
6515
|
+
[...unique].map(
|
|
6516
|
+
([filename, source]) => withMediaProbeSlot(
|
|
6517
|
+
() => ensurePreparedJpeg(source, join14(cacheDir, filename), join14(outputDir, filename), options)
|
|
6518
|
+
).catch((error) => {
|
|
6519
|
+
if (!options.onFailure) throw error;
|
|
6520
|
+
failed.add(filename);
|
|
6521
|
+
options.onFailure(error);
|
|
6522
|
+
})
|
|
6523
|
+
)
|
|
6524
|
+
);
|
|
6525
|
+
for (const { img, src, filename } of found) {
|
|
6526
|
+
if (failed.has(filename)) continue;
|
|
6527
|
+
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
6528
|
+
img.setAttribute("src", outputSrc);
|
|
6529
|
+
preparedAssets.set(outputSrc, join14(outputDir, filename));
|
|
6530
|
+
preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join14(outputDir, filename) });
|
|
6531
|
+
}
|
|
6532
|
+
return { html: document2.toString(), preparedAssets, preparedImages };
|
|
6533
|
+
}
|
|
6534
|
+
|
|
6535
|
+
// src/services/htmlCompiler.ts
|
|
6536
|
+
import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
|
|
6537
|
+
import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
|
|
6538
|
+
|
|
6202
6539
|
// src/services/assetMediaType.ts
|
|
6540
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
6541
|
+
import {
|
|
6542
|
+
fingerprintElementId,
|
|
6543
|
+
isNotMediaPayload,
|
|
6544
|
+
NotMediaPayloadError,
|
|
6545
|
+
probeMediaProfile,
|
|
6546
|
+
resolveProjectRelativeSrc as resolveProjectRelativeSrc2
|
|
6547
|
+
} from "@hyperframes/engine";
|
|
6203
6548
|
var ASSET_MEDIA_TYPE_MISMATCH = "ASSET_MEDIA_TYPE_MISMATCH";
|
|
6204
6549
|
var AssetMediaTypeMismatchError = class extends Error {
|
|
6205
6550
|
code = ASSET_MEDIA_TYPE_MISMATCH;
|
|
@@ -6309,7 +6654,7 @@ function logRemoteDownloadTelemetry(event) {
|
|
|
6309
6654
|
}
|
|
6310
6655
|
var INFERRED_MEDIA_DURATION_ATTR = "data-hf-inferred-duration";
|
|
6311
6656
|
function parseSubCompHtmlForValidity(html) {
|
|
6312
|
-
return
|
|
6657
|
+
return parseHTML5(html).document;
|
|
6313
6658
|
}
|
|
6314
6659
|
function endsWithSourceUrl(code) {
|
|
6315
6660
|
const trimmed = code.trimEnd();
|
|
@@ -6317,7 +6662,7 @@ function endsWithSourceUrl(code) {
|
|
|
6317
6662
|
return lastLine.startsWith("//# sourceURL=") || lastLine.startsWith("//@ sourceURL=");
|
|
6318
6663
|
}
|
|
6319
6664
|
function prepareCompositionScripts(html) {
|
|
6320
|
-
const { document: document2 } =
|
|
6665
|
+
const { document: document2 } = parseHTML5(html);
|
|
6321
6666
|
for (const el of document2.querySelectorAll("script:not([src])")) {
|
|
6322
6667
|
const isModule = (el.getAttribute("type") || "").trim().toLowerCase() === "module";
|
|
6323
6668
|
const runsAsScript = isModule || isJavaScriptType(el);
|
|
@@ -6354,14 +6699,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
|
|
|
6354
6699
|
}
|
|
6355
6700
|
};
|
|
6356
6701
|
function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
|
|
6357
|
-
const { document: document2 } =
|
|
6702
|
+
const { document: document2 } = parseHTML5(html);
|
|
6358
6703
|
const hosts = [...document2.querySelectorAll("[data-composition-src]")];
|
|
6359
6704
|
const problems = [];
|
|
6360
6705
|
for (const el of hosts) {
|
|
6361
6706
|
const srcPath = el.getAttribute("data-composition-src");
|
|
6362
6707
|
if (!srcPath) continue;
|
|
6363
6708
|
if (isUnresolvedAssetPlaceholder(srcPath)) continue;
|
|
6364
|
-
const filePath =
|
|
6709
|
+
const filePath = resolve7(projectDir, srcPath);
|
|
6365
6710
|
if (visited.has(filePath)) continue;
|
|
6366
6711
|
const read = readProjectFile(filePath);
|
|
6367
6712
|
if (read.kind === "missing") {
|
|
@@ -6417,7 +6762,7 @@ function runtimeRoot(document2) {
|
|
|
6417
6762
|
}
|
|
6418
6763
|
function detectRenderModeHints(html, pageMotionBlur) {
|
|
6419
6764
|
const reasons = [];
|
|
6420
|
-
const { document: document2 } =
|
|
6765
|
+
const { document: document2 } = parseHTML5(html);
|
|
6421
6766
|
if (document2.querySelector("canvas[layoutsubtree]")) {
|
|
6422
6767
|
reasons.push({
|
|
6423
6768
|
code: "htmlInCanvas",
|
|
@@ -6459,7 +6804,7 @@ function detectThreeDTransformUsage(html) {
|
|
|
6459
6804
|
}
|
|
6460
6805
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
6461
6806
|
function detectAncestorBackgroundImage(html) {
|
|
6462
|
-
const { document: document2 } =
|
|
6807
|
+
const { document: document2 } = parseHTML5(html);
|
|
6463
6808
|
const root = document2.querySelector("[data-composition-id]");
|
|
6464
6809
|
if (!root) return false;
|
|
6465
6810
|
const ancestors = [];
|
|
@@ -6503,10 +6848,10 @@ function detectShaderTransitionUsage(html) {
|
|
|
6503
6848
|
}
|
|
6504
6849
|
return false;
|
|
6505
6850
|
}
|
|
6506
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
6851
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
6507
6852
|
let filePath = src;
|
|
6508
6853
|
if (isHttpUrl(src)) {
|
|
6509
|
-
if (!existsSync10(downloadDir))
|
|
6854
|
+
if (!existsSync10(downloadDir)) mkdirSync7(downloadDir, { recursive: true });
|
|
6510
6855
|
try {
|
|
6511
6856
|
filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
|
|
6512
6857
|
onTelemetry: logRemoteDownloadTelemetry
|
|
@@ -6516,7 +6861,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6516
6861
|
}
|
|
6517
6862
|
} else {
|
|
6518
6863
|
filePath = decodedUrlPath(src);
|
|
6519
|
-
if (!filePath.startsWith("/")) filePath =
|
|
6864
|
+
if (!filePath.startsWith("/")) filePath = join15(baseDir, filePath);
|
|
6520
6865
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
6521
6866
|
}
|
|
6522
6867
|
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
@@ -6530,50 +6875,57 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6530
6875
|
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
|
|
6531
6876
|
return wrapped;
|
|
6532
6877
|
};
|
|
6533
|
-
|
|
6534
|
-
let profile;
|
|
6878
|
+
const probe = () => withMediaProbeSlot(async () => {
|
|
6535
6879
|
try {
|
|
6536
6880
|
await assertMediaPayload(filePath, elementIdentity);
|
|
6537
|
-
profile = await probeMediaProfile2(filePath);
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
if (error instanceof NotMediaPayloadError2) {
|
|
6541
|
-
log?.warn(
|
|
6542
|
-
`[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.`
|
|
6543
|
-
);
|
|
6544
|
-
}
|
|
6545
|
-
return { duration: null, resolvedPath: filePath };
|
|
6881
|
+
const profile = await probeMediaProfile2(filePath);
|
|
6882
|
+
if (tagName === "video") {
|
|
6883
|
+
return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
|
|
6546
6884
|
}
|
|
6547
|
-
throw withSrcContext(error);
|
|
6548
|
-
}
|
|
6549
|
-
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
|
|
6550
|
-
let metadata;
|
|
6551
|
-
if (tagName === "video") {
|
|
6552
6885
|
try {
|
|
6553
|
-
|
|
6554
|
-
} catch (error) {
|
|
6555
|
-
throw withSrcContext(error);
|
|
6556
|
-
}
|
|
6557
|
-
} else {
|
|
6558
|
-
try {
|
|
6559
|
-
metadata = await extractAudioMetadata(filePath);
|
|
6886
|
+
return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
|
|
6560
6887
|
} catch {
|
|
6561
|
-
return {
|
|
6888
|
+
return { profile, fileDuration: null };
|
|
6562
6889
|
}
|
|
6890
|
+
} catch (error) {
|
|
6891
|
+
return { error };
|
|
6563
6892
|
}
|
|
6564
|
-
const fileDuration = metadata.durationSeconds;
|
|
6565
|
-
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6566
|
-
fileDuration,
|
|
6567
|
-
mediaStart,
|
|
6568
|
-
playbackRate
|
|
6569
|
-
);
|
|
6570
|
-
return { duration, resolvedPath: filePath };
|
|
6571
6893
|
});
|
|
6894
|
+
const key = `${tagName}\0${filePath}`;
|
|
6895
|
+
let probed = probes?.get(key);
|
|
6896
|
+
if (!probed) {
|
|
6897
|
+
probed = probe();
|
|
6898
|
+
probes?.set(key, probed);
|
|
6899
|
+
}
|
|
6900
|
+
const outcome = await probed;
|
|
6901
|
+
if ("error" in outcome) {
|
|
6902
|
+
if (tagName !== "video") {
|
|
6903
|
+
if (outcome.error instanceof NotMediaPayloadError2) {
|
|
6904
|
+
log?.warn(
|
|
6905
|
+
`[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.`
|
|
6906
|
+
);
|
|
6907
|
+
}
|
|
6908
|
+
return { duration: null, resolvedPath: filePath };
|
|
6909
|
+
}
|
|
6910
|
+
throw withSrcContext(outcome.error);
|
|
6911
|
+
}
|
|
6912
|
+
assertAssetMediaTypeProfile(
|
|
6913
|
+
tagName === "video" ? "video" : "audio",
|
|
6914
|
+
outcome.profile,
|
|
6915
|
+
elementIdentity
|
|
6916
|
+
);
|
|
6917
|
+
if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
|
|
6918
|
+
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
6919
|
+
outcome.fileDuration,
|
|
6920
|
+
mediaStart,
|
|
6921
|
+
playbackRate
|
|
6922
|
+
);
|
|
6923
|
+
return { duration, resolvedPath: filePath };
|
|
6572
6924
|
}
|
|
6573
6925
|
function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
6574
6926
|
const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
|
|
6575
6927
|
if (unresolvedIds.size === 0) return html;
|
|
6576
|
-
const { document: document2 } =
|
|
6928
|
+
const { document: document2 } = parseHTML5(html);
|
|
6577
6929
|
let changed = false;
|
|
6578
6930
|
for (const element of document2.querySelectorAll("video[data-var-src], audio[data-var-src]")) {
|
|
6579
6931
|
if (!unresolvedIds.has(element.id)) continue;
|
|
@@ -6582,7 +6934,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
6582
6934
|
}
|
|
6583
6935
|
return changed ? document2.toString() : html;
|
|
6584
6936
|
}
|
|
6585
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
6937
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
|
|
6586
6938
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
6587
6939
|
const mediaUnresolved = unresolved.filter(
|
|
6588
6940
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -6599,7 +6951,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6599
6951
|
el.tagName,
|
|
6600
6952
|
el.id,
|
|
6601
6953
|
log,
|
|
6602
|
-
urlByAbsentLocalPath
|
|
6954
|
+
urlByAbsentLocalPath,
|
|
6955
|
+
probes
|
|
6603
6956
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
6604
6957
|
)
|
|
6605
6958
|
);
|
|
@@ -6620,7 +6973,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6620
6973
|
downloadDir,
|
|
6621
6974
|
el.tagName,
|
|
6622
6975
|
el.id,
|
|
6623
|
-
log
|
|
6976
|
+
log,
|
|
6977
|
+
void 0,
|
|
6978
|
+
probes
|
|
6624
6979
|
);
|
|
6625
6980
|
return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
|
|
6626
6981
|
})
|
|
@@ -6642,15 +6997,15 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
6642
6997
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
6643
6998
|
return { html: compiledHtml, unresolvedCompositions };
|
|
6644
6999
|
}
|
|
6645
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
7000
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
|
|
6646
7001
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
6647
|
-
const { document: document2 } =
|
|
7002
|
+
const { document: document2 } = parseHTML5(html);
|
|
6648
7003
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
6649
7004
|
const workItems = [];
|
|
6650
7005
|
for (const el of compEls) {
|
|
6651
7006
|
const srcPath = el.getAttribute("data-composition-src");
|
|
6652
7007
|
if (!srcPath) continue;
|
|
6653
|
-
const filePath =
|
|
7008
|
+
const filePath = resolve7(projectDir, srcPath);
|
|
6654
7009
|
if (visited.has(filePath)) {
|
|
6655
7010
|
continue;
|
|
6656
7011
|
}
|
|
@@ -6667,17 +7022,19 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6667
7022
|
workItems.map(async (item) => {
|
|
6668
7023
|
const { html: compiledSub } = await compileHtmlFile(
|
|
6669
7024
|
rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
|
|
6670
|
-
|
|
7025
|
+
dirname7(item.filePath),
|
|
6671
7026
|
downloadDir,
|
|
6672
7027
|
void 0,
|
|
6673
|
-
urlByAbsentLocalPath
|
|
7028
|
+
urlByAbsentLocalPath,
|
|
7029
|
+
probes
|
|
6674
7030
|
);
|
|
6675
7031
|
const nested = await parseSubCompositions(
|
|
6676
7032
|
compiledSub,
|
|
6677
7033
|
projectDir,
|
|
6678
7034
|
downloadDir,
|
|
6679
7035
|
item.nestedVisited,
|
|
6680
|
-
urlByAbsentLocalPath
|
|
7036
|
+
urlByAbsentLocalPath,
|
|
7037
|
+
probes
|
|
6681
7038
|
);
|
|
6682
7039
|
return {
|
|
6683
7040
|
srcPath: item.srcPath,
|
|
@@ -6695,7 +7052,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
6695
7052
|
return { subCompositions };
|
|
6696
7053
|
}
|
|
6697
7054
|
function promoteCssImportsToLinkTags(html) {
|
|
6698
|
-
const { document: document2 } =
|
|
7055
|
+
const { document: document2 } = parseHTML5(html);
|
|
6699
7056
|
const head = document2.querySelector("head");
|
|
6700
7057
|
if (!head) return html;
|
|
6701
7058
|
const importRe = /@import\s+url\(\s*['"]?([^'")\s]+)['"]?\s*\)\s*;?/gi;
|
|
@@ -6764,7 +7121,7 @@ var ProducerHostIdentityMap = class extends Map {
|
|
|
6764
7121
|
}
|
|
6765
7122
|
};
|
|
6766
7123
|
function coalesceHeadStylesAndBodyScripts(html) {
|
|
6767
|
-
const { document: document2 } =
|
|
7124
|
+
const { document: document2 } = parseHTML5(html);
|
|
6768
7125
|
const head = document2.querySelector("head");
|
|
6769
7126
|
const body = document2.querySelector("body");
|
|
6770
7127
|
if (!head && !body) return html;
|
|
@@ -6815,7 +7172,7 @@ function coalesceHeadStylesAndBodyScripts(html) {
|
|
|
6815
7172
|
return document2.toString();
|
|
6816
7173
|
}
|
|
6817
7174
|
function inlineSubCompositions(html, subCompositions, projectDir, variableOverrides = {}) {
|
|
6818
|
-
const { document: document2 } =
|
|
7175
|
+
const { document: document2 } = parseHTML5(html);
|
|
6819
7176
|
const head = document2.querySelector("head");
|
|
6820
7177
|
const body = document2.querySelector("body");
|
|
6821
7178
|
const hosts = Array.from(document2.querySelectorAll("[data-composition-src]"));
|
|
@@ -6844,17 +7201,17 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
|
|
|
6844
7201
|
resolveHtml: (srcPath) => {
|
|
6845
7202
|
let compHtml = subCompositions.get(srcPath) || null;
|
|
6846
7203
|
if (!compHtml) {
|
|
6847
|
-
const read = readProjectFile(
|
|
7204
|
+
const read = readProjectFile(resolve7(projectDir, srcPath));
|
|
6848
7205
|
if (read.kind === "file") {
|
|
6849
7206
|
compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
|
|
6850
7207
|
}
|
|
6851
7208
|
}
|
|
6852
7209
|
return compHtml;
|
|
6853
7210
|
},
|
|
6854
|
-
parseHtml: (htmlStr) =>
|
|
7211
|
+
parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
|
|
6855
7212
|
// Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
|
|
6856
7213
|
// against its own directory, project-root refs stay as authored.
|
|
6857
|
-
assetExists: (path) => existsSync10(
|
|
7214
|
+
assetExists: (path) => existsSync10(resolve7(projectDir, path)),
|
|
6858
7215
|
// Preserve the authored root wrapper as a child of the host, matching
|
|
6859
7216
|
// the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
|
|
6860
7217
|
// which the runtime compositionLoader mirrors with its own copy for the
|
|
@@ -6955,7 +7312,7 @@ ${html}
|
|
|
6955
7312
|
</html>`;
|
|
6956
7313
|
}
|
|
6957
7314
|
function injectTextRenderingRule(html) {
|
|
6958
|
-
const { document: document2 } =
|
|
7315
|
+
const { document: document2 } = parseHTML5(html);
|
|
6959
7316
|
const head = document2.querySelector("head");
|
|
6960
7317
|
if (!head) return html;
|
|
6961
7318
|
if (document2.querySelector("style[data-hyperframes-text-rendering]")) {
|
|
@@ -6979,7 +7336,7 @@ function matchesScriptIntegrity(bytes, metadata) {
|
|
|
6979
7336
|
(alg) => hashes.some((hash) => hash[1]?.toLowerCase() === alg)
|
|
6980
7337
|
);
|
|
6981
7338
|
if (!strongest) return true;
|
|
6982
|
-
const actual =
|
|
7339
|
+
const actual = createHash6(strongest).update(bytes).digest();
|
|
6983
7340
|
return hashes.some(
|
|
6984
7341
|
(hash) => hash[1]?.toLowerCase() === strongest && actual.equals(Buffer.from(hash[2], "base64"))
|
|
6985
7342
|
);
|
|
@@ -6987,7 +7344,7 @@ function matchesScriptIntegrity(bytes, metadata) {
|
|
|
6987
7344
|
async function inlineExternalScripts(html) {
|
|
6988
7345
|
const fullHtml = ensureFullDocument(html);
|
|
6989
7346
|
const wrappedFragment = fullHtml !== html;
|
|
6990
|
-
const { document: document2 } =
|
|
7347
|
+
const { document: document2 } = parseHTML5(fullHtml);
|
|
6991
7348
|
const scripts = document2.querySelectorAll("script[src]");
|
|
6992
7349
|
const externalScripts = [];
|
|
6993
7350
|
for (const el of scripts) {
|
|
@@ -7036,12 +7393,12 @@ ${safeText}
|
|
|
7036
7393
|
return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
7037
7394
|
}
|
|
7038
7395
|
function collectExternalAssets(html, projectDir) {
|
|
7039
|
-
const absProjectDir =
|
|
7396
|
+
const absProjectDir = resolve7(projectDir);
|
|
7040
7397
|
const externalAssets = /* @__PURE__ */ new Map();
|
|
7041
7398
|
function processPath(rawPath) {
|
|
7042
7399
|
const trimmed = rawPath.trim();
|
|
7043
7400
|
if (isNonRelativeUrl(trimmed)) return null;
|
|
7044
|
-
const absPath =
|
|
7401
|
+
const absPath = resolve7(absProjectDir, trimmed);
|
|
7045
7402
|
if (isPathInside2(absPath, absProjectDir)) {
|
|
7046
7403
|
return null;
|
|
7047
7404
|
}
|
|
@@ -7050,7 +7407,7 @@ function collectExternalAssets(html, projectDir) {
|
|
|
7050
7407
|
externalAssets.set(safeKey, absPath);
|
|
7051
7408
|
return safeKey;
|
|
7052
7409
|
}
|
|
7053
|
-
const { document: document2 } =
|
|
7410
|
+
const { document: document2 } = parseHTML5(html);
|
|
7054
7411
|
for (const el of document2.querySelectorAll("[src], [href]")) {
|
|
7055
7412
|
for (const attr of ["src", "href"]) {
|
|
7056
7413
|
const val = (el.getAttribute(attr) || "").trim();
|
|
@@ -7095,7 +7452,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
|
|
|
7095
7452
|
var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
7096
7453
|
async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
|
|
7097
7454
|
if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
|
|
7098
|
-
if (!existsSync10(remoteDir))
|
|
7455
|
+
if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
|
|
7099
7456
|
const urlToLocal = /* @__PURE__ */ new Map();
|
|
7100
7457
|
await Promise.all(
|
|
7101
7458
|
[...urlSet].map(async (url) => {
|
|
@@ -7142,7 +7499,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
7142
7499
|
return downloadAndRewriteUrls(
|
|
7143
7500
|
urlSet,
|
|
7144
7501
|
html,
|
|
7145
|
-
|
|
7502
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7146
7503
|
"Remote media download failed",
|
|
7147
7504
|
"Localized remote media source(s)"
|
|
7148
7505
|
);
|
|
@@ -7157,7 +7514,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
7157
7514
|
return downloadAndRewriteUrls(
|
|
7158
7515
|
urlSet,
|
|
7159
7516
|
html,
|
|
7160
|
-
|
|
7517
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7161
7518
|
"Remote image download failed",
|
|
7162
7519
|
"Localized remote image source(s)"
|
|
7163
7520
|
);
|
|
@@ -7173,7 +7530,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
7173
7530
|
return downloadAndRewriteUrls(
|
|
7174
7531
|
urlSet,
|
|
7175
7532
|
html,
|
|
7176
|
-
|
|
7533
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7177
7534
|
"Remote background-image download failed",
|
|
7178
7535
|
"Localized remote background-image(s)",
|
|
7179
7536
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -7308,7 +7665,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
|
|
|
7308
7665
|
return downloadAndRewriteUrls(
|
|
7309
7666
|
urlSet,
|
|
7310
7667
|
processed,
|
|
7311
|
-
|
|
7668
|
+
join15(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7312
7669
|
"Remote font download failed",
|
|
7313
7670
|
"Localized remote font face(s)",
|
|
7314
7671
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -7353,7 +7710,7 @@ async function embedLocalFontFaces(html, projectDir) {
|
|
|
7353
7710
|
while ((urlMatch = urlRe.exec(block)) !== null) {
|
|
7354
7711
|
const localPath = urlMatch[1];
|
|
7355
7712
|
if (!localPath || embeddedPaths.has(localPath)) continue;
|
|
7356
|
-
const absPath = localPath.startsWith("/") ? localPath :
|
|
7713
|
+
const absPath = localPath.startsWith("/") ? localPath : resolve7(projectDir, localPath);
|
|
7357
7714
|
if (!isPathInside2(absPath, projectDir)) continue;
|
|
7358
7715
|
const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
|
|
7359
7716
|
try {
|
|
@@ -7402,7 +7759,7 @@ function renderGsapDist() {
|
|
|
7402
7759
|
}
|
|
7403
7760
|
function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
|
|
7404
7761
|
let gsapDist;
|
|
7405
|
-
const exists = (path) => existsSync10(
|
|
7762
|
+
const exists = (path) => existsSync10(resolve7(projectDir, path));
|
|
7406
7763
|
return html.replace(
|
|
7407
7764
|
/(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
|
|
7408
7765
|
(full, prefix, src, file, suffix) => {
|
|
@@ -7418,8 +7775,8 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
|
|
|
7418
7775
|
if (!isPathInside2(htmlPath, projectDir)) return html;
|
|
7419
7776
|
const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
|
|
7420
7777
|
if (!entryPath.includes("/")) return html;
|
|
7421
|
-
const { document: document2 } =
|
|
7422
|
-
const assetExists = (path) => existsSync10(
|
|
7778
|
+
const { document: document2 } = parseHTML5(html);
|
|
7779
|
+
const assetExists = (path) => existsSync10(resolve7(projectDir, path));
|
|
7423
7780
|
rewriteAssetPaths(
|
|
7424
7781
|
document2.querySelectorAll("[src], [href]"),
|
|
7425
7782
|
entryPath,
|
|
@@ -7441,25 +7798,28 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
|
|
|
7441
7798
|
}
|
|
7442
7799
|
async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
|
|
7443
7800
|
const entryHtml = rebaseDirectEntryAssetPaths(
|
|
7444
|
-
|
|
7801
|
+
readFileSync7(htmlPath, "utf-8"),
|
|
7445
7802
|
projectDir,
|
|
7446
7803
|
htmlPath
|
|
7447
7804
|
);
|
|
7448
7805
|
const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
|
|
7449
7806
|
assertSubCompositionsUsable(rawHtml, projectDir);
|
|
7807
|
+
const probes = /* @__PURE__ */ new Map();
|
|
7450
7808
|
const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
|
|
7451
7809
|
rawHtml,
|
|
7452
7810
|
projectDir,
|
|
7453
7811
|
downloadDir,
|
|
7454
7812
|
options.log,
|
|
7455
|
-
options.urlByAbsentLocalPath
|
|
7813
|
+
options.urlByAbsentLocalPath,
|
|
7814
|
+
probes
|
|
7456
7815
|
);
|
|
7457
7816
|
const { subCompositions } = await parseSubCompositions(
|
|
7458
7817
|
compiledHtml,
|
|
7459
7818
|
projectDir,
|
|
7460
7819
|
downloadDir,
|
|
7461
7820
|
void 0,
|
|
7462
|
-
options.urlByAbsentLocalPath
|
|
7821
|
+
options.urlByAbsentLocalPath,
|
|
7822
|
+
probes
|
|
7463
7823
|
);
|
|
7464
7824
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
7465
7825
|
const inlinedHtml = inlineSubCompositions(
|
|
@@ -7511,12 +7871,26 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7511
7871
|
const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
|
|
7512
7872
|
const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
|
|
7513
7873
|
const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
|
|
7874
|
+
const {
|
|
7875
|
+
html: htmlWithJpegs,
|
|
7876
|
+
preparedAssets: preparedHeifAssets,
|
|
7877
|
+
preparedImages: preparedHeifImages
|
|
7878
|
+
} = await prepareHeifImageInputs(htmlWithLocalizedFonts, {
|
|
7879
|
+
projectDir,
|
|
7880
|
+
downloadDir,
|
|
7881
|
+
cacheDir: options.animatedGifCacheDir,
|
|
7882
|
+
sourceAssets: remoteImageAssets,
|
|
7883
|
+
timeoutMs: options.ffmpegProcessTimeout
|
|
7884
|
+
});
|
|
7885
|
+
if (preparedHeifImages.length > 0) {
|
|
7886
|
+
defaultLogger.info(`[Compiler] Converted ${preparedHeifImages.length} HEIC image(s) to JPEG`);
|
|
7887
|
+
}
|
|
7514
7888
|
const gifSourceAssets = new Map(remoteImageAssets);
|
|
7515
7889
|
const {
|
|
7516
7890
|
html: htmlWithPreparedGifs,
|
|
7517
7891
|
preparedAssets: preparedGifAssets,
|
|
7518
7892
|
preparedGifs
|
|
7519
|
-
} = await prepareAnimatedGifInputs(
|
|
7893
|
+
} = await prepareAnimatedGifInputs(htmlWithJpegs, {
|
|
7520
7894
|
projectDir,
|
|
7521
7895
|
downloadDir,
|
|
7522
7896
|
cacheDir: options.animatedGifCacheDir,
|
|
@@ -7543,11 +7917,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7543
7917
|
for (const [relPath, absPath] of preparedGifAssets) {
|
|
7544
7918
|
externalAssets.set(relPath, absPath);
|
|
7545
7919
|
}
|
|
7920
|
+
for (const [relPath, absPath] of preparedHeifAssets) {
|
|
7921
|
+
externalAssets.set(relPath, absPath);
|
|
7922
|
+
}
|
|
7546
7923
|
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
|
|
7547
7924
|
const masterFxChain = parseMasterFxChain(html);
|
|
7925
|
+
const advised = /* @__PURE__ */ new Set();
|
|
7548
7926
|
for (const video of videos) {
|
|
7549
7927
|
if (isHttpUrl(video.src)) continue;
|
|
7550
|
-
const videoPath =
|
|
7928
|
+
const videoPath = resolve7(projectDir, video.src);
|
|
7929
|
+
if (advised.has(videoPath)) continue;
|
|
7930
|
+
advised.add(videoPath);
|
|
7551
7931
|
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
7552
7932
|
(async () => {
|
|
7553
7933
|
const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
|
|
@@ -7567,7 +7947,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7567
7947
|
})().catch(() => {
|
|
7568
7948
|
});
|
|
7569
7949
|
}
|
|
7570
|
-
const { document: document2 } =
|
|
7950
|
+
const { document: document2 } = parseHTML5(html);
|
|
7571
7951
|
const rootEl = document2.querySelector("[data-composition-id]");
|
|
7572
7952
|
const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
|
|
7573
7953
|
const height = rootEl ? parseInt(rootEl.getAttribute("data-height") || "1920", 10) : 1920;
|
|
@@ -7974,7 +8354,7 @@ async function runCompileStage(input) {
|
|
|
7974
8354
|
abortSignal
|
|
7975
8355
|
} = input;
|
|
7976
8356
|
const compileStart = Date.now();
|
|
7977
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
8357
|
+
const compiled = await compileForRender(projectDir, htmlPath, join16(workDir, "downloads"), {
|
|
7978
8358
|
log,
|
|
7979
8359
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
7980
8360
|
allowSystemFontCapture,
|
|
@@ -7982,7 +8362,7 @@ async function runCompileStage(input) {
|
|
|
7982
8362
|
variables: input.variables,
|
|
7983
8363
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
7984
8364
|
pageMotionBlur: input.pageMotionBlur,
|
|
7985
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
8365
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join16(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
7986
8366
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
7987
8367
|
});
|
|
7988
8368
|
assertNotAborted();
|
|
@@ -8038,7 +8418,7 @@ async function runCompileStage(input) {
|
|
|
8038
8418
|
};
|
|
8039
8419
|
await preflightCompositionAssetMediaTypes({
|
|
8040
8420
|
projectDir,
|
|
8041
|
-
compiledDir:
|
|
8421
|
+
compiledDir: join16(workDir, "compiled"),
|
|
8042
8422
|
composition,
|
|
8043
8423
|
signal: abortSignal
|
|
8044
8424
|
});
|
|
@@ -8083,8 +8463,8 @@ async function runCompileStage(input) {
|
|
|
8083
8463
|
}
|
|
8084
8464
|
|
|
8085
8465
|
// src/services/render/stages/probeStage.ts
|
|
8086
|
-
import { join as
|
|
8087
|
-
import { parseHTML as
|
|
8466
|
+
import { join as join17 } from "node:path";
|
|
8467
|
+
import { parseHTML as parseHTML6 } from "linkedom";
|
|
8088
8468
|
import {
|
|
8089
8469
|
closeCaptureSession as closeCaptureSession4,
|
|
8090
8470
|
compositionRequiresWebGpu,
|
|
@@ -8098,13 +8478,14 @@ import {
|
|
|
8098
8478
|
} from "@hyperframes/engine";
|
|
8099
8479
|
import {
|
|
8100
8480
|
MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
|
|
8101
|
-
fpsToNumber as
|
|
8481
|
+
fpsToNumber as fpsToNumber6,
|
|
8102
8482
|
normalizeRateSpec as normalizeRateSpec3,
|
|
8103
8483
|
readMediaStart as readMediaStart2,
|
|
8104
8484
|
shiftRateLane as shiftRateLane2,
|
|
8105
8485
|
sourceTimeAt as sourceTimeAt3,
|
|
8106
8486
|
timeAtSourceTime
|
|
8107
8487
|
} from "@hyperframes/core";
|
|
8488
|
+
import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
|
|
8108
8489
|
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
8109
8490
|
|
|
8110
8491
|
// src/services/render/stages/probeFailures.ts
|
|
@@ -8131,7 +8512,7 @@ function durationToFrameCount(duration, fps) {
|
|
|
8131
8512
|
}
|
|
8132
8513
|
function hasScriptedAudioVolumeAutomation(html, audioCount) {
|
|
8133
8514
|
if (audioCount <= 0) return false;
|
|
8134
|
-
const { document: document2 } =
|
|
8515
|
+
const { document: document2 } = parseHTML6(html);
|
|
8135
8516
|
const scriptBodies = [...document2.querySelectorAll("script")].map((script) => script.textContent ?? "").join("\n");
|
|
8136
8517
|
if (!scriptBodies) return false;
|
|
8137
8518
|
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(
|
|
@@ -8139,23 +8520,30 @@ function hasScriptedAudioVolumeAutomation(html, audioCount) {
|
|
|
8139
8520
|
);
|
|
8140
8521
|
}
|
|
8141
8522
|
function hasAutoStartVideos(html) {
|
|
8142
|
-
const { document: document2 } =
|
|
8523
|
+
const { document: document2 } = parseHTML6(html);
|
|
8143
8524
|
return document2.querySelector("video[data-hf-auto-start]") !== null;
|
|
8144
8525
|
}
|
|
8526
|
+
function variableHasValue(element, variableId, variables) {
|
|
8527
|
+
if (variables && Object.hasOwn(variables, variableId)) return true;
|
|
8528
|
+
for (let scope = element.parentElement; scope; scope = scope.parentElement) {
|
|
8529
|
+
if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
|
|
8530
|
+
if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
|
|
8531
|
+
}
|
|
8532
|
+
return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
|
|
8533
|
+
}
|
|
8145
8534
|
function hasVariableBoundMedia(html, variables) {
|
|
8146
|
-
|
|
8147
|
-
const { document: document2 } = parseHTML5(html);
|
|
8535
|
+
const { document: document2 } = parseHTML6(html);
|
|
8148
8536
|
return Array.from(
|
|
8149
8537
|
document2.querySelectorAll(
|
|
8150
8538
|
"img[data-var-src], audio[data-var-src], video[data-var-src], source[data-var-src]"
|
|
8151
8539
|
)
|
|
8152
8540
|
).some((element) => {
|
|
8153
8541
|
const variableId = element.getAttribute("data-var-src")?.trim();
|
|
8154
|
-
return Boolean(variableId &&
|
|
8542
|
+
return Boolean(variableId && variableHasValue(element, variableId, variables));
|
|
8155
8543
|
});
|
|
8156
8544
|
}
|
|
8157
8545
|
function authoredMediaStarts(html) {
|
|
8158
|
-
const { document: document2 } =
|
|
8546
|
+
const { document: document2 } = parseHTML6(html);
|
|
8159
8547
|
const starts = /* @__PURE__ */ new Map();
|
|
8160
8548
|
for (const media of document2.querySelectorAll("video, audio")) {
|
|
8161
8549
|
const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
|
|
@@ -8192,7 +8580,7 @@ function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, dura
|
|
|
8192
8580
|
return existingEnd <= 0 ? projectedEnd : Math.min(existingEnd, projectedEnd);
|
|
8193
8581
|
}
|
|
8194
8582
|
function hasRuntimeMediaChanges(html) {
|
|
8195
|
-
const { document: document2 } =
|
|
8583
|
+
const { document: document2 } = parseHTML6(html);
|
|
8196
8584
|
const scriptBodies = [...document2.querySelectorAll("script")].map(
|
|
8197
8585
|
(script) => script.textContent ?? ""
|
|
8198
8586
|
);
|
|
@@ -8275,7 +8663,7 @@ async function runProbeStage(input) {
|
|
|
8275
8663
|
});
|
|
8276
8664
|
fileServer = await createFileServer({
|
|
8277
8665
|
projectDir,
|
|
8278
|
-
compiledDir:
|
|
8666
|
+
compiledDir: join17(workDir, "compiled"),
|
|
8279
8667
|
port: 0,
|
|
8280
8668
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
8281
8669
|
fps: job.config.fps
|
|
@@ -8304,7 +8692,7 @@ async function runProbeStage(input) {
|
|
|
8304
8692
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
8305
8693
|
probeSession = await createCaptureSession4(
|
|
8306
8694
|
fileServer.url,
|
|
8307
|
-
|
|
8695
|
+
join17(workDir, "probe"),
|
|
8308
8696
|
captureOpts,
|
|
8309
8697
|
null,
|
|
8310
8698
|
probeCfg
|
|
@@ -8389,7 +8777,7 @@ async function runProbeStage(input) {
|
|
|
8389
8777
|
});
|
|
8390
8778
|
probeSession = await createCaptureSession4(
|
|
8391
8779
|
fileServer.url,
|
|
8392
|
-
|
|
8780
|
+
join17(workDir, "probe-screenshot"),
|
|
8393
8781
|
captureOpts,
|
|
8394
8782
|
null,
|
|
8395
8783
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -8425,7 +8813,7 @@ async function runProbeStage(input) {
|
|
|
8425
8813
|
compiled,
|
|
8426
8814
|
resolutions,
|
|
8427
8815
|
projectDir,
|
|
8428
|
-
|
|
8816
|
+
join17(workDir, "downloads")
|
|
8429
8817
|
);
|
|
8430
8818
|
assertNotAborted();
|
|
8431
8819
|
composition.videos = compiled.videos;
|
|
@@ -8577,7 +8965,7 @@ async function runProbeStage(input) {
|
|
|
8577
8965
|
session.page,
|
|
8578
8966
|
composition.audios.map((audio) => audio.id),
|
|
8579
8967
|
composition.duration,
|
|
8580
|
-
|
|
8968
|
+
fpsToNumber6(job.config.fps)
|
|
8581
8969
|
);
|
|
8582
8970
|
assertNotAborted();
|
|
8583
8971
|
if (automation.length > 0) {
|
|
@@ -8616,7 +9004,7 @@ async function runProbeStage(input) {
|
|
|
8616
9004
|
}
|
|
8617
9005
|
await preflightCompositionAssetMediaTypes({
|
|
8618
9006
|
projectDir,
|
|
8619
|
-
compiledDir:
|
|
9007
|
+
compiledDir: join17(workDir, "compiled"),
|
|
8620
9008
|
composition,
|
|
8621
9009
|
signal: abortSignal
|
|
8622
9010
|
});
|
|
@@ -8640,7 +9028,7 @@ async function runProbeStage(input) {
|
|
|
8640
9028
|
}
|
|
8641
9029
|
const browserProbeMs = Date.now() - probeStart;
|
|
8642
9030
|
const duration = composition.duration;
|
|
8643
|
-
const totalFrames = durationToFrameCount(duration,
|
|
9031
|
+
const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
|
|
8644
9032
|
if (duration <= 0) {
|
|
8645
9033
|
const diagnostics = [];
|
|
8646
9034
|
try {
|
|
@@ -8750,7 +9138,7 @@ function validateRenderDuration(input) {
|
|
|
8750
9138
|
|
|
8751
9139
|
// src/services/render/stages/extractVideosStage.ts
|
|
8752
9140
|
import { existsSync as existsSync11 } from "node:fs";
|
|
8753
|
-
import { join as
|
|
9141
|
+
import { join as join18 } from "node:path";
|
|
8754
9142
|
import {
|
|
8755
9143
|
classifyVideoExtractionError,
|
|
8756
9144
|
createFrameLookupTable,
|
|
@@ -9065,14 +9453,14 @@ async function runExtractVideosStage(input) {
|
|
|
9065
9453
|
// because short boundary counts can differ by one frame.
|
|
9066
9454
|
{
|
|
9067
9455
|
fps: job.config.fps,
|
|
9068
|
-
outputDir:
|
|
9456
|
+
outputDir: join18(compiledDir, "__hyperframes_video_frames"),
|
|
9069
9457
|
format: job.config.videoFrameFormat ?? "auto",
|
|
9070
9458
|
toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
|
|
9071
9459
|
timelineEnd: composition.duration,
|
|
9072
9460
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
9073
9461
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
9074
9462
|
deferRangeExtraction,
|
|
9075
|
-
contentKeyedDirs: [
|
|
9463
|
+
contentKeyedDirs: [join18(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
9076
9464
|
urlByAbsentLocalPath
|
|
9077
9465
|
},
|
|
9078
9466
|
abortSignal,
|
|
@@ -9139,18 +9527,19 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
9139
9527
|
}
|
|
9140
9528
|
|
|
9141
9529
|
// src/services/render/stages/audioStage.ts
|
|
9142
|
-
import { join as
|
|
9143
|
-
import { rmSync as rmSync9 } from "node:fs";
|
|
9530
|
+
import { join as join20 } from "node:path";
|
|
9144
9531
|
import {
|
|
9145
9532
|
MIXED_AUDIO_FILENAME,
|
|
9146
9533
|
audioFailureDiagnostics,
|
|
9147
|
-
processCompositionAudio
|
|
9534
|
+
processCompositionAudio,
|
|
9535
|
+
removeScratch as removeScratch2,
|
|
9536
|
+
collectScratchLeftovers
|
|
9148
9537
|
} from "@hyperframes/engine";
|
|
9149
9538
|
|
|
9150
9539
|
// src/services/render/audioPadTrim.ts
|
|
9151
9540
|
import { spawn } from "node:child_process";
|
|
9152
|
-
import { mkdtempSync as mkdtempSync3
|
|
9153
|
-
import { dirname as
|
|
9541
|
+
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
9542
|
+
import { dirname as dirname8, join as join19 } from "node:path";
|
|
9154
9543
|
import {
|
|
9155
9544
|
DEFAULT_CONFIG,
|
|
9156
9545
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -9158,8 +9547,9 @@ import {
|
|
|
9158
9547
|
extractAudioMetadata as extractAudioMetadata3,
|
|
9159
9548
|
getFfprobeBinary,
|
|
9160
9549
|
ManagedChildProcess,
|
|
9161
|
-
|
|
9162
|
-
|
|
9550
|
+
removeScratch,
|
|
9551
|
+
renameWithRetry as renameWithRetry5,
|
|
9552
|
+
runFfmpeg as runFfmpeg5,
|
|
9163
9553
|
trackChildProcess
|
|
9164
9554
|
} from "@hyperframes/engine";
|
|
9165
9555
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
@@ -9169,7 +9559,7 @@ import {
|
|
|
9169
9559
|
aacSegmentCount,
|
|
9170
9560
|
describeFfmpegFailure,
|
|
9171
9561
|
extractAudioMetadata as extractAudioMetadata2,
|
|
9172
|
-
runFfmpeg as
|
|
9562
|
+
runFfmpeg as runFfmpeg4
|
|
9173
9563
|
} from "@hyperframes/engine";
|
|
9174
9564
|
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
9175
9565
|
function checkedDbfs(value) {
|
|
@@ -9193,7 +9583,7 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
9193
9583
|
else laterBlocks = Math.max(laterBlocks, block);
|
|
9194
9584
|
}
|
|
9195
9585
|
};
|
|
9196
|
-
const result = await
|
|
9586
|
+
const result = await runFfmpeg4(
|
|
9197
9587
|
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
9198
9588
|
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
9199
9589
|
);
|
|
@@ -9353,7 +9743,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9353
9743
|
error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9354
9744
|
};
|
|
9355
9745
|
} finally {
|
|
9356
|
-
for (const path of plan.cleanupPaths)
|
|
9746
|
+
for (const path of plan.cleanupPaths) removeScratch(path);
|
|
9357
9747
|
}
|
|
9358
9748
|
let loweredDb = 0;
|
|
9359
9749
|
if (probeTruePeak) {
|
|
@@ -9400,8 +9790,8 @@ async function constrainAacTruePeak(input) {
|
|
|
9400
9790
|
async function enforceAacTruePeak(input) {
|
|
9401
9791
|
let scratchDir;
|
|
9402
9792
|
try {
|
|
9403
|
-
scratchDir = mkdtempSync3(
|
|
9404
|
-
const correctedPath =
|
|
9793
|
+
scratchDir = mkdtempSync3(join19(dirname8(input.audioPath), ".true-peak-"));
|
|
9794
|
+
const correctedPath = join19(scratchDir, "audio.m4a");
|
|
9405
9795
|
const measurements = [];
|
|
9406
9796
|
let attenuationDb = 0;
|
|
9407
9797
|
let sourcePeakDbfs;
|
|
@@ -9416,7 +9806,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9416
9806
|
}
|
|
9417
9807
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
9418
9808
|
if (measuredPath !== correctedPath) return { success: true };
|
|
9419
|
-
|
|
9809
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
9420
9810
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
9421
9811
|
}
|
|
9422
9812
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -9457,7 +9847,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9457
9847
|
)}`
|
|
9458
9848
|
};
|
|
9459
9849
|
} finally {
|
|
9460
|
-
if (scratchDir)
|
|
9850
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
9461
9851
|
}
|
|
9462
9852
|
}
|
|
9463
9853
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9534,7 +9924,7 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
9534
9924
|
};
|
|
9535
9925
|
}
|
|
9536
9926
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
9537
|
-
return toStepResult(await
|
|
9927
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
9538
9928
|
}
|
|
9539
9929
|
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
9540
9930
|
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
@@ -9602,9 +9992,17 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9602
9992
|
])
|
|
9603
9993
|
});
|
|
9604
9994
|
}
|
|
9605
|
-
|
|
9995
|
+
function runAudioStage(input) {
|
|
9996
|
+
return collectScratchLeftovers(
|
|
9997
|
+
() => runAudioStageScoped(input),
|
|
9998
|
+
(left) => {
|
|
9999
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
10000
|
+
}
|
|
10001
|
+
);
|
|
10002
|
+
}
|
|
10003
|
+
async function runAudioStageScoped(input) {
|
|
9606
10004
|
const stageStart = Date.now();
|
|
9607
|
-
const pcmSourcePath =
|
|
10005
|
+
const pcmSourcePath = join20(input.workDir, "audio-source.wav");
|
|
9608
10006
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
9609
10007
|
try {
|
|
9610
10008
|
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
@@ -9624,7 +10022,7 @@ async function runAudioStage(input) {
|
|
|
9624
10022
|
}
|
|
9625
10023
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9626
10024
|
} finally {
|
|
9627
|
-
|
|
10025
|
+
removeScratch2(pcmSourcePath);
|
|
9628
10026
|
}
|
|
9629
10027
|
}
|
|
9630
10028
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -9642,7 +10040,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9642
10040
|
assertNotAborted,
|
|
9643
10041
|
log
|
|
9644
10042
|
} = input;
|
|
9645
|
-
const audioOutputPath =
|
|
10043
|
+
const audioOutputPath = join20(workDir, MIXED_AUDIO_FILENAME);
|
|
9646
10044
|
let hasAudio = false;
|
|
9647
10045
|
let audioError;
|
|
9648
10046
|
let audioFailures;
|
|
@@ -9653,7 +10051,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9653
10051
|
audioResult = await processCompositionAudio(
|
|
9654
10052
|
audios,
|
|
9655
10053
|
projectDir,
|
|
9656
|
-
|
|
10054
|
+
join20(workDir, "audio-work"),
|
|
9657
10055
|
audioOutputPath,
|
|
9658
10056
|
duration,
|
|
9659
10057
|
abortSignal,
|
|
@@ -9722,7 +10120,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9722
10120
|
|
|
9723
10121
|
// src/services/render/stages/captureStage.ts
|
|
9724
10122
|
import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
|
|
9725
|
-
import { join as
|
|
10123
|
+
import { join as join21 } from "node:path";
|
|
9726
10124
|
import {
|
|
9727
10125
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
9728
10126
|
frameFileExtension,
|
|
@@ -9795,13 +10193,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
9795
10193
|
if (!existsSync12(workDir)) return total;
|
|
9796
10194
|
for (const attempt of readdirSync3(workDir)) {
|
|
9797
10195
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
9798
|
-
total += sampleDirectoryBytes(
|
|
10196
|
+
total += sampleDirectoryBytes(join21(workDir, attempt));
|
|
9799
10197
|
continue;
|
|
9800
10198
|
}
|
|
9801
10199
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
9802
|
-
const attemptDir =
|
|
10200
|
+
const attemptDir = join21(workDir, attempt);
|
|
9803
10201
|
for (const name of readdirSync3(attemptDir)) {
|
|
9804
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
10202
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join21(attemptDir, name));
|
|
9805
10203
|
}
|
|
9806
10204
|
}
|
|
9807
10205
|
return total;
|
|
@@ -10094,8 +10492,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
|
|
|
10094
10492
|
}
|
|
10095
10493
|
|
|
10096
10494
|
// src/services/render/stages/captureHdrStage.ts
|
|
10097
|
-
import { existsSync as existsSync14, mkdirSync as
|
|
10098
|
-
import { join as
|
|
10495
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "node:fs";
|
|
10496
|
+
import { join as join25 } from "node:path";
|
|
10099
10497
|
import {
|
|
10100
10498
|
closeCaptureSession as closeCaptureSession7,
|
|
10101
10499
|
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
@@ -10104,7 +10502,7 @@ import {
|
|
|
10104
10502
|
initializeSession as initializeSession7,
|
|
10105
10503
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
10106
10504
|
} from "@hyperframes/engine";
|
|
10107
|
-
import { fpsToNumber as
|
|
10505
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
10108
10506
|
|
|
10109
10507
|
// src/services/hdrImageTransferCache.ts
|
|
10110
10508
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -10167,7 +10565,7 @@ function createHdrImageTransferCache(options = {}) {
|
|
|
10167
10565
|
}
|
|
10168
10566
|
|
|
10169
10567
|
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
10170
|
-
import { join as
|
|
10568
|
+
import { join as join22 } from "node:path";
|
|
10171
10569
|
import {
|
|
10172
10570
|
TRANSITIONS,
|
|
10173
10571
|
crossfade
|
|
@@ -10292,7 +10690,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
10292
10690
|
);
|
|
10293
10691
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
10294
10692
|
writeFileExclusiveSync(
|
|
10295
|
-
|
|
10693
|
+
join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
10296
10694
|
normalCanvas
|
|
10297
10695
|
);
|
|
10298
10696
|
}
|
|
@@ -10324,7 +10722,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
10324
10722
|
}
|
|
10325
10723
|
|
|
10326
10724
|
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
10327
|
-
import { join as
|
|
10725
|
+
import { join as join24 } from "node:path";
|
|
10328
10726
|
import {
|
|
10329
10727
|
TRANSITIONS as TRANSITIONS2,
|
|
10330
10728
|
closeCaptureSession as closeCaptureSession6,
|
|
@@ -10338,7 +10736,7 @@ import {
|
|
|
10338
10736
|
// src/services/shaderTransitionWorkerPool.ts
|
|
10339
10737
|
import { Worker } from "node:worker_threads";
|
|
10340
10738
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
10341
|
-
import { dirname as
|
|
10739
|
+
import { dirname as dirname9, join as join23 } from "node:path";
|
|
10342
10740
|
import { createRequire } from "node:module";
|
|
10343
10741
|
import { existsSync as existsSync13 } from "node:fs";
|
|
10344
10742
|
import { cpus as cpus2 } from "node:os";
|
|
@@ -10351,10 +10749,10 @@ function resolveWorkerEntry(explicit) {
|
|
|
10351
10749
|
const isTs = override.endsWith(".ts");
|
|
10352
10750
|
return { path: override, isTs };
|
|
10353
10751
|
}
|
|
10354
|
-
const moduleDir =
|
|
10355
|
-
const jsPath =
|
|
10752
|
+
const moduleDir = dirname9(fileURLToPath3(import.meta.url));
|
|
10753
|
+
const jsPath = join23(moduleDir, "shaderTransitionWorker.js");
|
|
10356
10754
|
if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
|
|
10357
|
-
const tsPath =
|
|
10755
|
+
const tsPath = join23(moduleDir, "shaderTransitionWorker.ts");
|
|
10358
10756
|
return { path: tsPath, isTs: true };
|
|
10359
10757
|
}
|
|
10360
10758
|
function buildExecArgv(entryIsTs) {
|
|
@@ -10497,8 +10895,8 @@ async function createShaderTransitionWorkerPool(opts) {
|
|
|
10497
10895
|
if (terminated) {
|
|
10498
10896
|
throw new Error("shader-blend pool already terminated");
|
|
10499
10897
|
}
|
|
10500
|
-
return new Promise((
|
|
10501
|
-
const task = traceEnabled ? { req, resolve:
|
|
10898
|
+
return new Promise((resolve11, reject) => {
|
|
10899
|
+
const task = traceEnabled ? { req, resolve: resolve11, reject, enqueuedAtMs: Date.now(), traceId: ++nextTaskId } : { req, resolve: resolve11, reject };
|
|
10502
10900
|
const idle = slots.find((s) => !s.busy && !s.dead);
|
|
10503
10901
|
if (idle) {
|
|
10504
10902
|
queue.unshift(task);
|
|
@@ -10722,7 +11120,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
10722
11120
|
);
|
|
10723
11121
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
10724
11122
|
writeFileExclusiveSync(
|
|
10725
|
-
|
|
11123
|
+
join24(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
10726
11124
|
canvas
|
|
10727
11125
|
);
|
|
10728
11126
|
}
|
|
@@ -10778,6 +11176,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10778
11176
|
preset,
|
|
10779
11177
|
effectiveQuality,
|
|
10780
11178
|
effectiveBitrate,
|
|
11179
|
+
deliveryEncoder,
|
|
10781
11180
|
fileServer,
|
|
10782
11181
|
buildCaptureOptions,
|
|
10783
11182
|
createRenderVideoFrameInjector,
|
|
@@ -10839,7 +11238,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10839
11238
|
}
|
|
10840
11239
|
return map;
|
|
10841
11240
|
});
|
|
10842
|
-
const fpsDecimal =
|
|
11241
|
+
const fpsDecimal = fpsToNumber7(job.config.fps);
|
|
10843
11242
|
const transitionRanges = transitionMeta.map((t) => ({
|
|
10844
11243
|
...t,
|
|
10845
11244
|
startFrame: Math.floor(t.time * fpsDecimal),
|
|
@@ -10867,6 +11266,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10867
11266
|
preset: preset.preset,
|
|
10868
11267
|
quality: effectiveQuality,
|
|
10869
11268
|
bitrate: effectiveBitrate,
|
|
11269
|
+
...deliveryEncoder,
|
|
10870
11270
|
pixelFormat: preset.pixelFormat,
|
|
10871
11271
|
hdr: preset.hdr,
|
|
10872
11272
|
rawInputFormat: "rgb48le"
|
|
@@ -10910,9 +11310,9 @@ async function runCaptureHdrStage(input) {
|
|
|
10910
11310
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
10911
11311
|
}
|
|
10912
11312
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
10913
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
11313
|
+
const debugDumpDir = debugDumpEnabled ? join25(framesDir, "debug-composite") : null;
|
|
10914
11314
|
if (debugDumpDir && !existsSync14(debugDumpDir)) {
|
|
10915
|
-
|
|
11315
|
+
mkdirSync8(debugDumpDir, { recursive: true });
|
|
10916
11316
|
}
|
|
10917
11317
|
const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
|
|
10918
11318
|
const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
|
|
@@ -10926,7 +11326,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10926
11326
|
beforeCaptureHook: domSession.onBeforeCapture,
|
|
10927
11327
|
width,
|
|
10928
11328
|
height,
|
|
10929
|
-
fps:
|
|
11329
|
+
fps: fpsToNumber7(job.config.fps),
|
|
10930
11330
|
compositeTransfer,
|
|
10931
11331
|
nativeHdrImageIds,
|
|
10932
11332
|
hdrImageBuffers,
|
|
@@ -11065,24 +11465,24 @@ async function runCaptureHdrStage(input) {
|
|
|
11065
11465
|
|
|
11066
11466
|
// src/services/render/stages/encodeStage.ts
|
|
11067
11467
|
import {
|
|
11068
|
-
copyFileSync as
|
|
11468
|
+
copyFileSync as copyFileSync4,
|
|
11069
11469
|
existsSync as existsSync15,
|
|
11070
|
-
mkdirSync as
|
|
11470
|
+
mkdirSync as mkdirSync9,
|
|
11071
11471
|
readdirSync as readdirSync4,
|
|
11072
|
-
readFileSync as
|
|
11073
|
-
rmSync as
|
|
11074
|
-
statSync as
|
|
11075
|
-
writeFileSync as
|
|
11472
|
+
readFileSync as readFileSync8,
|
|
11473
|
+
rmSync as rmSync9,
|
|
11474
|
+
statSync as statSync7,
|
|
11475
|
+
writeFileSync as writeFileSync5
|
|
11076
11476
|
} from "node:fs";
|
|
11077
11477
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
11078
|
-
import { dirname as
|
|
11478
|
+
import { dirname as dirname10, join as join26 } from "node:path";
|
|
11079
11479
|
import {
|
|
11080
11480
|
encodeFramesChunkedConcat,
|
|
11081
11481
|
encodeFramesFromDir,
|
|
11082
11482
|
formatFfmpegError,
|
|
11083
11483
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
11084
11484
|
resolveConfig,
|
|
11085
|
-
runFfmpeg as
|
|
11485
|
+
runFfmpeg as runFfmpeg6,
|
|
11086
11486
|
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
11087
11487
|
} from "@hyperframes/engine";
|
|
11088
11488
|
import {
|
|
@@ -11158,17 +11558,17 @@ function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
|
11158
11558
|
}
|
|
11159
11559
|
async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
11160
11560
|
const { outputPath } = argsInput;
|
|
11161
|
-
const result = await
|
|
11561
|
+
const result = await runFfmpeg6(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
|
|
11162
11562
|
if (!result.success) {
|
|
11163
11563
|
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
11164
11564
|
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
11165
11565
|
}
|
|
11166
|
-
const gif =
|
|
11566
|
+
const gif = readFileSync8(outputPath);
|
|
11167
11567
|
if (!clearGifFramesBeforeNext(gif)) {
|
|
11168
11568
|
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
11169
11569
|
return gifEncodeFailure(startTime, outputPath, error);
|
|
11170
11570
|
}
|
|
11171
|
-
|
|
11571
|
+
writeFileSync5(outputPath, gif);
|
|
11172
11572
|
return null;
|
|
11173
11573
|
}
|
|
11174
11574
|
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
@@ -11190,17 +11590,17 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11190
11590
|
try {
|
|
11191
11591
|
const run = { signal: input.signal, timeout: input.timeout };
|
|
11192
11592
|
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
11193
|
-
const result = await
|
|
11593
|
+
const result = await runFfmpeg6(args, run);
|
|
11194
11594
|
if (!result.success) {
|
|
11195
11595
|
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
11196
11596
|
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
11197
11597
|
}
|
|
11198
11598
|
}
|
|
11199
|
-
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(
|
|
11599
|
+
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
|
|
11200
11600
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
11201
11601
|
if (failure) return failure;
|
|
11202
11602
|
}
|
|
11203
|
-
const fileSize = existsSync15(outputPath) ?
|
|
11603
|
+
const fileSize = existsSync15(outputPath) ? statSync7(outputPath).size : 0;
|
|
11204
11604
|
return {
|
|
11205
11605
|
success: true,
|
|
11206
11606
|
outputPath,
|
|
@@ -11209,7 +11609,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
11209
11609
|
fileSize
|
|
11210
11610
|
};
|
|
11211
11611
|
} finally {
|
|
11212
|
-
|
|
11612
|
+
rmSync9(input.palettePath, { force: true });
|
|
11213
11613
|
}
|
|
11214
11614
|
}
|
|
11215
11615
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -11233,6 +11633,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
11233
11633
|
preset: preset.preset,
|
|
11234
11634
|
quality: input.effectiveQuality,
|
|
11235
11635
|
bitrate: input.effectiveBitrate,
|
|
11636
|
+
maxBitrate: input.maxBitrate,
|
|
11637
|
+
bufferSize: input.bufferSize,
|
|
11236
11638
|
pixelFormat: preset.pixelFormat,
|
|
11237
11639
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
11238
11640
|
useGpu: job.config.useGpu,
|
|
@@ -11281,10 +11683,10 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
11281
11683
|
const stream = await encoder;
|
|
11282
11684
|
for (let next = 0; next < totalFrames; next++) {
|
|
11283
11685
|
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
11284
|
-
await new Promise((
|
|
11686
|
+
await new Promise((resolve11) => wake = resolve11);
|
|
11285
11687
|
}
|
|
11286
11688
|
if (signal.aborted) return;
|
|
11287
|
-
const mergedPath =
|
|
11689
|
+
const mergedPath = join26(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
11288
11690
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
11289
11691
|
landed.delete(next);
|
|
11290
11692
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -11359,7 +11761,7 @@ async function runEncodeStage(input) {
|
|
|
11359
11761
|
const stage5Start = Date.now();
|
|
11360
11762
|
if (isPngSequence) {
|
|
11361
11763
|
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
11362
|
-
if (!existsSync15(outputPath))
|
|
11764
|
+
if (!existsSync15(outputPath)) mkdirSync9(outputPath, { recursive: true });
|
|
11363
11765
|
const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
11364
11766
|
if (captured.length === 0) {
|
|
11365
11767
|
throw new Error(
|
|
@@ -11367,11 +11769,11 @@ async function runEncodeStage(input) {
|
|
|
11367
11769
|
);
|
|
11368
11770
|
}
|
|
11369
11771
|
captured.forEach((name, i) => {
|
|
11370
|
-
const dst =
|
|
11371
|
-
|
|
11772
|
+
const dst = join26(outputPath, formatExportFrameName(i, "png"));
|
|
11773
|
+
copyFileSync4(join26(framesDir, name), dst);
|
|
11372
11774
|
});
|
|
11373
11775
|
if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
|
|
11374
|
-
|
|
11776
|
+
copyFileSync4(audioOutputPath, join26(outputPath, MIXED_AUDIO_FILENAME2));
|
|
11375
11777
|
log.info(
|
|
11376
11778
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
11377
11779
|
);
|
|
@@ -11391,7 +11793,7 @@ async function runEncodeStage(input) {
|
|
|
11391
11793
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
11392
11794
|
fps: job.config.fps,
|
|
11393
11795
|
loop,
|
|
11394
|
-
palettePath:
|
|
11796
|
+
palettePath: join26(dirname10(videoOnlyPath), "gif-palette.png"),
|
|
11395
11797
|
preserveAlpha: needsAlpha,
|
|
11396
11798
|
signal: abortSignal,
|
|
11397
11799
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -11601,9 +12003,9 @@ function sampleDirectoryBytes(dir) {
|
|
|
11601
12003
|
continue;
|
|
11602
12004
|
}
|
|
11603
12005
|
for (const name of entries) {
|
|
11604
|
-
const full =
|
|
12006
|
+
const full = join27(current, name);
|
|
11605
12007
|
try {
|
|
11606
|
-
const st =
|
|
12008
|
+
const st = statSync8(full);
|
|
11607
12009
|
if (st.isDirectory()) {
|
|
11608
12010
|
stack.push(full);
|
|
11609
12011
|
} else if (st.isFile()) {
|
|
@@ -11750,8 +12152,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11750
12152
|
const ranges = [];
|
|
11751
12153
|
let rangeStart = null;
|
|
11752
12154
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11753
|
-
const framePath =
|
|
11754
|
-
const missing = !existsSync16(framePath) ||
|
|
12155
|
+
const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
12156
|
+
const missing = !existsSync16(framePath) || statSync8(framePath).size <= 8;
|
|
11755
12157
|
if (missing && rangeStart === null) {
|
|
11756
12158
|
rangeStart = frameIndex;
|
|
11757
12159
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -11767,7 +12169,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11767
12169
|
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
11768
12170
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
11769
12171
|
const batches = [];
|
|
11770
|
-
const outputDirFor = (batchIndex, workerId) =>
|
|
12172
|
+
const outputDirFor = (batchIndex, workerId) => join27(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
11771
12173
|
if (stride > 1) {
|
|
11772
12174
|
const spans = /* @__PURE__ */ new Map();
|
|
11773
12175
|
for (const range of ranges) {
|
|
@@ -11827,8 +12229,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11827
12229
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11828
12230
|
}
|
|
11829
12231
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11830
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
11831
|
-
return
|
|
12232
|
+
if (platform2 === "win32" || platform2 === "darwin") return join27(systemTempDir, "hf-render-");
|
|
12233
|
+
return join27(dirname11(outputPath), `work-${jobId}-`);
|
|
11832
12234
|
}
|
|
11833
12235
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
11834
12236
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -11870,7 +12272,7 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
11870
12272
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
11871
12273
|
let captured = 0;
|
|
11872
12274
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11873
|
-
const framePath =
|
|
12275
|
+
const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11874
12276
|
if (existsSync16(framePath)) captured++;
|
|
11875
12277
|
}
|
|
11876
12278
|
return captured;
|
|
@@ -11896,7 +12298,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11896
12298
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
11897
12299
|
});
|
|
11898
12300
|
pendingTransientRetry = false;
|
|
11899
|
-
const attemptWorkDir =
|
|
12301
|
+
const attemptWorkDir = join27(options.workDir, `capture-attempt-${attempt}`);
|
|
11900
12302
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
11901
12303
|
missingRanges,
|
|
11902
12304
|
currentWorkers,
|
|
@@ -12502,18 +12904,19 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
12502
12904
|
};
|
|
12503
12905
|
}
|
|
12504
12906
|
function resolveRenderDebugDir() {
|
|
12505
|
-
const producerRoot = process.env.PRODUCER_RENDERS_DIR ?
|
|
12506
|
-
return
|
|
12907
|
+
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname11(fileURLToPath4(import.meta.url)), "../..");
|
|
12908
|
+
return join27(producerRoot, ".debug");
|
|
12507
12909
|
}
|
|
12508
12910
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12509
12911
|
validateHlsRenderConfig(job.config);
|
|
12912
|
+
validateDeliveryEncoderConfig(job.config);
|
|
12510
12913
|
const debugDir = resolveRenderDebugDir();
|
|
12511
|
-
const outputDir =
|
|
12512
|
-
if (!existsSync16(outputDir))
|
|
12513
|
-
const workDir = job.config.debug ?
|
|
12914
|
+
const outputDir = dirname11(outputPath);
|
|
12915
|
+
if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
|
|
12916
|
+
const workDir = job.config.debug ? join27(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
12514
12917
|
const pipelineStart = Date.now();
|
|
12515
12918
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
12516
|
-
const logPath = job.config.debug ?
|
|
12919
|
+
const logPath = job.config.debug ? join27(workDir, "render.log") : null;
|
|
12517
12920
|
const execution = new RenderExecutionContext({
|
|
12518
12921
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
12519
12922
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -12527,7 +12930,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12527
12930
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12528
12931
|
return;
|
|
12529
12932
|
}
|
|
12530
|
-
|
|
12933
|
+
rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12531
12934
|
});
|
|
12532
12935
|
try {
|
|
12533
12936
|
await executeRenderPipeline({
|
|
@@ -12573,7 +12976,7 @@ async function executeRenderPipeline(input) {
|
|
|
12573
12976
|
imageDecodeFailures: 0
|
|
12574
12977
|
};
|
|
12575
12978
|
let hdrPerf;
|
|
12576
|
-
const perfOutputPath =
|
|
12979
|
+
const perfOutputPath = join27(workDir, "perf-summary.json");
|
|
12577
12980
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
12578
12981
|
const observability = new RenderObservabilityRecorder({
|
|
12579
12982
|
pipelineStartMs: pipelineStart,
|
|
@@ -12585,11 +12988,14 @@ async function executeRenderPipeline(input) {
|
|
|
12585
12988
|
const isGif = outputFormat === "gif";
|
|
12586
12989
|
const isHls = outputFormat === "hls";
|
|
12587
12990
|
const hlsSegmentSeconds = isHls ? resolveHlsSegmentSeconds(job.config) : void 0;
|
|
12588
|
-
const
|
|
12589
|
-
outputFormat,
|
|
12590
|
-
job.config.fps,
|
|
12591
|
-
job.config.hlsSegmentSeconds
|
|
12592
|
-
|
|
12991
|
+
const deliveryEncoder = resolveDeliveryEncoderOptions({
|
|
12992
|
+
format: outputFormat,
|
|
12993
|
+
fps: job.config.fps,
|
|
12994
|
+
hlsSegmentSeconds: job.config.hlsSegmentSeconds,
|
|
12995
|
+
keyframeIntervalSeconds: job.config.keyframeIntervalSeconds,
|
|
12996
|
+
maxBitrate: job.config.maxBitrate,
|
|
12997
|
+
bufferSize: job.config.bufferSize
|
|
12998
|
+
});
|
|
12593
12999
|
const artifactTransaction = new ArtifactTransaction(
|
|
12594
13000
|
outputPath,
|
|
12595
13001
|
isPngSequence || isHls ? "directory" : "file",
|
|
@@ -12646,7 +13052,7 @@ async function executeRenderPipeline(input) {
|
|
|
12646
13052
|
job.startedAt = /* @__PURE__ */ new Date();
|
|
12647
13053
|
assertNotAborted();
|
|
12648
13054
|
assertConfiguredFfmpegBinariesExist();
|
|
12649
|
-
if (!existsSync16(workDir))
|
|
13055
|
+
if (!existsSync16(workDir)) mkdirSync10(workDir, { recursive: true });
|
|
12650
13056
|
if (job.config.debug) {
|
|
12651
13057
|
log.info("[Render] Debug artifacts enabled", { workDir, logPath });
|
|
12652
13058
|
}
|
|
@@ -12675,22 +13081,22 @@ async function executeRenderPipeline(input) {
|
|
|
12675
13081
|
requestedWorkers: job.config.workers ?? "auto"
|
|
12676
13082
|
});
|
|
12677
13083
|
const entryFile = job.config.entryFile || "index.html";
|
|
12678
|
-
let htmlPath =
|
|
13084
|
+
let htmlPath = join27(projectDir, entryFile);
|
|
12679
13085
|
if (!existsSync16(htmlPath)) {
|
|
12680
13086
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
12681
13087
|
}
|
|
12682
13088
|
assertNotAborted();
|
|
12683
|
-
const rawEntry =
|
|
13089
|
+
const rawEntry = readFileSync9(htmlPath, "utf-8");
|
|
12684
13090
|
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
12685
|
-
const wrapperPath =
|
|
12686
|
-
const projectIndexPath =
|
|
13091
|
+
const wrapperPath = join27(workDir, "standalone-entry.html");
|
|
13092
|
+
const projectIndexPath = join27(projectDir, "index.html");
|
|
12687
13093
|
if (!existsSync16(projectIndexPath)) {
|
|
12688
13094
|
throw new Error(
|
|
12689
13095
|
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
12690
13096
|
);
|
|
12691
13097
|
}
|
|
12692
13098
|
const standaloneHtml = extractStandaloneEntryFromIndex(
|
|
12693
|
-
|
|
13099
|
+
readFileSync9(projectIndexPath, "utf-8"),
|
|
12694
13100
|
entryFile,
|
|
12695
13101
|
rawEntry
|
|
12696
13102
|
);
|
|
@@ -12699,7 +13105,7 @@ async function executeRenderPipeline(input) {
|
|
|
12699
13105
|
`Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
|
|
12700
13106
|
);
|
|
12701
13107
|
}
|
|
12702
|
-
|
|
13108
|
+
writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
|
|
12703
13109
|
htmlPath = wrapperPath;
|
|
12704
13110
|
log.info("Extracted standalone entry from index.html host context", {
|
|
12705
13111
|
entryFile
|
|
@@ -12829,7 +13235,7 @@ async function executeRenderPipeline(input) {
|
|
|
12829
13235
|
validateRenderDuration({
|
|
12830
13236
|
duration: probeResult.duration,
|
|
12831
13237
|
totalFrames,
|
|
12832
|
-
fps:
|
|
13238
|
+
fps: fpsToNumber8(job.config.fps)
|
|
12833
13239
|
});
|
|
12834
13240
|
perfStages.browserProbeMs = probeResult.browserProbeMs;
|
|
12835
13241
|
perfStages.compileMs = Date.now() - stage1Start;
|
|
@@ -12846,7 +13252,7 @@ async function executeRenderPipeline(input) {
|
|
|
12846
13252
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
12847
13253
|
code: "extract_video"
|
|
12848
13254
|
});
|
|
12849
|
-
const compiledDir =
|
|
13255
|
+
const compiledDir = join27(workDir, "compiled");
|
|
12850
13256
|
const extractResult = await observeRenderStage(
|
|
12851
13257
|
observability,
|
|
12852
13258
|
"video_extract",
|
|
@@ -12980,7 +13386,7 @@ async function executeRenderPipeline(input) {
|
|
|
12980
13386
|
try {
|
|
12981
13387
|
fileServer = await createFileServer({
|
|
12982
13388
|
projectDir,
|
|
12983
|
-
compiledDir:
|
|
13389
|
+
compiledDir: join27(workDir, "compiled"),
|
|
12984
13390
|
port: 0,
|
|
12985
13391
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
12986
13392
|
fps: job.config.fps
|
|
@@ -12998,8 +13404,8 @@ async function executeRenderPipeline(input) {
|
|
|
12998
13404
|
if (!activeFileServer) {
|
|
12999
13405
|
throw new Error("File server failed to initialize before frame capture");
|
|
13000
13406
|
}
|
|
13001
|
-
const framesDir =
|
|
13002
|
-
if (!existsSync16(framesDir))
|
|
13407
|
+
const framesDir = join27(workDir, "captured-frames");
|
|
13408
|
+
if (!existsSync16(framesDir)) mkdirSync10(framesDir, { recursive: true });
|
|
13003
13409
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
13004
13410
|
code: "start_browsers"
|
|
13005
13411
|
});
|
|
@@ -13470,7 +13876,7 @@ async function executeRenderPipeline(input) {
|
|
|
13470
13876
|
hls: ".mp4"
|
|
13471
13877
|
};
|
|
13472
13878
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
13473
|
-
const videoOnlyPath =
|
|
13879
|
+
const videoOnlyPath = join27(workDir, `video-only${videoExt}`);
|
|
13474
13880
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
13475
13881
|
if (usePageSideCompositingForTransitions) {
|
|
13476
13882
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -13672,6 +14078,7 @@ async function executeRenderPipeline(input) {
|
|
|
13672
14078
|
preset,
|
|
13673
14079
|
effectiveQuality,
|
|
13674
14080
|
effectiveBitrate,
|
|
14081
|
+
deliveryEncoder,
|
|
13675
14082
|
fileServer: activeFileServer,
|
|
13676
14083
|
buildCaptureOptions,
|
|
13677
14084
|
createRenderVideoFrameInjector,
|
|
@@ -13696,9 +14103,7 @@ async function executeRenderPipeline(input) {
|
|
|
13696
14103
|
} else {
|
|
13697
14104
|
let streamingHandled = false;
|
|
13698
14105
|
if (capturePlan.kind === "sdr_segmented") {
|
|
13699
|
-
const segmentedPlan = capturePlan;
|
|
13700
14106
|
const captureFrameStart = Date.now();
|
|
13701
|
-
resetCaptureAttemptProgress(job);
|
|
13702
14107
|
const segmentFrames = resolveSegmentFrames(process.env);
|
|
13703
14108
|
const segmentImageFormat = captureOptions.format || "jpeg";
|
|
13704
14109
|
const segmentPlanHash = computeSegmentPlanHash({
|
|
@@ -13713,6 +14118,9 @@ async function executeRenderPipeline(input) {
|
|
|
13713
14118
|
preset: preset.preset,
|
|
13714
14119
|
quality: effectiveQuality,
|
|
13715
14120
|
bitrate: effectiveBitrate,
|
|
14121
|
+
maxBitrate: deliveryEncoder.maxBitrate,
|
|
14122
|
+
bufferSize: deliveryEncoder.bufferSize,
|
|
14123
|
+
gopSize: deliveryEncoder.gopSize,
|
|
13716
14124
|
pixelFormat: preset.pixelFormat,
|
|
13717
14125
|
imageFormat: segmentImageFormat,
|
|
13718
14126
|
useGpu: job.config.useGpu === true,
|
|
@@ -13723,7 +14131,7 @@ async function executeRenderPipeline(input) {
|
|
|
13723
14131
|
pageMotionBlur: job.config.pageMotionBlur,
|
|
13724
14132
|
captureQuality: captureOptions.quality
|
|
13725
14133
|
});
|
|
13726
|
-
const segmentDir = segmentDirFor(
|
|
14134
|
+
const segmentDir = segmentDirFor(join27(projectDir, "renders"), segmentPlanHash);
|
|
13727
14135
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
13728
14136
|
if (job.config.resumeSegments === true) {
|
|
13729
14137
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -13738,71 +14146,129 @@ async function executeRenderPipeline(input) {
|
|
|
13738
14146
|
});
|
|
13739
14147
|
}
|
|
13740
14148
|
if (completedSegments.size === 0) {
|
|
13741
|
-
|
|
14149
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13742
14150
|
}
|
|
13743
14151
|
} else {
|
|
13744
|
-
|
|
14152
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13745
14153
|
}
|
|
13746
|
-
const
|
|
14154
|
+
const emptySegmentManifest = () => ({
|
|
13747
14155
|
version: 1,
|
|
13748
14156
|
planHash: segmentPlanHash,
|
|
13749
14157
|
totalFrames,
|
|
13750
14158
|
segmentFrames,
|
|
13751
14159
|
completed: []
|
|
14160
|
+
});
|
|
14161
|
+
let segmentManifest = readSegmentManifest(segmentDir) ?? emptySegmentManifest();
|
|
14162
|
+
const invokeSegmented = () => {
|
|
14163
|
+
if (capturePlan.kind !== "sdr_segmented") {
|
|
14164
|
+
throw new Error(`Cannot invoke segmented stage with ${capturePlan.kind} plan`);
|
|
14165
|
+
}
|
|
14166
|
+
const segmentedPlan = capturePlan;
|
|
14167
|
+
resetCaptureAttemptProgress(job);
|
|
14168
|
+
return observeRenderStage(
|
|
14169
|
+
observability,
|
|
14170
|
+
"capture_segmented",
|
|
14171
|
+
captureStageObservationData(),
|
|
14172
|
+
() => runCaptureSegmentedStage({
|
|
14173
|
+
fileServer: activeFileServer,
|
|
14174
|
+
workDir,
|
|
14175
|
+
framesDir,
|
|
14176
|
+
videoOnlyPath,
|
|
14177
|
+
job,
|
|
14178
|
+
totalFrames,
|
|
14179
|
+
cfg,
|
|
14180
|
+
plan: segmentedPlan,
|
|
14181
|
+
log,
|
|
14182
|
+
probeSession,
|
|
14183
|
+
outputFormat,
|
|
14184
|
+
streamingEncoderOptions: {
|
|
14185
|
+
fps: job.config.fps,
|
|
14186
|
+
width: captureCompositionWidth ?? width,
|
|
14187
|
+
height: captureCompositionHeight ?? height,
|
|
14188
|
+
codec: preset.codec,
|
|
14189
|
+
preset: preset.preset,
|
|
14190
|
+
quality: effectiveQuality,
|
|
14191
|
+
bitrate: effectiveBitrate,
|
|
14192
|
+
pixelFormat: preset.pixelFormat,
|
|
14193
|
+
vp9CpuUsed: cfg.vp9CpuUsed,
|
|
14194
|
+
useGpu: job.config.useGpu,
|
|
14195
|
+
imageFormat: segmentImageFormat,
|
|
14196
|
+
hdr: preset.hdr,
|
|
14197
|
+
// Each segment sets its own GOP (at most the interval); hls never reaches this route.
|
|
14198
|
+
...deliveryEncoder
|
|
14199
|
+
},
|
|
14200
|
+
buildCaptureOptions,
|
|
14201
|
+
createRenderVideoFrameInjector,
|
|
14202
|
+
abortSignal: executionSignal,
|
|
14203
|
+
assertNotAborted,
|
|
14204
|
+
onProgress,
|
|
14205
|
+
dedupPerfs,
|
|
14206
|
+
segmentFrames,
|
|
14207
|
+
segmentDir,
|
|
14208
|
+
workerCount: segmentedPlan.workerCount,
|
|
14209
|
+
browserRecycleEverySegments: resolveSegmentBrowserRecycle(process.env),
|
|
14210
|
+
completedSegments,
|
|
14211
|
+
onSegmentComplete: (entry) => {
|
|
14212
|
+
segmentManifest.completed = [
|
|
14213
|
+
...segmentManifest.completed.filter((e) => e.index !== entry.index),
|
|
14214
|
+
{ ...entry, completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
14215
|
+
];
|
|
14216
|
+
writeSegmentManifest(segmentDir, segmentManifest);
|
|
14217
|
+
},
|
|
14218
|
+
updateCaptureObservability
|
|
14219
|
+
})
|
|
14220
|
+
);
|
|
13752
14221
|
};
|
|
13753
|
-
|
|
13754
|
-
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
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
|
-
updateCaptureObservability
|
|
13804
|
-
})
|
|
13805
|
-
);
|
|
14222
|
+
let segmentedRes;
|
|
14223
|
+
try {
|
|
14224
|
+
segmentedRes = await invokeSegmented();
|
|
14225
|
+
} catch (err) {
|
|
14226
|
+
const isVerifyError = isDrawElementVerificationError(err);
|
|
14227
|
+
const isDeStall = isDeRendererStallError(err);
|
|
14228
|
+
if (!isVerifyError && !isDrawElementCaptureError(err) && !isDeStall || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
14229
|
+
throw err;
|
|
14230
|
+
}
|
|
14231
|
+
deSelfVerifyFallback = isVerifyError;
|
|
14232
|
+
const t = deVerifyFallbackTelemetry(err);
|
|
14233
|
+
deFallbackReason = isVerifyError ? t.reason : isDeStall ? "de_renderer_stall" : "capture_error";
|
|
14234
|
+
deFallbackFailedDb = t.failedDb;
|
|
14235
|
+
deFallbackFrameIndex = t.frameIndex;
|
|
14236
|
+
deFallbackThresholdDb = t.thresholdDb;
|
|
14237
|
+
log.warn(
|
|
14238
|
+
"[Render] drawElement capture failed on the segmented path; re-rendering via screenshot",
|
|
14239
|
+
{
|
|
14240
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14241
|
+
}
|
|
14242
|
+
);
|
|
14243
|
+
observability.checkpoint(
|
|
14244
|
+
"capture_segmented",
|
|
14245
|
+
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14246
|
+
);
|
|
14247
|
+
if (probeSession) {
|
|
14248
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
14249
|
+
const orphaned = probeSession;
|
|
14250
|
+
probeSession = null;
|
|
14251
|
+
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14252
|
+
}
|
|
14253
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
14254
|
+
completedSegments = /* @__PURE__ */ new Set();
|
|
14255
|
+
segmentManifest = emptySegmentManifest();
|
|
14256
|
+
dedupPerfs.length = 0;
|
|
14257
|
+
capturePlan = replanAfterFailure(capturePlan, {
|
|
14258
|
+
kind: isVerifyError ? "draw_element_verification" : "draw_element_capture"
|
|
14259
|
+
});
|
|
14260
|
+
syncCapturePlan();
|
|
14261
|
+
updateCaptureObservability({
|
|
14262
|
+
forceScreenshot: capturePlan.forceScreenshot,
|
|
14263
|
+
deSelfVerifyFallback,
|
|
14264
|
+
deFallbackReason,
|
|
14265
|
+
deFallbackFailedDb,
|
|
14266
|
+
deFallbackFrameIndex,
|
|
14267
|
+
deFallbackThresholdDb
|
|
14268
|
+
});
|
|
14269
|
+
segmentedRes = await invokeSegmented();
|
|
14270
|
+
observability.clearFailure("capture_segmented");
|
|
14271
|
+
}
|
|
13806
14272
|
if (segmentedRes.success) {
|
|
13807
14273
|
streamingHandled = true;
|
|
13808
14274
|
workerCount = segmentedRes.workerCount;
|
|
@@ -13822,7 +14288,7 @@ async function executeRenderPipeline(input) {
|
|
|
13822
14288
|
);
|
|
13823
14289
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
13824
14290
|
if (job.config.keepSegments !== true) {
|
|
13825
|
-
|
|
14291
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13826
14292
|
}
|
|
13827
14293
|
} else {
|
|
13828
14294
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -13875,7 +14341,7 @@ async function executeRenderPipeline(input) {
|
|
|
13875
14341
|
// assemble stage's `-c copy` segmentation cuts exactly on
|
|
13876
14342
|
// the segment boundary. Every other format leaves the
|
|
13877
14343
|
// encoder's open-GOP output untouched.
|
|
13878
|
-
...
|
|
14344
|
+
...deliveryEncoder
|
|
13879
14345
|
},
|
|
13880
14346
|
buildCaptureOptions,
|
|
13881
14347
|
createRenderVideoFrameInjector,
|
|
@@ -14038,7 +14504,7 @@ async function executeRenderPipeline(input) {
|
|
|
14038
14504
|
engineConfig: cfg,
|
|
14039
14505
|
// Same value the streaming encoder above spreads — the disk and
|
|
14040
14506
|
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
14041
|
-
...
|
|
14507
|
+
...deliveryEncoder,
|
|
14042
14508
|
abortSignal: executionSignal,
|
|
14043
14509
|
assertNotAborted,
|
|
14044
14510
|
onProgress
|
|
@@ -14094,8 +14560,8 @@ async function executeRenderPipeline(input) {
|
|
|
14094
14560
|
"capture_disk",
|
|
14095
14561
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
14096
14562
|
);
|
|
14097
|
-
|
|
14098
|
-
|
|
14563
|
+
rmSync10(framesDir, { recursive: true, force: true });
|
|
14564
|
+
mkdirSync10(framesDir, { recursive: true });
|
|
14099
14565
|
resetCaptureAttemptProgress(job);
|
|
14100
14566
|
dedupPerfs.length = 0;
|
|
14101
14567
|
cfg.useDrawElement = false;
|
|
@@ -14192,7 +14658,7 @@ async function executeRenderPipeline(input) {
|
|
|
14192
14658
|
buildArtifactExpectation({
|
|
14193
14659
|
outputFormat,
|
|
14194
14660
|
durationSeconds: job.duration,
|
|
14195
|
-
fps:
|
|
14661
|
+
fps: fpsToNumber8(job.config.fps),
|
|
14196
14662
|
expectedFrames: captureTotalFrames,
|
|
14197
14663
|
width: outputWidth,
|
|
14198
14664
|
height: outputHeight
|
|
@@ -14271,7 +14737,7 @@ async function executeRenderPipeline(input) {
|
|
|
14271
14737
|
job.perfSummary = perfSummary;
|
|
14272
14738
|
if (job.config.debug) {
|
|
14273
14739
|
try {
|
|
14274
|
-
|
|
14740
|
+
writeFileSync6(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
|
|
14275
14741
|
} catch (err) {
|
|
14276
14742
|
log.debug("Failed to write perf summary", {
|
|
14277
14743
|
perfOutputPath,
|
|
@@ -14281,8 +14747,8 @@ async function executeRenderPipeline(input) {
|
|
|
14281
14747
|
}
|
|
14282
14748
|
if (job.config.debug) {
|
|
14283
14749
|
if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
|
|
14284
|
-
const debugOutput =
|
|
14285
|
-
|
|
14750
|
+
const debugOutput = join27(workDir, `output${videoExt}`);
|
|
14751
|
+
copyFileSync5(stagedOutputPath, debugOutput);
|
|
14286
14752
|
}
|
|
14287
14753
|
}
|
|
14288
14754
|
await commitArtifactTransaction(artifactTransaction, () => {
|
|
@@ -14387,14 +14853,14 @@ async function executeRenderPipeline(input) {
|
|
|
14387
14853
|
// src/services/hyperframeLint.ts
|
|
14388
14854
|
import {
|
|
14389
14855
|
existsSync as existsSync17,
|
|
14390
|
-
readFileSync as
|
|
14391
|
-
statSync as
|
|
14392
|
-
openSync as
|
|
14856
|
+
readFileSync as readFileSync10,
|
|
14857
|
+
statSync as statSync9,
|
|
14858
|
+
openSync as openSync5,
|
|
14393
14859
|
fstatSync as fstatSync3,
|
|
14394
|
-
closeSync as
|
|
14860
|
+
closeSync as closeSync5,
|
|
14395
14861
|
constants as constants3
|
|
14396
14862
|
} from "node:fs";
|
|
14397
|
-
import { resolve as
|
|
14863
|
+
import { resolve as resolve9, join as join28, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
|
|
14398
14864
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
14399
14865
|
function isStringRecord(value) {
|
|
14400
14866
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -14424,30 +14890,30 @@ function pickEntryFile(files, preferredEntryFile) {
|
|
|
14424
14890
|
function readEntryHtml(filePath) {
|
|
14425
14891
|
let fd;
|
|
14426
14892
|
try {
|
|
14427
|
-
fd =
|
|
14893
|
+
fd = openSync5(filePath, constants3.O_RDONLY | constants3.O_NONBLOCK);
|
|
14428
14894
|
} catch (error) {
|
|
14429
14895
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
14430
14896
|
return null;
|
|
14431
|
-
if (!
|
|
14897
|
+
if (!statSync9(filePath, { throwIfNoEntry: false })?.isFile()) return null;
|
|
14432
14898
|
throw error;
|
|
14433
14899
|
}
|
|
14434
14900
|
try {
|
|
14435
14901
|
if (!fstatSync3(fd).isFile()) return null;
|
|
14436
|
-
return
|
|
14902
|
+
return readFileSync10(fd, "utf-8");
|
|
14437
14903
|
} finally {
|
|
14438
|
-
|
|
14904
|
+
closeSync5(fd);
|
|
14439
14905
|
}
|
|
14440
14906
|
}
|
|
14441
14907
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
14442
|
-
const absProjectDir =
|
|
14443
|
-
if (!existsSync17(absProjectDir) || !
|
|
14908
|
+
const absProjectDir = resolve9(projectDir);
|
|
14909
|
+
if (!existsSync17(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
|
|
14444
14910
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
14445
14911
|
}
|
|
14446
14912
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
14447
14913
|
(value) => typeof value === "string" && value.trim().length > 0
|
|
14448
14914
|
);
|
|
14449
14915
|
for (const entryFile of entryCandidates) {
|
|
14450
|
-
const absoluteEntryPath =
|
|
14916
|
+
const absoluteEntryPath = resolve9(absProjectDir, entryFile);
|
|
14451
14917
|
const entryRelativePath = relative3(absProjectDir, absoluteEntryPath);
|
|
14452
14918
|
if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep2}`) || isAbsolute3(entryRelativePath)) {
|
|
14453
14919
|
return { error: `Entry file must stay inside project directory: ${entryFile}` };
|
|
@@ -14462,7 +14928,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
14462
14928
|
}
|
|
14463
14929
|
}
|
|
14464
14930
|
return {
|
|
14465
|
-
error: `No HTML entry file found in project directory: ${
|
|
14931
|
+
error: `No HTML entry file found in project directory: ${join28(absProjectDir, preferredEntryFile || "index.html")}`
|
|
14466
14932
|
};
|
|
14467
14933
|
}
|
|
14468
14934
|
function prepareHyperframeLintBody(body) {
|
|
@@ -14508,7 +14974,7 @@ async function runHyperframeLint(prepared) {
|
|
|
14508
14974
|
// src/services/healthWorker.ts
|
|
14509
14975
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
14510
14976
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
14511
|
-
import { dirname as
|
|
14977
|
+
import { dirname as dirname12, join as join29 } from "node:path";
|
|
14512
14978
|
import { existsSync as existsSync18 } from "node:fs";
|
|
14513
14979
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
14514
14980
|
async function startHealthWorker(options = {}) {
|
|
@@ -14527,12 +14993,12 @@ async function startHealthWorker(options = {}) {
|
|
|
14527
14993
|
stdout: false,
|
|
14528
14994
|
stderr: false
|
|
14529
14995
|
});
|
|
14530
|
-
await new Promise((
|
|
14996
|
+
await new Promise((resolve11, reject) => {
|
|
14531
14997
|
const onMessage = (msg) => {
|
|
14532
14998
|
if (msg?.type === "listening") {
|
|
14533
14999
|
worker.off("error", onError);
|
|
14534
15000
|
worker.off("message", onMessage);
|
|
14535
|
-
|
|
15001
|
+
resolve11();
|
|
14536
15002
|
} else if (msg?.type === "listen-error") {
|
|
14537
15003
|
worker.off("error", onError);
|
|
14538
15004
|
worker.off("message", onMessage);
|
|
@@ -14583,8 +15049,8 @@ function defaultLogger2() {
|
|
|
14583
15049
|
};
|
|
14584
15050
|
}
|
|
14585
15051
|
function resolveWorkerEntry2() {
|
|
14586
|
-
const here =
|
|
14587
|
-
const candidates = [
|
|
15052
|
+
const here = dirname12(fileURLToPath5(import.meta.url));
|
|
15053
|
+
const candidates = [join29(here, "healthWorkerThread.js"), join29(here, "healthWorkerThread.ts")];
|
|
14588
15054
|
for (const candidate of candidates) {
|
|
14589
15055
|
if (existsSync18(candidate)) return candidate;
|
|
14590
15056
|
}
|
|
@@ -14620,17 +15086,18 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
|
14620
15086
|
|
|
14621
15087
|
// src/services/distributed/plan.ts
|
|
14622
15088
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
14623
|
-
import { extname as extname4, isAbsolute as isAbsolute4, join as
|
|
15089
|
+
import { extname as extname4, isAbsolute as isAbsolute4, join as join30, relative as relative4, sep as sep3 } from "node:path";
|
|
14624
15090
|
import {
|
|
14625
|
-
fpsToNumber as
|
|
15091
|
+
fpsToNumber as fpsToNumber9,
|
|
14626
15092
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
14627
15093
|
} from "@hyperframes/core";
|
|
14628
15094
|
import {
|
|
14629
15095
|
getEncoderPreset as getEncoderPreset4,
|
|
15096
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
14630
15097
|
normalizeVp9CpuUsed,
|
|
14631
15098
|
resolveConfig as resolveConfig3,
|
|
14632
15099
|
declareContentAddressedUrl,
|
|
14633
|
-
renameWithRetry as
|
|
15100
|
+
renameWithRetry as renameWithRetry7,
|
|
14634
15101
|
declareProbeFactsUrl
|
|
14635
15102
|
} from "@hyperframes/engine";
|
|
14636
15103
|
|
|
@@ -14701,10 +15168,10 @@ var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
|
|
|
14701
15168
|
var FIELD_DELIMITER = Buffer.from([0]);
|
|
14702
15169
|
|
|
14703
15170
|
// src/services/distributed/chunkAudio.ts
|
|
14704
|
-
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as
|
|
15171
|
+
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
|
|
14705
15172
|
|
|
14706
15173
|
// src/services/distributed/mp4Stitch.ts
|
|
14707
|
-
import { renameWithRetry as
|
|
15174
|
+
import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
|
|
14708
15175
|
var SCAN_BYTES = 64 * 1024;
|
|
14709
15176
|
|
|
14710
15177
|
// src/services/distributed/chunkAudio.ts
|
|
@@ -14724,9 +15191,9 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
14724
15191
|
var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
14725
15192
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
14726
15193
|
"plan.json",
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
15194
|
+
join30("meta", "composition.json"),
|
|
15195
|
+
join30("meta", "encoder.json"),
|
|
15196
|
+
join30("meta", "chunks.json")
|
|
14730
15197
|
];
|
|
14731
15198
|
|
|
14732
15199
|
// src/services/distributed/renderConfigValidation.ts
|
|
@@ -14822,6 +15289,12 @@ function assertOptionalInteger(options, field, min = 0) {
|
|
|
14822
15289
|
throw new Error(`Render request ${field} must be an integer >= ${min}`);
|
|
14823
15290
|
}
|
|
14824
15291
|
}
|
|
15292
|
+
function assertOptionalPositiveNumber(options, field) {
|
|
15293
|
+
const value = options[field];
|
|
15294
|
+
if (value !== void 0 && !(typeof value === "number" && value > 0)) {
|
|
15295
|
+
throw new Error(`Render request ${field} must be a positive number`);
|
|
15296
|
+
}
|
|
15297
|
+
}
|
|
14825
15298
|
function assertOptionalString(options, field) {
|
|
14826
15299
|
if (options[field] !== void 0 && typeof options[field] !== "string") {
|
|
14827
15300
|
throw new Error(`Render request ${field} must be a string`);
|
|
@@ -14869,10 +15342,11 @@ function assertRequestOptionScalars(options) {
|
|
|
14869
15342
|
assertOptionalInteger(options, "workers", 1);
|
|
14870
15343
|
assertOptionalInteger(options, "maxRenderMs");
|
|
14871
15344
|
assertOptionalInteger(options, "crf");
|
|
15345
|
+
assertOptionalPositiveNumber(options, "keyframeIntervalSeconds");
|
|
14872
15346
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
14873
15347
|
assertOptionalBoolean(options, field);
|
|
14874
15348
|
}
|
|
14875
|
-
for (const field of ["entryFile", "videoBitrate"]) {
|
|
15349
|
+
for (const field of ["entryFile", "videoBitrate", "maxBitrate", "bufferSize"]) {
|
|
14876
15350
|
assertOptionalString(options, field);
|
|
14877
15351
|
}
|
|
14878
15352
|
assertOptionalEnum(options, "strictness", ["strict", "best-effort"]);
|
|
@@ -14957,6 +15431,8 @@ var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
|
|
|
14957
15431
|
var SSE_KEEPALIVE_MS = 3e4;
|
|
14958
15432
|
var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
14959
15433
|
"ASSET_MEDIA_TYPE_MISMATCH",
|
|
15434
|
+
"HEIF_IMAGE_CONVERSION_FAILED",
|
|
15435
|
+
"HEIF_TOOL_UNAVAILABLE",
|
|
14960
15436
|
"NOT_MEDIA_PAYLOAD",
|
|
14961
15437
|
"INVALID_VIDEO_METADATA",
|
|
14962
15438
|
"VIDEO_SOURCE_UNRENDERABLE",
|
|
@@ -15031,7 +15507,7 @@ function parseRenderOptions(body) {
|
|
|
15031
15507
|
const format = parseServerFormat(body.format);
|
|
15032
15508
|
const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
|
|
15033
15509
|
const encoder = parseEncoderOptions(body, format ?? "mp4");
|
|
15034
|
-
const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
|
|
15510
|
+
const { crf, videoBitrate, videoFrameFormat, keyframeIntervalSeconds, maxBitrate, bufferSize } = encoder.ok ? encoder.options : {};
|
|
15035
15511
|
const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
|
|
15036
15512
|
return {
|
|
15037
15513
|
outputPath,
|
|
@@ -15041,6 +15517,9 @@ function parseRenderOptions(body) {
|
|
|
15041
15517
|
maxRenderMs,
|
|
15042
15518
|
crf,
|
|
15043
15519
|
videoBitrate,
|
|
15520
|
+
keyframeIntervalSeconds,
|
|
15521
|
+
maxBitrate,
|
|
15522
|
+
bufferSize,
|
|
15044
15523
|
useGpu,
|
|
15045
15524
|
debug,
|
|
15046
15525
|
strictness,
|
|
@@ -15075,6 +15554,9 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
15075
15554
|
maxRenderMs: input.maxRenderMs,
|
|
15076
15555
|
crf: input.crf,
|
|
15077
15556
|
videoBitrate: input.videoBitrate,
|
|
15557
|
+
keyframeIntervalSeconds: input.keyframeIntervalSeconds,
|
|
15558
|
+
maxBitrate: input.maxBitrate,
|
|
15559
|
+
bufferSize: input.bufferSize,
|
|
15078
15560
|
useGpu: input.useGpu,
|
|
15079
15561
|
debug: input.debug,
|
|
15080
15562
|
strictness: input.strictness,
|
|
@@ -15092,8 +15574,8 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
15092
15574
|
function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
15093
15575
|
const { input, cleanupProjectDir } = prepared;
|
|
15094
15576
|
const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
|
|
15095
|
-
const outputDir =
|
|
15096
|
-
if (!existsSync19(outputDir))
|
|
15577
|
+
const outputDir = dirname13(absoluteOutputPath);
|
|
15578
|
+
if (!existsSync19(outputDir)) mkdirSync11(outputDir, { recursive: true });
|
|
15097
15579
|
return { input, cleanupProjectDir, absoluteOutputPath };
|
|
15098
15580
|
}
|
|
15099
15581
|
function validateRenderOverrides(body) {
|
|
@@ -15133,12 +15615,12 @@ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
|
|
|
15133
15615
|
function prepareProjectDirectory(projectDir, options) {
|
|
15134
15616
|
const candidate = nonEmptyString(projectDir);
|
|
15135
15617
|
if (!candidate) return null;
|
|
15136
|
-
const absProjectDir =
|
|
15137
|
-
if (!existsSync19(absProjectDir) || !
|
|
15618
|
+
const absProjectDir = resolve10(candidate);
|
|
15619
|
+
if (!existsSync19(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
|
|
15138
15620
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
15139
15621
|
}
|
|
15140
15622
|
const entry = options.entryFile || "index.html";
|
|
15141
|
-
if (!existsSync19(
|
|
15623
|
+
if (!existsSync19(resolve10(absProjectDir, entry))) {
|
|
15142
15624
|
return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
|
|
15143
15625
|
}
|
|
15144
15626
|
return { prepared: { input: { projectDir: absProjectDir, ...options } } };
|
|
@@ -15159,8 +15641,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
15159
15641
|
}
|
|
15160
15642
|
function materializeInlineProject(html, options) {
|
|
15161
15643
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
15162
|
-
const tempProjectDir = mkdtempSync4(
|
|
15163
|
-
|
|
15644
|
+
const tempProjectDir = mkdtempSync4(join31(tempRoot, "producer-project-"));
|
|
15645
|
+
writeFileSync7(join31(tempProjectDir, "index.html"), html, "utf-8");
|
|
15164
15646
|
return {
|
|
15165
15647
|
prepared: {
|
|
15166
15648
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -15185,7 +15667,7 @@ function resolveOutputPath(projectDir, outputCandidate, rendersDir, log) {
|
|
|
15185
15667
|
try {
|
|
15186
15668
|
return resolveRenderPaths(projectDir, outputCandidate, rendersDir).absoluteOutputPath;
|
|
15187
15669
|
} catch (error) {
|
|
15188
|
-
const fallbackPath =
|
|
15670
|
+
const fallbackPath = resolve10(rendersDir, `producer-fallback-${Date.now()}.mp4`);
|
|
15189
15671
|
log.warn("Failed to resolve output path, using fallback", {
|
|
15190
15672
|
fallback: fallbackPath,
|
|
15191
15673
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -15221,7 +15703,7 @@ function createArtifactStore(ttlMs) {
|
|
|
15221
15703
|
function cleanupTempDir(dir, log) {
|
|
15222
15704
|
if (!dir) return;
|
|
15223
15705
|
try {
|
|
15224
|
-
|
|
15706
|
+
rmSync11(dir, { recursive: true, force: true });
|
|
15225
15707
|
} catch (error) {
|
|
15226
15708
|
log.warn("Failed to cleanup temp project dir", {
|
|
15227
15709
|
cleanupProjectDir: dir,
|
|
@@ -15230,7 +15712,7 @@ function cleanupTempDir(dir, log) {
|
|
|
15230
15712
|
}
|
|
15231
15713
|
}
|
|
15232
15714
|
function outputFileSize(path) {
|
|
15233
|
-
return existsSync19(path) ?
|
|
15715
|
+
return existsSync19(path) ? statSync10(path).size : 0;
|
|
15234
15716
|
}
|
|
15235
15717
|
function createBlockingProgressReporter(log, requestId) {
|
|
15236
15718
|
let lastLoggedPct = -10;
|
|
@@ -15536,7 +16018,7 @@ function createRenderHandlers(options = {}) {
|
|
|
15536
16018
|
store.delete(token);
|
|
15537
16019
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
15538
16020
|
}
|
|
15539
|
-
const stats =
|
|
16021
|
+
const stats = statSync10(artifact.path);
|
|
15540
16022
|
return new Response(createReadStream3(artifact.path), {
|
|
15541
16023
|
headers: {
|
|
15542
16024
|
"content-type": "video/mp4",
|
|
@@ -15603,7 +16085,7 @@ function startServer(options = {}) {
|
|
|
15603
16085
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
15604
16086
|
return server;
|
|
15605
16087
|
}
|
|
15606
|
-
var entryScript = process.argv[1] ?
|
|
16088
|
+
var entryScript = process.argv[1] ? resolve10(process.argv[1]) : "";
|
|
15607
16089
|
var isPublicServerEntry = entryScript.endsWith("/public-server.js") || entryScript.endsWith("/src/server.ts");
|
|
15608
16090
|
if (isPublicServerEntry) {
|
|
15609
16091
|
const { values } = parseArgs({
|