@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/distributed.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
// src/services/distributed/plan.ts
|
|
2
2
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
3
3
|
import {
|
|
4
|
-
copyFileSync as
|
|
4
|
+
copyFileSync as copyFileSync6,
|
|
5
5
|
cpSync as cpSync2,
|
|
6
6
|
existsSync as existsSync15,
|
|
7
7
|
linkSync,
|
|
8
|
-
mkdirSync as
|
|
9
|
-
rmSync as
|
|
10
|
-
writeFileSync as
|
|
8
|
+
mkdirSync as mkdirSync10,
|
|
9
|
+
rmSync as rmSync7,
|
|
10
|
+
writeFileSync as writeFileSync7
|
|
11
11
|
} from "node:fs";
|
|
12
|
-
import { extname as extname4, isAbsolute as isAbsolute3, join as
|
|
12
|
+
import { extname as extname4, isAbsolute as isAbsolute3, join as join22, relative as relative5, sep as sep2 } from "node:path";
|
|
13
13
|
import {
|
|
14
|
-
fpsToNumber as
|
|
14
|
+
fpsToNumber as fpsToNumber9,
|
|
15
15
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
16
16
|
} from "@hyperframes/core";
|
|
17
17
|
import {
|
|
18
18
|
getEncoderPreset as getEncoderPreset4,
|
|
19
|
+
keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
|
|
19
20
|
normalizeVp9CpuUsed,
|
|
20
21
|
resolveConfig as resolveConfig3,
|
|
21
22
|
declareContentAddressedUrl,
|
|
22
|
-
renameWithRetry as
|
|
23
|
+
renameWithRetry as renameWithRetry7,
|
|
23
24
|
declareProbeFactsUrl
|
|
24
25
|
} from "@hyperframes/engine";
|
|
25
26
|
|
|
@@ -66,18 +67,18 @@ var defaultLogger = createConsoleLogger("info");
|
|
|
66
67
|
import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
|
|
67
68
|
import {
|
|
68
69
|
existsSync as existsSync10,
|
|
69
|
-
mkdirSync as
|
|
70
|
-
readFileSync as
|
|
70
|
+
mkdirSync as mkdirSync6,
|
|
71
|
+
readFileSync as readFileSync6,
|
|
71
72
|
readdirSync as readdirSync3,
|
|
72
|
-
rmSync as
|
|
73
|
-
statSync as
|
|
74
|
-
writeFileSync as
|
|
75
|
-
copyFileSync as
|
|
73
|
+
rmSync as rmSync5,
|
|
74
|
+
statSync as statSync5,
|
|
75
|
+
writeFileSync as writeFileSync4,
|
|
76
|
+
copyFileSync as copyFileSync5,
|
|
76
77
|
appendFileSync
|
|
77
78
|
} from "fs";
|
|
78
79
|
import {
|
|
79
80
|
chainCapture,
|
|
80
|
-
fpsToNumber as
|
|
81
|
+
fpsToNumber as fpsToNumber8,
|
|
81
82
|
HF_COLOR_GRADING_ATTR,
|
|
82
83
|
HF_VFX_ATTR,
|
|
83
84
|
parseVfxChain,
|
|
@@ -119,7 +120,7 @@ import {
|
|
|
119
120
|
augmentProtocolTimeoutError,
|
|
120
121
|
augmentPageNavigationTimeoutError
|
|
121
122
|
} from "@hyperframes/engine";
|
|
122
|
-
import { join as
|
|
123
|
+
import { join as join14, dirname as dirname8, resolve as resolve7 } from "path";
|
|
123
124
|
import { randomUUID } from "crypto";
|
|
124
125
|
|
|
125
126
|
// src/services/fileServer.ts
|
|
@@ -141,6 +142,7 @@ import { Readable } from "node:stream";
|
|
|
141
142
|
import { join, extname, resolve as resolve2, sep } from "node:path";
|
|
142
143
|
import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
|
|
143
144
|
import { fpsToNumber } from "@hyperframes/core";
|
|
145
|
+
import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
|
|
144
146
|
|
|
145
147
|
// src/services/hyperframeRuntimeLoader.ts
|
|
146
148
|
import { createHash } from "node:crypto";
|
|
@@ -218,11 +220,11 @@ function getHfEarlyStub() {
|
|
|
218
220
|
|
|
219
221
|
// src/services/fileServer.ts
|
|
220
222
|
var readFileAsync = promisify(readFile);
|
|
221
|
-
function isPathInside(
|
|
223
|
+
function isPathInside(child3, parent, options = {}) {
|
|
222
224
|
const { resolveSymlinks = false, pathModule } = options;
|
|
223
225
|
const resolveFn = pathModule?.resolve ?? resolve2;
|
|
224
226
|
const separator = pathModule?.sep ?? sep;
|
|
225
|
-
const resolvedChild = resolveFn(
|
|
227
|
+
const resolvedChild = resolveFn(child3);
|
|
226
228
|
const resolvedParent = resolveFn(parent);
|
|
227
229
|
const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
|
|
228
230
|
const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
|
|
@@ -738,6 +740,18 @@ function createFileServer(options) {
|
|
|
738
740
|
html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
|
|
739
741
|
return c.text(html, 200, { "Content-Type": contentType });
|
|
740
742
|
}
|
|
743
|
+
if (ext === ".png") {
|
|
744
|
+
const untagged = stripSdrPngColourTags(await readFileAsync(fd));
|
|
745
|
+
if (untagged) {
|
|
746
|
+
const headers = {
|
|
747
|
+
"Content-Type": contentType,
|
|
748
|
+
"Content-Length": String(untagged.length)
|
|
749
|
+
};
|
|
750
|
+
return new Response(c.req.method === "HEAD" ? null : new Uint8Array(untagged), {
|
|
751
|
+
headers
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
741
755
|
const totalSize = stat.size;
|
|
742
756
|
const rangeHeader = c.req.header("range");
|
|
743
757
|
const rangeRequest = parseRangeHeader(rangeHeader, totalSize);
|
|
@@ -765,7 +779,8 @@ function createFileServer(options) {
|
|
|
765
779
|
const stream = createReadStream(filePath, {
|
|
766
780
|
fd,
|
|
767
781
|
autoClose: true,
|
|
768
|
-
|
|
782
|
+
start: range?.start ?? 0,
|
|
783
|
+
...range ? { end: range.end } : {}
|
|
769
784
|
});
|
|
770
785
|
streamOwnsFd = true;
|
|
771
786
|
try {
|
|
@@ -779,10 +794,10 @@ function createFileServer(options) {
|
|
|
779
794
|
if (!streamOwnsFd) closeSync(fd);
|
|
780
795
|
}
|
|
781
796
|
});
|
|
782
|
-
return new Promise((
|
|
797
|
+
return new Promise((resolve10) => {
|
|
783
798
|
const connections = /* @__PURE__ */ new Set();
|
|
784
799
|
const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
|
|
785
|
-
|
|
800
|
+
resolve10({
|
|
786
801
|
url: `http://localhost:${info.port}`,
|
|
787
802
|
port: info.port,
|
|
788
803
|
addPreHeadScript: (script) => {
|
|
@@ -808,7 +823,34 @@ function outputNeedsAlpha(format) {
|
|
|
808
823
|
}
|
|
809
824
|
|
|
810
825
|
// src/services/render/hlsConfig.ts
|
|
811
|
-
import {
|
|
826
|
+
import { keyframeIntervalToGopSize } from "@hyperframes/engine";
|
|
827
|
+
|
|
828
|
+
// src/services/render/deliveryEncoder.ts
|
|
829
|
+
import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
|
|
830
|
+
function validateDeliveryEncoderConfig(config) {
|
|
831
|
+
const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
|
|
832
|
+
if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
|
|
833
|
+
return;
|
|
834
|
+
if (keyframeIntervalSeconds !== void 0) {
|
|
835
|
+
if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
|
|
836
|
+
throw new Error(
|
|
837
|
+
`keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
if (format === "hls") {
|
|
841
|
+
throw new Error(
|
|
842
|
+
"keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (format !== void 0 && format !== "mp4") {
|
|
847
|
+
throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
|
|
848
|
+
}
|
|
849
|
+
appendRateCapArgs([], { maxBitrate, bufferSize });
|
|
850
|
+
}
|
|
851
|
+
function chunkGopFrames(framesInChunk, intervalFrames) {
|
|
852
|
+
return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
|
|
853
|
+
}
|
|
812
854
|
|
|
813
855
|
// src/services/render/shared.ts
|
|
814
856
|
import {
|
|
@@ -1231,7 +1273,7 @@ import {
|
|
|
1231
1273
|
import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
1232
1274
|
|
|
1233
1275
|
// src/services/render/perfSummary.ts
|
|
1234
|
-
import { fpsToNumber as
|
|
1276
|
+
import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
|
|
1235
1277
|
function pushWorkerDedupPerfs(results, sink) {
|
|
1236
1278
|
for (const r of results) {
|
|
1237
1279
|
if (r.perf) sink.push(r.perf);
|
|
@@ -1310,7 +1352,7 @@ import {
|
|
|
1310
1352
|
resolveVideoExtractionWindow,
|
|
1311
1353
|
runFfmpeg
|
|
1312
1354
|
} from "@hyperframes/engine";
|
|
1313
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
1355
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
1314
1356
|
var BYTES_PER_MIB = 1024 * 1024;
|
|
1315
1357
|
var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
|
|
1316
1358
|
|
|
@@ -1361,7 +1403,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
1361
1403
|
}
|
|
1362
1404
|
|
|
1363
1405
|
// src/services/render/captureCost.ts
|
|
1364
|
-
import { fpsToNumber as
|
|
1406
|
+
import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
1365
1407
|
import {
|
|
1366
1408
|
calculateOptimalWorkers,
|
|
1367
1409
|
captureFrameToBuffer as captureFrameToBuffer3,
|
|
@@ -1771,7 +1813,7 @@ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
|
|
|
1771
1813
|
// src/services/render/videoFrameCoverage.ts
|
|
1772
1814
|
import { parseHTML } from "linkedom";
|
|
1773
1815
|
import {
|
|
1774
|
-
fpsToNumber as
|
|
1816
|
+
fpsToNumber as fpsToNumber5,
|
|
1775
1817
|
normalizeRateSpec,
|
|
1776
1818
|
sourceTimeAt,
|
|
1777
1819
|
toFps
|
|
@@ -1782,15 +1824,15 @@ import {
|
|
|
1782
1824
|
} from "@hyperframes/engine";
|
|
1783
1825
|
|
|
1784
1826
|
// src/services/render/stages/compileStage.ts
|
|
1785
|
-
import { join as
|
|
1827
|
+
import { join as join7 } from "node:path";
|
|
1786
1828
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
1787
1829
|
|
|
1788
1830
|
// src/services/htmlCompiler.ts
|
|
1789
1831
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
1790
|
-
import { createHash as
|
|
1791
|
-
import { createReadStream as createReadStream2, existsSync as existsSync6, mkdirSync as
|
|
1792
|
-
import { join as
|
|
1793
|
-
import { parseHTML as
|
|
1832
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
1833
|
+
import { createReadStream as createReadStream2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
|
|
1834
|
+
import { join as join6, dirname as dirname5, resolve as resolve6, basename as basename3, relative as relative2 } from "path";
|
|
1835
|
+
import { parseHTML as parseHTML5 } from "linkedom";
|
|
1794
1836
|
import {
|
|
1795
1837
|
COMPOSITION_SOURCE_URL,
|
|
1796
1838
|
compileTimingAttrs,
|
|
@@ -2066,7 +2108,7 @@ function readLoopOverride(el) {
|
|
|
2066
2108
|
}
|
|
2067
2109
|
return null;
|
|
2068
2110
|
}
|
|
2069
|
-
function
|
|
2111
|
+
function resolveLocalImageSourcePath(src, options) {
|
|
2070
2112
|
const trimmed = src.trim();
|
|
2071
2113
|
if (!trimmed || trimmed.startsWith("data:")) return null;
|
|
2072
2114
|
const { basePath } = splitUrlSuffix(trimmed);
|
|
@@ -2289,7 +2331,7 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
2289
2331
|
const img = images[i];
|
|
2290
2332
|
const src = (img.getAttribute("src") || "").trim();
|
|
2291
2333
|
if (!hasGifExtension(src)) continue;
|
|
2292
|
-
const sourcePath =
|
|
2334
|
+
const sourcePath = resolveLocalImageSourcePath(src, options);
|
|
2293
2335
|
if (!sourcePath) continue;
|
|
2294
2336
|
const bytes = readFileSync2(sourcePath);
|
|
2295
2337
|
const metadata = parseAnimatedGifMetadata(bytes);
|
|
@@ -2337,19 +2379,74 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
2337
2379
|
};
|
|
2338
2380
|
}
|
|
2339
2381
|
|
|
2340
|
-
// src/services/
|
|
2341
|
-
import {
|
|
2342
|
-
import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
|
|
2343
|
-
|
|
2344
|
-
// src/services/assetMediaType.ts
|
|
2345
|
-
import { existsSync as existsSync5 } from "node:fs";
|
|
2382
|
+
// src/services/heifImagePrep.ts
|
|
2383
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2346
2384
|
import {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2385
|
+
closeSync as closeSync2,
|
|
2386
|
+
copyFileSync as copyFileSync3,
|
|
2387
|
+
mkdirSync as mkdirSync3,
|
|
2388
|
+
openSync as openSync2,
|
|
2389
|
+
readFileSync as readFileSync3,
|
|
2390
|
+
readSync,
|
|
2391
|
+
rmSync as rmSync3,
|
|
2392
|
+
statSync as statSync3,
|
|
2393
|
+
writeFileSync as writeFileSync2
|
|
2394
|
+
} from "node:fs";
|
|
2395
|
+
import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
|
|
2396
|
+
import { parseHTML as parseHTML4 } from "linkedom";
|
|
2397
|
+
import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
|
|
2398
|
+
|
|
2399
|
+
// src/services/heifRotation.ts
|
|
2400
|
+
function* boxes(buf, start, end) {
|
|
2401
|
+
let at = start;
|
|
2402
|
+
while (at + 8 <= end) {
|
|
2403
|
+
let size = buf.readUInt32BE(at);
|
|
2404
|
+
let header = 8;
|
|
2405
|
+
if (size === 1 && at + 16 <= end) {
|
|
2406
|
+
size = Number(buf.readBigUInt64BE(at + 8));
|
|
2407
|
+
header = 16;
|
|
2408
|
+
} else if (size === 0) {
|
|
2409
|
+
size = end - at;
|
|
2410
|
+
}
|
|
2411
|
+
if (size < header || at + size > end) return;
|
|
2412
|
+
yield { type: buf.toString("latin1", at + 4, at + 8), start: at + header, end: at + size };
|
|
2413
|
+
at += size;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
function child(buf, parent, type, skip = 0) {
|
|
2417
|
+
return [...boxes(buf, parent.start + skip, parent.end)].filter((box2) => box2.type === type);
|
|
2418
|
+
}
|
|
2419
|
+
function readHeifPrimaryRotation(buf) {
|
|
2420
|
+
try {
|
|
2421
|
+
const root = { type: "", start: 0, end: buf.length };
|
|
2422
|
+
const meta = child(buf, root, "meta")[0];
|
|
2423
|
+
const pitm = meta && child(buf, meta, "pitm", 4)[0];
|
|
2424
|
+
const iprp = meta && child(buf, meta, "iprp", 4)[0];
|
|
2425
|
+
const ipco = iprp && child(buf, iprp, "ipco")[0];
|
|
2426
|
+
if (!pitm || !ipco || !iprp) return 0;
|
|
2427
|
+
const primaryId = buf[pitm.start] === 0 ? buf.readUInt16BE(pitm.start + 4) : buf.readUInt32BE(pitm.start + 4);
|
|
2428
|
+
const props = [...boxes(buf, ipco.start, ipco.end)];
|
|
2429
|
+
for (const ipma of child(buf, iprp, "ipma")) {
|
|
2430
|
+
const version = buf[ipma.start];
|
|
2431
|
+
const wide = (buf[ipma.start + 3] & 1) === 1;
|
|
2432
|
+
let at = ipma.start + 8;
|
|
2433
|
+
for (let n = buf.readUInt32BE(ipma.start + 4); n > 0 && at < ipma.end; n--) {
|
|
2434
|
+
const id = version < 1 ? buf.readUInt16BE(at) : buf.readUInt32BE(at);
|
|
2435
|
+
at += version < 1 ? 2 : 4;
|
|
2436
|
+
const count = buf[at++];
|
|
2437
|
+
for (let i = 0; i < count; i++) {
|
|
2438
|
+
const index = wide ? buf.readUInt16BE(at) & 32767 : buf[at] & 127;
|
|
2439
|
+
at += wide ? 2 : 1;
|
|
2440
|
+
const prop = props[index - 1];
|
|
2441
|
+
if (id === primaryId && prop?.type === "irot")
|
|
2442
|
+
return (buf[prop.start] & 3) * 90;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
} catch {
|
|
2447
|
+
}
|
|
2448
|
+
return 0;
|
|
2449
|
+
}
|
|
2353
2450
|
|
|
2354
2451
|
// src/utils/semaphore.ts
|
|
2355
2452
|
var Semaphore = class {
|
|
@@ -2364,10 +2461,10 @@ var Semaphore = class {
|
|
|
2364
2461
|
this.active++;
|
|
2365
2462
|
return () => this.release();
|
|
2366
2463
|
}
|
|
2367
|
-
return new Promise((
|
|
2464
|
+
return new Promise((resolve10) => {
|
|
2368
2465
|
this.queue.push(() => {
|
|
2369
2466
|
this.active++;
|
|
2370
|
-
|
|
2467
|
+
resolve10(() => this.release());
|
|
2371
2468
|
});
|
|
2372
2469
|
});
|
|
2373
2470
|
}
|
|
@@ -2398,7 +2495,229 @@ async function withMediaProbeSlot(operation) {
|
|
|
2398
2495
|
}
|
|
2399
2496
|
}
|
|
2400
2497
|
|
|
2498
|
+
// src/services/heifImagePrep.ts
|
|
2499
|
+
var PREPARED_HEIF_SUBDIR = "_heif_images";
|
|
2500
|
+
var CACHE_SCHEMA2 = "hfheif-v1";
|
|
2501
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
2502
|
+
var HEIF_IMAGE_CONVERSION_FAILED = "HEIF_IMAGE_CONVERSION_FAILED";
|
|
2503
|
+
var HEIF_TOOL_UNAVAILABLE = "HEIF_TOOL_UNAVAILABLE";
|
|
2504
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
|
|
2505
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
2506
|
+
var DATA_URL_RE = /^data:image\/(?:heic|heif)(?:-sequence)?;base64,(.*)$/is;
|
|
2507
|
+
var HeifImageConversionError = class extends Error {
|
|
2508
|
+
code = HEIF_IMAGE_CONVERSION_FAILED;
|
|
2509
|
+
owner = "user";
|
|
2510
|
+
retryable = false;
|
|
2511
|
+
constructor(label, reason) {
|
|
2512
|
+
super(
|
|
2513
|
+
`Could not convert HEIC/HEIF image ${label} to JPEG: ${reason}. Re-export the photo as JPEG or PNG and update its src.`
|
|
2514
|
+
);
|
|
2515
|
+
this.name = "HeifImageConversionError";
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
var HeifToolUnavailableError = class extends Error {
|
|
2519
|
+
code = HEIF_TOOL_UNAVAILABLE;
|
|
2520
|
+
owner = "system";
|
|
2521
|
+
retryable = false;
|
|
2522
|
+
constructor(label, reason) {
|
|
2523
|
+
super(
|
|
2524
|
+
`Could not convert HEIC/HEIF image ${label}: ${reason}. Converting HEIC needs ffmpeg and ffprobe 7.0 or newer with the hevc decoder.`
|
|
2525
|
+
);
|
|
2526
|
+
this.name = "HeifToolUnavailableError";
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
var TOOL_FAILURE_RE = /(?:ffmpeg|ffprobe) not found|ENOENT|Option not found|Unrecognized option|Unknown decoder|Decoder \S+ not found/i;
|
|
2530
|
+
function isHeifBytes(head) {
|
|
2531
|
+
const bytes = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
|
|
2532
|
+
if (bytes.length < 16 || bytes.toString("ascii", 4, 8) !== "ftyp") return false;
|
|
2533
|
+
const boxEnd = Math.min(bytes.readUInt32BE(0), bytes.length);
|
|
2534
|
+
if (boxEnd < 16) return false;
|
|
2535
|
+
const brands = [bytes.toString("ascii", 8, 12)];
|
|
2536
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
|
|
2537
|
+
brands.push(bytes.toString("ascii", offset, offset + 4));
|
|
2538
|
+
}
|
|
2539
|
+
return !brands.some((b) => AVIF_BRANDS.has(b)) && brands.some((b) => HEIF_BRANDS.has(b));
|
|
2540
|
+
}
|
|
2541
|
+
function readHead(path) {
|
|
2542
|
+
let fd;
|
|
2543
|
+
try {
|
|
2544
|
+
fd = openSync2(path, "r");
|
|
2545
|
+
const buf = Buffer.alloc(4096);
|
|
2546
|
+
return buf.subarray(0, readSync(fd, buf, 0, buf.length, 0));
|
|
2547
|
+
} catch {
|
|
2548
|
+
return null;
|
|
2549
|
+
} finally {
|
|
2550
|
+
if (fd !== void 0) closeSync2(fd);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
async function probeJson(path, withGroups) {
|
|
2554
|
+
const stdout = await runFfprobe(
|
|
2555
|
+
path,
|
|
2556
|
+
withGroups ? ["-show_stream_groups", "-show_streams", "-of", "json"] : ["-show_streams", "-of", "json"]
|
|
2557
|
+
);
|
|
2558
|
+
return JSON.parse(stdout);
|
|
2559
|
+
}
|
|
2560
|
+
var ROTATION_FILTER = {
|
|
2561
|
+
0: "",
|
|
2562
|
+
90: "transpose=2",
|
|
2563
|
+
180: "hflip,vflip",
|
|
2564
|
+
270: "transpose=1"
|
|
2565
|
+
};
|
|
2566
|
+
function primaryGrid(probed) {
|
|
2567
|
+
const groups = probed.stream_groups ?? [];
|
|
2568
|
+
return (groups.find((g) => g.disposition?.default === 1) ?? groups[0])?.components?.[0];
|
|
2569
|
+
}
|
|
2570
|
+
async function convertHeifToJpeg(request) {
|
|
2571
|
+
const probed = await probeJson(request.inputPath, true);
|
|
2572
|
+
const grid = primaryGrid(probed);
|
|
2573
|
+
const rotation = readHeifPrimaryRotation(readFileSync3(request.inputPath));
|
|
2574
|
+
const filters = [];
|
|
2575
|
+
let inputs = [];
|
|
2576
|
+
let size = probed.streams?.[0];
|
|
2577
|
+
if (grid?.subcomponents?.length) {
|
|
2578
|
+
size = grid;
|
|
2579
|
+
const layout = grid.subcomponents.map(
|
|
2580
|
+
(t) => `${t.tile_horizontal_offset}_${t.tile_vertical_offset}`
|
|
2581
|
+
);
|
|
2582
|
+
inputs = grid.subcomponents.map((t) => `[0:${t.stream_index}]`);
|
|
2583
|
+
filters.push(
|
|
2584
|
+
`${inputs.join("")}xstack=inputs=${inputs.length}:layout=${layout.join("|")}`,
|
|
2585
|
+
`crop=${grid.width}:${grid.height}:${grid.horizontal_offset ?? 0}:${grid.vertical_offset ?? 0}`
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
if (!size?.width || !size.height) throw new Error("no declared image size");
|
|
2589
|
+
if (rotation && grid) filters.push(ROTATION_FILTER[rotation]);
|
|
2590
|
+
const mapArgs = filters.length > 0 ? ["-filter_complex", `${filters.join(",")}[v]`, "-map", "[v]"] : [];
|
|
2591
|
+
const result = await runFfmpeg3(
|
|
2592
|
+
[
|
|
2593
|
+
"-hide_banner",
|
|
2594
|
+
"-loglevel",
|
|
2595
|
+
"error",
|
|
2596
|
+
"-i",
|
|
2597
|
+
request.inputPath,
|
|
2598
|
+
...mapArgs,
|
|
2599
|
+
"-frames:v",
|
|
2600
|
+
"1",
|
|
2601
|
+
"-q:v",
|
|
2602
|
+
"2",
|
|
2603
|
+
"-f",
|
|
2604
|
+
"mjpeg",
|
|
2605
|
+
"-y",
|
|
2606
|
+
request.outputPath
|
|
2607
|
+
],
|
|
2608
|
+
{ timeout: request.timeoutMs ?? DEFAULT_TIMEOUT_MS }
|
|
2609
|
+
);
|
|
2610
|
+
if (!result.success) {
|
|
2611
|
+
throw result.error ?? new Error(`ffmpeg exited ${result.exitCode}: ${result.stderr.slice(-300)}`);
|
|
2612
|
+
}
|
|
2613
|
+
const out = (await probeJson(request.outputPath, false)).streams?.[0];
|
|
2614
|
+
const same = out?.width === size.width && out.height === size.height;
|
|
2615
|
+
const swapped = out?.width === size.height && out.height === size.width;
|
|
2616
|
+
if (!same && !swapped) {
|
|
2617
|
+
throw new Error(
|
|
2618
|
+
`wrong size: decoded ${out?.width}x${out?.height}, file declares ${size.width}x${size.height}`
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function resolveImageSource(src, options) {
|
|
2623
|
+
const data = DATA_URL_RE.exec(src);
|
|
2624
|
+
if (data) {
|
|
2625
|
+
const bytes = Buffer.from(data[1], "base64");
|
|
2626
|
+
return isHeifBytes(bytes.subarray(0, 4096)) ? { path: null, bytes, label: '"inline image"' } : null;
|
|
2627
|
+
}
|
|
2628
|
+
const path = resolveLocalImageSourcePath(src, options);
|
|
2629
|
+
const head = path ? readHead(path) : null;
|
|
2630
|
+
if (!path || !head || !isHeifBytes(head)) return null;
|
|
2631
|
+
return { path, bytes: readFileSync3(path), label: `"${splitUrlSuffix(src).basePath}"` };
|
|
2632
|
+
}
|
|
2633
|
+
function isUsableFile2(path) {
|
|
2634
|
+
try {
|
|
2635
|
+
return statSync3(path).size > 0;
|
|
2636
|
+
} catch {
|
|
2637
|
+
return false;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
2641
|
+
mkdirSync3(dirname4(cachePath), { recursive: true });
|
|
2642
|
+
mkdirSync3(dirname4(outputPath), { recursive: true });
|
|
2643
|
+
if (!isUsableFile2(cachePath)) {
|
|
2644
|
+
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
2645
|
+
const inlinePath = `${tmpPath}.heic`;
|
|
2646
|
+
const inputPath = source.path ?? inlinePath;
|
|
2647
|
+
try {
|
|
2648
|
+
if (!source.path) writeFileSync2(inlinePath, source.bytes);
|
|
2649
|
+
await (options.transcode ?? convertHeifToJpeg)({
|
|
2650
|
+
inputPath,
|
|
2651
|
+
outputPath: tmpPath,
|
|
2652
|
+
timeoutMs: options.timeoutMs
|
|
2653
|
+
});
|
|
2654
|
+
if (!isUsableFile2(tmpPath)) throw new Error("conversion produced an empty file");
|
|
2655
|
+
renameWithRetry4(tmpPath, cachePath);
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2658
|
+
throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
|
|
2659
|
+
} finally {
|
|
2660
|
+
rmSync3(tmpPath, { force: true });
|
|
2661
|
+
rmSync3(inlinePath, { force: true });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (resolve5(outputPath) !== resolve5(cachePath) && !isUsableFile2(outputPath)) {
|
|
2665
|
+
copyFileSync3(cachePath, outputPath);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
async function prepareHeifImageInputs(html, options) {
|
|
2669
|
+
const outputDir = options.outputDir ?? join5(options.downloadDir, PREPARED_HEIF_SUBDIR);
|
|
2670
|
+
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
|
|
2671
|
+
const cacheDir = options.cacheDir ?? outputDir;
|
|
2672
|
+
const preparedAssets = /* @__PURE__ */ new Map();
|
|
2673
|
+
const preparedImages = [];
|
|
2674
|
+
if (!/<img\b/i.test(html)) return { html, preparedAssets, preparedImages };
|
|
2675
|
+
const { document: document2 } = parseHTML4(html);
|
|
2676
|
+
const found = [];
|
|
2677
|
+
for (const img of Array.from(document2.querySelectorAll("img[src]"))) {
|
|
2678
|
+
const src = (img.getAttribute("src") || "").trim();
|
|
2679
|
+
const source = src ? resolveImageSource(src, options) : null;
|
|
2680
|
+
if (!source) continue;
|
|
2681
|
+
const hash = createHash3("sha256").update(CACHE_SCHEMA2).update("\0").update(source.bytes).digest("hex");
|
|
2682
|
+
found.push({ img, src, source, filename: `${CACHE_SCHEMA2}-${hash.slice(0, 24)}.jpg` });
|
|
2683
|
+
}
|
|
2684
|
+
if (found.length === 0) return { html, preparedAssets, preparedImages };
|
|
2685
|
+
const unique = new Map(found.map((item) => [item.filename, item.source]));
|
|
2686
|
+
const failed = /* @__PURE__ */ new Set();
|
|
2687
|
+
await Promise.all(
|
|
2688
|
+
[...unique].map(
|
|
2689
|
+
([filename, source]) => withMediaProbeSlot(
|
|
2690
|
+
() => ensurePreparedJpeg(source, join5(cacheDir, filename), join5(outputDir, filename), options)
|
|
2691
|
+
).catch((error) => {
|
|
2692
|
+
if (!options.onFailure) throw error;
|
|
2693
|
+
failed.add(filename);
|
|
2694
|
+
options.onFailure(error);
|
|
2695
|
+
})
|
|
2696
|
+
)
|
|
2697
|
+
);
|
|
2698
|
+
for (const { img, src, filename } of found) {
|
|
2699
|
+
if (failed.has(filename)) continue;
|
|
2700
|
+
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
2701
|
+
img.setAttribute("src", outputSrc);
|
|
2702
|
+
preparedAssets.set(outputSrc, join5(outputDir, filename));
|
|
2703
|
+
preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join5(outputDir, filename) });
|
|
2704
|
+
}
|
|
2705
|
+
return { html: document2.toString(), preparedAssets, preparedImages };
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// src/services/htmlCompiler.ts
|
|
2709
|
+
import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
|
|
2710
|
+
import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
|
|
2711
|
+
|
|
2401
2712
|
// src/services/assetMediaType.ts
|
|
2713
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2714
|
+
import {
|
|
2715
|
+
fingerprintElementId,
|
|
2716
|
+
isNotMediaPayload,
|
|
2717
|
+
NotMediaPayloadError,
|
|
2718
|
+
probeMediaProfile,
|
|
2719
|
+
resolveProjectRelativeSrc as resolveProjectRelativeSrc2
|
|
2720
|
+
} from "@hyperframes/engine";
|
|
2402
2721
|
var ASSET_MEDIA_TYPE_MISMATCH = "ASSET_MEDIA_TYPE_MISMATCH";
|
|
2403
2722
|
var AssetMediaTypeMismatchError = class extends Error {
|
|
2404
2723
|
code = ASSET_MEDIA_TYPE_MISMATCH;
|
|
@@ -2508,7 +2827,7 @@ function logRemoteDownloadTelemetry(event) {
|
|
|
2508
2827
|
}
|
|
2509
2828
|
var INFERRED_MEDIA_DURATION_ATTR = "data-hf-inferred-duration";
|
|
2510
2829
|
function parseSubCompHtmlForValidity(html) {
|
|
2511
|
-
return
|
|
2830
|
+
return parseHTML5(html).document;
|
|
2512
2831
|
}
|
|
2513
2832
|
function endsWithSourceUrl(code) {
|
|
2514
2833
|
const trimmed = code.trimEnd();
|
|
@@ -2516,7 +2835,7 @@ function endsWithSourceUrl(code) {
|
|
|
2516
2835
|
return lastLine.startsWith("//# sourceURL=") || lastLine.startsWith("//@ sourceURL=");
|
|
2517
2836
|
}
|
|
2518
2837
|
function prepareCompositionScripts(html) {
|
|
2519
|
-
const { document: document2 } =
|
|
2838
|
+
const { document: document2 } = parseHTML5(html);
|
|
2520
2839
|
for (const el of document2.querySelectorAll("script:not([src])")) {
|
|
2521
2840
|
const isModule = (el.getAttribute("type") || "").trim().toLowerCase() === "module";
|
|
2522
2841
|
const runsAsScript = isModule || isJavaScriptType(el);
|
|
@@ -2553,14 +2872,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
|
|
|
2553
2872
|
}
|
|
2554
2873
|
};
|
|
2555
2874
|
function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
|
|
2556
|
-
const { document: document2 } =
|
|
2875
|
+
const { document: document2 } = parseHTML5(html);
|
|
2557
2876
|
const hosts = [...document2.querySelectorAll("[data-composition-src]")];
|
|
2558
2877
|
const problems = [];
|
|
2559
2878
|
for (const el of hosts) {
|
|
2560
2879
|
const srcPath = el.getAttribute("data-composition-src");
|
|
2561
2880
|
if (!srcPath) continue;
|
|
2562
2881
|
if (isUnresolvedAssetPlaceholder(srcPath)) continue;
|
|
2563
|
-
const filePath =
|
|
2882
|
+
const filePath = resolve6(projectDir, srcPath);
|
|
2564
2883
|
if (visited.has(filePath)) continue;
|
|
2565
2884
|
const read = readProjectFile(filePath);
|
|
2566
2885
|
if (read.kind === "missing") {
|
|
@@ -2616,7 +2935,7 @@ function runtimeRoot(document2) {
|
|
|
2616
2935
|
}
|
|
2617
2936
|
function detectRenderModeHints(html, pageMotionBlur) {
|
|
2618
2937
|
const reasons = [];
|
|
2619
|
-
const { document: document2 } =
|
|
2938
|
+
const { document: document2 } = parseHTML5(html);
|
|
2620
2939
|
if (document2.querySelector("canvas[layoutsubtree]")) {
|
|
2621
2940
|
reasons.push({
|
|
2622
2941
|
code: "htmlInCanvas",
|
|
@@ -2658,7 +2977,7 @@ function detectThreeDTransformUsage(html) {
|
|
|
2658
2977
|
}
|
|
2659
2978
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
2660
2979
|
function detectAncestorBackgroundImage(html) {
|
|
2661
|
-
const { document: document2 } =
|
|
2980
|
+
const { document: document2 } = parseHTML5(html);
|
|
2662
2981
|
const root = document2.querySelector("[data-composition-id]");
|
|
2663
2982
|
if (!root) return false;
|
|
2664
2983
|
const ancestors = [];
|
|
@@ -2702,10 +3021,10 @@ function detectShaderTransitionUsage(html) {
|
|
|
2702
3021
|
}
|
|
2703
3022
|
return false;
|
|
2704
3023
|
}
|
|
2705
|
-
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
|
|
3024
|
+
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
2706
3025
|
let filePath = src;
|
|
2707
3026
|
if (isHttpUrl(src)) {
|
|
2708
|
-
if (!existsSync6(downloadDir))
|
|
3027
|
+
if (!existsSync6(downloadDir)) mkdirSync4(downloadDir, { recursive: true });
|
|
2709
3028
|
try {
|
|
2710
3029
|
filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
|
|
2711
3030
|
onTelemetry: logRemoteDownloadTelemetry
|
|
@@ -2715,7 +3034,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
2715
3034
|
}
|
|
2716
3035
|
} else {
|
|
2717
3036
|
filePath = decodedUrlPath(src);
|
|
2718
|
-
if (!filePath.startsWith("/")) filePath =
|
|
3037
|
+
if (!filePath.startsWith("/")) filePath = join6(baseDir, filePath);
|
|
2719
3038
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
2720
3039
|
}
|
|
2721
3040
|
if (!isHttpUrl(filePath) && !existsSync6(filePath)) {
|
|
@@ -2729,50 +3048,57 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
2729
3048
|
if (error instanceof Error && error.stack) wrapped.stack = error.stack;
|
|
2730
3049
|
return wrapped;
|
|
2731
3050
|
};
|
|
2732
|
-
|
|
2733
|
-
let profile;
|
|
3051
|
+
const probe = () => withMediaProbeSlot(async () => {
|
|
2734
3052
|
try {
|
|
2735
3053
|
await assertMediaPayload(filePath, elementIdentity);
|
|
2736
|
-
profile = await probeMediaProfile2(filePath);
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
if (error instanceof NotMediaPayloadError2) {
|
|
2740
|
-
log?.warn(
|
|
2741
|
-
`[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.`
|
|
2742
|
-
);
|
|
2743
|
-
}
|
|
2744
|
-
return { duration: null, resolvedPath: filePath };
|
|
3054
|
+
const profile = await probeMediaProfile2(filePath);
|
|
3055
|
+
if (tagName === "video") {
|
|
3056
|
+
return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
|
|
2745
3057
|
}
|
|
2746
|
-
throw withSrcContext(error);
|
|
2747
|
-
}
|
|
2748
|
-
assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
|
|
2749
|
-
let metadata;
|
|
2750
|
-
if (tagName === "video") {
|
|
2751
3058
|
try {
|
|
2752
|
-
|
|
2753
|
-
} catch (error) {
|
|
2754
|
-
throw withSrcContext(error);
|
|
2755
|
-
}
|
|
2756
|
-
} else {
|
|
2757
|
-
try {
|
|
2758
|
-
metadata = await extractAudioMetadata(filePath);
|
|
3059
|
+
return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
|
|
2759
3060
|
} catch {
|
|
2760
|
-
return {
|
|
3061
|
+
return { profile, fileDuration: null };
|
|
2761
3062
|
}
|
|
3063
|
+
} catch (error) {
|
|
3064
|
+
return { error };
|
|
2762
3065
|
}
|
|
2763
|
-
const fileDuration = metadata.durationSeconds;
|
|
2764
|
-
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
2765
|
-
fileDuration,
|
|
2766
|
-
mediaStart,
|
|
2767
|
-
playbackRate
|
|
2768
|
-
);
|
|
2769
|
-
return { duration, resolvedPath: filePath };
|
|
2770
3066
|
});
|
|
3067
|
+
const key = `${tagName}\0${filePath}`;
|
|
3068
|
+
let probed = probes?.get(key);
|
|
3069
|
+
if (!probed) {
|
|
3070
|
+
probed = probe();
|
|
3071
|
+
probes?.set(key, probed);
|
|
3072
|
+
}
|
|
3073
|
+
const outcome = await probed;
|
|
3074
|
+
if ("error" in outcome) {
|
|
3075
|
+
if (tagName !== "video") {
|
|
3076
|
+
if (outcome.error instanceof NotMediaPayloadError2) {
|
|
3077
|
+
log?.warn(
|
|
3078
|
+
`[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.`
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
return { duration: null, resolvedPath: filePath };
|
|
3082
|
+
}
|
|
3083
|
+
throw withSrcContext(outcome.error);
|
|
3084
|
+
}
|
|
3085
|
+
assertAssetMediaTypeProfile(
|
|
3086
|
+
tagName === "video" ? "video" : "audio",
|
|
3087
|
+
outcome.profile,
|
|
3088
|
+
elementIdentity
|
|
3089
|
+
);
|
|
3090
|
+
if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
|
|
3091
|
+
const duration = resolveNaturalMediaTimelineDurationFromValues(
|
|
3092
|
+
outcome.fileDuration,
|
|
3093
|
+
mediaStart,
|
|
3094
|
+
playbackRate
|
|
3095
|
+
);
|
|
3096
|
+
return { duration, resolvedPath: filePath };
|
|
2771
3097
|
}
|
|
2772
3098
|
function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
2773
3099
|
const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
|
|
2774
3100
|
if (unresolvedIds.size === 0) return html;
|
|
2775
|
-
const { document: document2 } =
|
|
3101
|
+
const { document: document2 } = parseHTML5(html);
|
|
2776
3102
|
let changed = false;
|
|
2777
3103
|
for (const element of document2.querySelectorAll("video[data-var-src], audio[data-var-src]")) {
|
|
2778
3104
|
if (!unresolvedIds.has(element.id)) continue;
|
|
@@ -2781,7 +3107,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
|
|
|
2781
3107
|
}
|
|
2782
3108
|
return changed ? document2.toString() : html;
|
|
2783
3109
|
}
|
|
2784
|
-
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
|
|
3110
|
+
async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
|
|
2785
3111
|
const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
|
|
2786
3112
|
const mediaUnresolved = unresolved.filter(
|
|
2787
3113
|
(el) => (el.tagName === "video" || el.tagName === "audio") && el.src
|
|
@@ -2798,7 +3124,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
2798
3124
|
el.tagName,
|
|
2799
3125
|
el.id,
|
|
2800
3126
|
log,
|
|
2801
|
-
urlByAbsentLocalPath
|
|
3127
|
+
urlByAbsentLocalPath,
|
|
3128
|
+
probes
|
|
2802
3129
|
).then(({ duration }) => ({ id: el.id, duration }))
|
|
2803
3130
|
)
|
|
2804
3131
|
);
|
|
@@ -2819,7 +3146,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
2819
3146
|
downloadDir,
|
|
2820
3147
|
el.tagName,
|
|
2821
3148
|
el.id,
|
|
2822
|
-
log
|
|
3149
|
+
log,
|
|
3150
|
+
void 0,
|
|
3151
|
+
probes
|
|
2823
3152
|
);
|
|
2824
3153
|
return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
|
|
2825
3154
|
})
|
|
@@ -2841,15 +3170,15 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
|
|
|
2841
3170
|
compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
2842
3171
|
return { html: compiledHtml, unresolvedCompositions };
|
|
2843
3172
|
}
|
|
2844
|
-
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
|
|
3173
|
+
async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
|
|
2845
3174
|
const subCompositions = /* @__PURE__ */ new Map();
|
|
2846
|
-
const { document: document2 } =
|
|
3175
|
+
const { document: document2 } = parseHTML5(html);
|
|
2847
3176
|
const compEls = document2.querySelectorAll("[data-composition-src]");
|
|
2848
3177
|
const workItems = [];
|
|
2849
3178
|
for (const el of compEls) {
|
|
2850
3179
|
const srcPath = el.getAttribute("data-composition-src");
|
|
2851
3180
|
if (!srcPath) continue;
|
|
2852
|
-
const filePath =
|
|
3181
|
+
const filePath = resolve6(projectDir, srcPath);
|
|
2853
3182
|
if (visited.has(filePath)) {
|
|
2854
3183
|
continue;
|
|
2855
3184
|
}
|
|
@@ -2866,17 +3195,19 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
2866
3195
|
workItems.map(async (item) => {
|
|
2867
3196
|
const { html: compiledSub } = await compileHtmlFile(
|
|
2868
3197
|
rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
|
|
2869
|
-
|
|
3198
|
+
dirname5(item.filePath),
|
|
2870
3199
|
downloadDir,
|
|
2871
3200
|
void 0,
|
|
2872
|
-
urlByAbsentLocalPath
|
|
3201
|
+
urlByAbsentLocalPath,
|
|
3202
|
+
probes
|
|
2873
3203
|
);
|
|
2874
3204
|
const nested = await parseSubCompositions(
|
|
2875
3205
|
compiledSub,
|
|
2876
3206
|
projectDir,
|
|
2877
3207
|
downloadDir,
|
|
2878
3208
|
item.nestedVisited,
|
|
2879
|
-
urlByAbsentLocalPath
|
|
3209
|
+
urlByAbsentLocalPath,
|
|
3210
|
+
probes
|
|
2880
3211
|
);
|
|
2881
3212
|
return {
|
|
2882
3213
|
srcPath: item.srcPath,
|
|
@@ -2894,7 +3225,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
2894
3225
|
return { subCompositions };
|
|
2895
3226
|
}
|
|
2896
3227
|
function promoteCssImportsToLinkTags(html) {
|
|
2897
|
-
const { document: document2 } =
|
|
3228
|
+
const { document: document2 } = parseHTML5(html);
|
|
2898
3229
|
const head = document2.querySelector("head");
|
|
2899
3230
|
if (!head) return html;
|
|
2900
3231
|
const importRe = /@import\s+url\(\s*['"]?([^'")\s]+)['"]?\s*\)\s*;?/gi;
|
|
@@ -2963,7 +3294,7 @@ var ProducerHostIdentityMap = class extends Map {
|
|
|
2963
3294
|
}
|
|
2964
3295
|
};
|
|
2965
3296
|
function coalesceHeadStylesAndBodyScripts(html) {
|
|
2966
|
-
const { document: document2 } =
|
|
3297
|
+
const { document: document2 } = parseHTML5(html);
|
|
2967
3298
|
const head = document2.querySelector("head");
|
|
2968
3299
|
const body = document2.querySelector("body");
|
|
2969
3300
|
if (!head && !body) return html;
|
|
@@ -3014,7 +3345,7 @@ function coalesceHeadStylesAndBodyScripts(html) {
|
|
|
3014
3345
|
return document2.toString();
|
|
3015
3346
|
}
|
|
3016
3347
|
function inlineSubCompositions(html, subCompositions, projectDir, variableOverrides = {}) {
|
|
3017
|
-
const { document: document2 } =
|
|
3348
|
+
const { document: document2 } = parseHTML5(html);
|
|
3018
3349
|
const head = document2.querySelector("head");
|
|
3019
3350
|
const body = document2.querySelector("body");
|
|
3020
3351
|
const hosts = Array.from(document2.querySelectorAll("[data-composition-src]"));
|
|
@@ -3043,17 +3374,17 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
|
|
|
3043
3374
|
resolveHtml: (srcPath) => {
|
|
3044
3375
|
let compHtml = subCompositions.get(srcPath) || null;
|
|
3045
3376
|
if (!compHtml) {
|
|
3046
|
-
const read = readProjectFile(
|
|
3377
|
+
const read = readProjectFile(resolve6(projectDir, srcPath));
|
|
3047
3378
|
if (read.kind === "file") {
|
|
3048
3379
|
compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
|
|
3049
3380
|
}
|
|
3050
3381
|
}
|
|
3051
3382
|
return compHtml;
|
|
3052
3383
|
},
|
|
3053
|
-
parseHtml: (htmlStr) =>
|
|
3384
|
+
parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
|
|
3054
3385
|
// Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
|
|
3055
3386
|
// against its own directory, project-root refs stay as authored.
|
|
3056
|
-
assetExists: (path) => existsSync6(
|
|
3387
|
+
assetExists: (path) => existsSync6(resolve6(projectDir, path)),
|
|
3057
3388
|
// Preserve the authored root wrapper as a child of the host, matching
|
|
3058
3389
|
// the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
|
|
3059
3390
|
// which the runtime compositionLoader mirrors with its own copy for the
|
|
@@ -3154,7 +3485,7 @@ ${html}
|
|
|
3154
3485
|
</html>`;
|
|
3155
3486
|
}
|
|
3156
3487
|
function injectTextRenderingRule(html) {
|
|
3157
|
-
const { document: document2 } =
|
|
3488
|
+
const { document: document2 } = parseHTML5(html);
|
|
3158
3489
|
const head = document2.querySelector("head");
|
|
3159
3490
|
if (!head) return html;
|
|
3160
3491
|
if (document2.querySelector("style[data-hyperframes-text-rendering]")) {
|
|
@@ -3178,7 +3509,7 @@ function matchesScriptIntegrity(bytes, metadata) {
|
|
|
3178
3509
|
(alg) => hashes.some((hash) => hash[1]?.toLowerCase() === alg)
|
|
3179
3510
|
);
|
|
3180
3511
|
if (!strongest) return true;
|
|
3181
|
-
const actual =
|
|
3512
|
+
const actual = createHash4(strongest).update(bytes).digest();
|
|
3182
3513
|
return hashes.some(
|
|
3183
3514
|
(hash) => hash[1]?.toLowerCase() === strongest && actual.equals(Buffer.from(hash[2], "base64"))
|
|
3184
3515
|
);
|
|
@@ -3186,7 +3517,7 @@ function matchesScriptIntegrity(bytes, metadata) {
|
|
|
3186
3517
|
async function inlineExternalScripts(html) {
|
|
3187
3518
|
const fullHtml = ensureFullDocument(html);
|
|
3188
3519
|
const wrappedFragment = fullHtml !== html;
|
|
3189
|
-
const { document: document2 } =
|
|
3520
|
+
const { document: document2 } = parseHTML5(fullHtml);
|
|
3190
3521
|
const scripts = document2.querySelectorAll("script[src]");
|
|
3191
3522
|
const externalScripts = [];
|
|
3192
3523
|
for (const el of scripts) {
|
|
@@ -3235,12 +3566,12 @@ ${safeText}
|
|
|
3235
3566
|
return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
|
|
3236
3567
|
}
|
|
3237
3568
|
function collectExternalAssets(html, projectDir) {
|
|
3238
|
-
const absProjectDir =
|
|
3569
|
+
const absProjectDir = resolve6(projectDir);
|
|
3239
3570
|
const externalAssets = /* @__PURE__ */ new Map();
|
|
3240
3571
|
function processPath(rawPath) {
|
|
3241
3572
|
const trimmed = rawPath.trim();
|
|
3242
3573
|
if (isNonRelativeUrl(trimmed)) return null;
|
|
3243
|
-
const absPath =
|
|
3574
|
+
const absPath = resolve6(absProjectDir, trimmed);
|
|
3244
3575
|
if (isPathInside2(absPath, absProjectDir)) {
|
|
3245
3576
|
return null;
|
|
3246
3577
|
}
|
|
@@ -3249,7 +3580,7 @@ function collectExternalAssets(html, projectDir) {
|
|
|
3249
3580
|
externalAssets.set(safeKey, absPath);
|
|
3250
3581
|
return safeKey;
|
|
3251
3582
|
}
|
|
3252
|
-
const { document: document2 } =
|
|
3583
|
+
const { document: document2 } = parseHTML5(html);
|
|
3253
3584
|
for (const el of document2.querySelectorAll("[src], [href]")) {
|
|
3254
3585
|
for (const attr of ["src", "href"]) {
|
|
3255
3586
|
const val = (el.getAttribute(attr) || "").trim();
|
|
@@ -3294,7 +3625,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
|
|
|
3294
3625
|
var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
3295
3626
|
async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
|
|
3296
3627
|
if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
|
|
3297
|
-
if (!existsSync6(remoteDir))
|
|
3628
|
+
if (!existsSync6(remoteDir)) mkdirSync4(remoteDir, { recursive: true });
|
|
3298
3629
|
const urlToLocal = /* @__PURE__ */ new Map();
|
|
3299
3630
|
await Promise.all(
|
|
3300
3631
|
[...urlSet].map(async (url) => {
|
|
@@ -3341,7 +3672,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
3341
3672
|
return downloadAndRewriteUrls(
|
|
3342
3673
|
urlSet,
|
|
3343
3674
|
html,
|
|
3344
|
-
|
|
3675
|
+
join6(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
3345
3676
|
"Remote media download failed",
|
|
3346
3677
|
"Localized remote media source(s)"
|
|
3347
3678
|
);
|
|
@@ -3356,7 +3687,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
3356
3687
|
return downloadAndRewriteUrls(
|
|
3357
3688
|
urlSet,
|
|
3358
3689
|
html,
|
|
3359
|
-
|
|
3690
|
+
join6(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
3360
3691
|
"Remote image download failed",
|
|
3361
3692
|
"Localized remote image source(s)"
|
|
3362
3693
|
);
|
|
@@ -3372,7 +3703,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
3372
3703
|
return downloadAndRewriteUrls(
|
|
3373
3704
|
urlSet,
|
|
3374
3705
|
html,
|
|
3375
|
-
|
|
3706
|
+
join6(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
3376
3707
|
"Remote background-image download failed",
|
|
3377
3708
|
"Localized remote background-image(s)",
|
|
3378
3709
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -3507,7 +3838,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
|
|
|
3507
3838
|
return downloadAndRewriteUrls(
|
|
3508
3839
|
urlSet,
|
|
3509
3840
|
processed,
|
|
3510
|
-
|
|
3841
|
+
join6(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
3511
3842
|
"Remote font download failed",
|
|
3512
3843
|
"Localized remote font face(s)",
|
|
3513
3844
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -3552,7 +3883,7 @@ async function embedLocalFontFaces(html, projectDir) {
|
|
|
3552
3883
|
while ((urlMatch = urlRe.exec(block)) !== null) {
|
|
3553
3884
|
const localPath = urlMatch[1];
|
|
3554
3885
|
if (!localPath || embeddedPaths.has(localPath)) continue;
|
|
3555
|
-
const absPath = localPath.startsWith("/") ? localPath :
|
|
3886
|
+
const absPath = localPath.startsWith("/") ? localPath : resolve6(projectDir, localPath);
|
|
3556
3887
|
if (!isPathInside2(absPath, projectDir)) continue;
|
|
3557
3888
|
const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
|
|
3558
3889
|
try {
|
|
@@ -3601,7 +3932,7 @@ function renderGsapDist() {
|
|
|
3601
3932
|
}
|
|
3602
3933
|
function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
|
|
3603
3934
|
let gsapDist;
|
|
3604
|
-
const exists = (path) => existsSync6(
|
|
3935
|
+
const exists = (path) => existsSync6(resolve6(projectDir, path));
|
|
3605
3936
|
return html.replace(
|
|
3606
3937
|
/(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
|
|
3607
3938
|
(full, prefix, src, file, suffix) => {
|
|
@@ -3617,8 +3948,8 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
|
|
|
3617
3948
|
if (!isPathInside2(htmlPath, projectDir)) return html;
|
|
3618
3949
|
const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
|
|
3619
3950
|
if (!entryPath.includes("/")) return html;
|
|
3620
|
-
const { document: document2 } =
|
|
3621
|
-
const assetExists = (path) => existsSync6(
|
|
3951
|
+
const { document: document2 } = parseHTML5(html);
|
|
3952
|
+
const assetExists = (path) => existsSync6(resolve6(projectDir, path));
|
|
3622
3953
|
rewriteAssetPaths(
|
|
3623
3954
|
document2.querySelectorAll("[src], [href]"),
|
|
3624
3955
|
entryPath,
|
|
@@ -3640,25 +3971,28 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
|
|
|
3640
3971
|
}
|
|
3641
3972
|
async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
|
|
3642
3973
|
const entryHtml = rebaseDirectEntryAssetPaths(
|
|
3643
|
-
|
|
3974
|
+
readFileSync4(htmlPath, "utf-8"),
|
|
3644
3975
|
projectDir,
|
|
3645
3976
|
htmlPath
|
|
3646
3977
|
);
|
|
3647
3978
|
const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
|
|
3648
3979
|
assertSubCompositionsUsable(rawHtml, projectDir);
|
|
3980
|
+
const probes = /* @__PURE__ */ new Map();
|
|
3649
3981
|
const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
|
|
3650
3982
|
rawHtml,
|
|
3651
3983
|
projectDir,
|
|
3652
3984
|
downloadDir,
|
|
3653
3985
|
options.log,
|
|
3654
|
-
options.urlByAbsentLocalPath
|
|
3986
|
+
options.urlByAbsentLocalPath,
|
|
3987
|
+
probes
|
|
3655
3988
|
);
|
|
3656
3989
|
const { subCompositions } = await parseSubCompositions(
|
|
3657
3990
|
compiledHtml,
|
|
3658
3991
|
projectDir,
|
|
3659
3992
|
downloadDir,
|
|
3660
3993
|
void 0,
|
|
3661
|
-
options.urlByAbsentLocalPath
|
|
3994
|
+
options.urlByAbsentLocalPath,
|
|
3995
|
+
probes
|
|
3662
3996
|
);
|
|
3663
3997
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
3664
3998
|
const inlinedHtml = inlineSubCompositions(
|
|
@@ -3710,12 +4044,26 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3710
4044
|
const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
|
|
3711
4045
|
const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
|
|
3712
4046
|
const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
|
|
4047
|
+
const {
|
|
4048
|
+
html: htmlWithJpegs,
|
|
4049
|
+
preparedAssets: preparedHeifAssets,
|
|
4050
|
+
preparedImages: preparedHeifImages
|
|
4051
|
+
} = await prepareHeifImageInputs(htmlWithLocalizedFonts, {
|
|
4052
|
+
projectDir,
|
|
4053
|
+
downloadDir,
|
|
4054
|
+
cacheDir: options.animatedGifCacheDir,
|
|
4055
|
+
sourceAssets: remoteImageAssets,
|
|
4056
|
+
timeoutMs: options.ffmpegProcessTimeout
|
|
4057
|
+
});
|
|
4058
|
+
if (preparedHeifImages.length > 0) {
|
|
4059
|
+
defaultLogger.info(`[Compiler] Converted ${preparedHeifImages.length} HEIC image(s) to JPEG`);
|
|
4060
|
+
}
|
|
3713
4061
|
const gifSourceAssets = new Map(remoteImageAssets);
|
|
3714
4062
|
const {
|
|
3715
4063
|
html: htmlWithPreparedGifs,
|
|
3716
4064
|
preparedAssets: preparedGifAssets,
|
|
3717
4065
|
preparedGifs
|
|
3718
|
-
} = await prepareAnimatedGifInputs(
|
|
4066
|
+
} = await prepareAnimatedGifInputs(htmlWithJpegs, {
|
|
3719
4067
|
projectDir,
|
|
3720
4068
|
downloadDir,
|
|
3721
4069
|
cacheDir: options.animatedGifCacheDir,
|
|
@@ -3742,11 +4090,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3742
4090
|
for (const [relPath, absPath] of preparedGifAssets) {
|
|
3743
4091
|
externalAssets.set(relPath, absPath);
|
|
3744
4092
|
}
|
|
4093
|
+
for (const [relPath, absPath] of preparedHeifAssets) {
|
|
4094
|
+
externalAssets.set(relPath, absPath);
|
|
4095
|
+
}
|
|
3745
4096
|
const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
|
|
3746
4097
|
const masterFxChain = parseMasterFxChain(html);
|
|
4098
|
+
const advised = /* @__PURE__ */ new Set();
|
|
3747
4099
|
for (const video of videos) {
|
|
3748
4100
|
if (isHttpUrl(video.src)) continue;
|
|
3749
|
-
const videoPath =
|
|
4101
|
+
const videoPath = resolve6(projectDir, video.src);
|
|
4102
|
+
if (advised.has(videoPath)) continue;
|
|
4103
|
+
advised.add(videoPath);
|
|
3750
4104
|
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
3751
4105
|
(async () => {
|
|
3752
4106
|
const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
|
|
@@ -3766,7 +4120,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3766
4120
|
})().catch(() => {
|
|
3767
4121
|
});
|
|
3768
4122
|
}
|
|
3769
|
-
const { document: document2 } =
|
|
4123
|
+
const { document: document2 } = parseHTML5(html);
|
|
3770
4124
|
const rootEl = document2.querySelector("[data-composition-id]");
|
|
3771
4125
|
const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
|
|
3772
4126
|
const height = rootEl ? parseInt(rootEl.getAttribute("data-height") || "1920", 10) : 1920;
|
|
@@ -4173,7 +4527,7 @@ async function runCompileStage(input) {
|
|
|
4173
4527
|
abortSignal
|
|
4174
4528
|
} = input;
|
|
4175
4529
|
const compileStart = Date.now();
|
|
4176
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
4530
|
+
const compiled = await compileForRender(projectDir, htmlPath, join7(workDir, "downloads"), {
|
|
4177
4531
|
log,
|
|
4178
4532
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
4179
4533
|
allowSystemFontCapture,
|
|
@@ -4181,7 +4535,7 @@ async function runCompileStage(input) {
|
|
|
4181
4535
|
variables: input.variables,
|
|
4182
4536
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
4183
4537
|
pageMotionBlur: input.pageMotionBlur,
|
|
4184
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
4538
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join7(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
4185
4539
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
4186
4540
|
});
|
|
4187
4541
|
assertNotAborted();
|
|
@@ -4237,7 +4591,7 @@ async function runCompileStage(input) {
|
|
|
4237
4591
|
};
|
|
4238
4592
|
await preflightCompositionAssetMediaTypes({
|
|
4239
4593
|
projectDir,
|
|
4240
|
-
compiledDir:
|
|
4594
|
+
compiledDir: join7(workDir, "compiled"),
|
|
4241
4595
|
composition,
|
|
4242
4596
|
signal: abortSignal
|
|
4243
4597
|
});
|
|
@@ -4282,8 +4636,8 @@ async function runCompileStage(input) {
|
|
|
4282
4636
|
}
|
|
4283
4637
|
|
|
4284
4638
|
// src/services/render/stages/probeStage.ts
|
|
4285
|
-
import { join as
|
|
4286
|
-
import { parseHTML as
|
|
4639
|
+
import { join as join8 } from "node:path";
|
|
4640
|
+
import { parseHTML as parseHTML6 } from "linkedom";
|
|
4287
4641
|
import {
|
|
4288
4642
|
closeCaptureSession as closeCaptureSession4,
|
|
4289
4643
|
compositionRequiresWebGpu,
|
|
@@ -4297,13 +4651,14 @@ import {
|
|
|
4297
4651
|
} from "@hyperframes/engine";
|
|
4298
4652
|
import {
|
|
4299
4653
|
MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
|
|
4300
|
-
fpsToNumber as
|
|
4654
|
+
fpsToNumber as fpsToNumber6,
|
|
4301
4655
|
normalizeRateSpec as normalizeRateSpec3,
|
|
4302
4656
|
readMediaStart as readMediaStart2,
|
|
4303
4657
|
shiftRateLane as shiftRateLane2,
|
|
4304
4658
|
sourceTimeAt as sourceTimeAt3,
|
|
4305
4659
|
timeAtSourceTime
|
|
4306
4660
|
} from "@hyperframes/core";
|
|
4661
|
+
import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
|
|
4307
4662
|
import { extractMediaSrcMutations } from "@hyperframes/parsers";
|
|
4308
4663
|
|
|
4309
4664
|
// src/services/render/stages/probeFailures.ts
|
|
@@ -4330,7 +4685,7 @@ function durationToFrameCount(duration, fps) {
|
|
|
4330
4685
|
}
|
|
4331
4686
|
function hasScriptedAudioVolumeAutomation(html, audioCount) {
|
|
4332
4687
|
if (audioCount <= 0) return false;
|
|
4333
|
-
const { document: document2 } =
|
|
4688
|
+
const { document: document2 } = parseHTML6(html);
|
|
4334
4689
|
const scriptBodies = [...document2.querySelectorAll("script")].map((script) => script.textContent ?? "").join("\n");
|
|
4335
4690
|
if (!scriptBodies) return false;
|
|
4336
4691
|
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(
|
|
@@ -4338,23 +4693,30 @@ function hasScriptedAudioVolumeAutomation(html, audioCount) {
|
|
|
4338
4693
|
);
|
|
4339
4694
|
}
|
|
4340
4695
|
function hasAutoStartVideos(html) {
|
|
4341
|
-
const { document: document2 } =
|
|
4696
|
+
const { document: document2 } = parseHTML6(html);
|
|
4342
4697
|
return document2.querySelector("video[data-hf-auto-start]") !== null;
|
|
4343
4698
|
}
|
|
4699
|
+
function variableHasValue(element, variableId, variables) {
|
|
4700
|
+
if (variables && Object.hasOwn(variables, variableId)) return true;
|
|
4701
|
+
for (let scope = element.parentElement; scope; scope = scope.parentElement) {
|
|
4702
|
+
if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
|
|
4703
|
+
if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
|
|
4704
|
+
}
|
|
4705
|
+
return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
|
|
4706
|
+
}
|
|
4344
4707
|
function hasVariableBoundMedia(html, variables) {
|
|
4345
|
-
|
|
4346
|
-
const { document: document2 } = parseHTML5(html);
|
|
4708
|
+
const { document: document2 } = parseHTML6(html);
|
|
4347
4709
|
return Array.from(
|
|
4348
4710
|
document2.querySelectorAll(
|
|
4349
4711
|
"img[data-var-src], audio[data-var-src], video[data-var-src], source[data-var-src]"
|
|
4350
4712
|
)
|
|
4351
4713
|
).some((element) => {
|
|
4352
4714
|
const variableId = element.getAttribute("data-var-src")?.trim();
|
|
4353
|
-
return Boolean(variableId &&
|
|
4715
|
+
return Boolean(variableId && variableHasValue(element, variableId, variables));
|
|
4354
4716
|
});
|
|
4355
4717
|
}
|
|
4356
4718
|
function authoredMediaStarts(html) {
|
|
4357
|
-
const { document: document2 } =
|
|
4719
|
+
const { document: document2 } = parseHTML6(html);
|
|
4358
4720
|
const starts = /* @__PURE__ */ new Map();
|
|
4359
4721
|
for (const media of document2.querySelectorAll("video, audio")) {
|
|
4360
4722
|
const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
|
|
@@ -4391,7 +4753,7 @@ function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, dura
|
|
|
4391
4753
|
return existingEnd <= 0 ? projectedEnd : Math.min(existingEnd, projectedEnd);
|
|
4392
4754
|
}
|
|
4393
4755
|
function hasRuntimeMediaChanges(html) {
|
|
4394
|
-
const { document: document2 } =
|
|
4756
|
+
const { document: document2 } = parseHTML6(html);
|
|
4395
4757
|
const scriptBodies = [...document2.querySelectorAll("script")].map(
|
|
4396
4758
|
(script) => script.textContent ?? ""
|
|
4397
4759
|
);
|
|
@@ -4474,7 +4836,7 @@ async function runProbeStage(input) {
|
|
|
4474
4836
|
});
|
|
4475
4837
|
fileServer = await createFileServer({
|
|
4476
4838
|
projectDir,
|
|
4477
|
-
compiledDir:
|
|
4839
|
+
compiledDir: join8(workDir, "compiled"),
|
|
4478
4840
|
port: 0,
|
|
4479
4841
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
4480
4842
|
fps: job.config.fps
|
|
@@ -4503,7 +4865,7 @@ async function runProbeStage(input) {
|
|
|
4503
4865
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
4504
4866
|
probeSession = await createCaptureSession4(
|
|
4505
4867
|
fileServer.url,
|
|
4506
|
-
|
|
4868
|
+
join8(workDir, "probe"),
|
|
4507
4869
|
captureOpts,
|
|
4508
4870
|
null,
|
|
4509
4871
|
probeCfg
|
|
@@ -4588,7 +4950,7 @@ async function runProbeStage(input) {
|
|
|
4588
4950
|
});
|
|
4589
4951
|
probeSession = await createCaptureSession4(
|
|
4590
4952
|
fileServer.url,
|
|
4591
|
-
|
|
4953
|
+
join8(workDir, "probe-screenshot"),
|
|
4592
4954
|
captureOpts,
|
|
4593
4955
|
null,
|
|
4594
4956
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -4624,7 +4986,7 @@ async function runProbeStage(input) {
|
|
|
4624
4986
|
compiled,
|
|
4625
4987
|
resolutions,
|
|
4626
4988
|
projectDir,
|
|
4627
|
-
|
|
4989
|
+
join8(workDir, "downloads")
|
|
4628
4990
|
);
|
|
4629
4991
|
assertNotAborted();
|
|
4630
4992
|
composition.videos = compiled.videos;
|
|
@@ -4776,7 +5138,7 @@ async function runProbeStage(input) {
|
|
|
4776
5138
|
session.page,
|
|
4777
5139
|
composition.audios.map((audio) => audio.id),
|
|
4778
5140
|
composition.duration,
|
|
4779
|
-
|
|
5141
|
+
fpsToNumber6(job.config.fps)
|
|
4780
5142
|
);
|
|
4781
5143
|
assertNotAborted();
|
|
4782
5144
|
if (automation.length > 0) {
|
|
@@ -4815,7 +5177,7 @@ async function runProbeStage(input) {
|
|
|
4815
5177
|
}
|
|
4816
5178
|
await preflightCompositionAssetMediaTypes({
|
|
4817
5179
|
projectDir,
|
|
4818
|
-
compiledDir:
|
|
5180
|
+
compiledDir: join8(workDir, "compiled"),
|
|
4819
5181
|
composition,
|
|
4820
5182
|
signal: abortSignal
|
|
4821
5183
|
});
|
|
@@ -4839,7 +5201,7 @@ async function runProbeStage(input) {
|
|
|
4839
5201
|
}
|
|
4840
5202
|
const browserProbeMs = Date.now() - probeStart;
|
|
4841
5203
|
const duration = composition.duration;
|
|
4842
|
-
const totalFrames = durationToFrameCount(duration,
|
|
5204
|
+
const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
|
|
4843
5205
|
if (duration <= 0) {
|
|
4844
5206
|
const diagnostics = [];
|
|
4845
5207
|
try {
|
|
@@ -4980,7 +5342,7 @@ function validateDistributedDuration(input) {
|
|
|
4980
5342
|
|
|
4981
5343
|
// src/services/render/stages/extractVideosStage.ts
|
|
4982
5344
|
import { existsSync as existsSync7 } from "node:fs";
|
|
4983
|
-
import { join as
|
|
5345
|
+
import { join as join9 } from "node:path";
|
|
4984
5346
|
import {
|
|
4985
5347
|
classifyVideoExtractionError,
|
|
4986
5348
|
createFrameLookupTable,
|
|
@@ -5300,14 +5662,14 @@ async function runExtractVideosStage(input) {
|
|
|
5300
5662
|
// because short boundary counts can differ by one frame.
|
|
5301
5663
|
{
|
|
5302
5664
|
fps: job.config.fps,
|
|
5303
|
-
outputDir:
|
|
5665
|
+
outputDir: join9(compiledDir, "__hyperframes_video_frames"),
|
|
5304
5666
|
format: job.config.videoFrameFormat ?? "auto",
|
|
5305
5667
|
toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
|
|
5306
5668
|
timelineEnd: composition.duration,
|
|
5307
5669
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
5308
5670
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
5309
5671
|
deferRangeExtraction,
|
|
5310
|
-
contentKeyedDirs: [
|
|
5672
|
+
contentKeyedDirs: [join9(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
5311
5673
|
urlByAbsentLocalPath
|
|
5312
5674
|
},
|
|
5313
5675
|
abortSignal,
|
|
@@ -5401,18 +5763,19 @@ async function appendVideoAudioFromProbe(input) {
|
|
|
5401
5763
|
}
|
|
5402
5764
|
|
|
5403
5765
|
// src/services/render/stages/audioStage.ts
|
|
5404
|
-
import { join as
|
|
5405
|
-
import { rmSync as rmSync4 } from "node:fs";
|
|
5766
|
+
import { join as join11 } from "node:path";
|
|
5406
5767
|
import {
|
|
5407
5768
|
MIXED_AUDIO_FILENAME,
|
|
5408
5769
|
audioFailureDiagnostics,
|
|
5409
|
-
processCompositionAudio
|
|
5770
|
+
processCompositionAudio,
|
|
5771
|
+
removeScratch as removeScratch2,
|
|
5772
|
+
collectScratchLeftovers
|
|
5410
5773
|
} from "@hyperframes/engine";
|
|
5411
5774
|
|
|
5412
5775
|
// src/services/render/audioPadTrim.ts
|
|
5413
5776
|
import { spawn } from "node:child_process";
|
|
5414
|
-
import { mkdtempSync
|
|
5415
|
-
import { dirname as
|
|
5777
|
+
import { mkdtempSync } from "node:fs";
|
|
5778
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
5416
5779
|
import {
|
|
5417
5780
|
DEFAULT_CONFIG,
|
|
5418
5781
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -5420,8 +5783,9 @@ import {
|
|
|
5420
5783
|
extractAudioMetadata as extractAudioMetadata3,
|
|
5421
5784
|
getFfprobeBinary,
|
|
5422
5785
|
ManagedChildProcess,
|
|
5423
|
-
|
|
5424
|
-
|
|
5786
|
+
removeScratch,
|
|
5787
|
+
renameWithRetry as renameWithRetry5,
|
|
5788
|
+
runFfmpeg as runFfmpeg5,
|
|
5425
5789
|
trackChildProcess
|
|
5426
5790
|
} from "@hyperframes/engine";
|
|
5427
5791
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
@@ -5431,7 +5795,7 @@ import {
|
|
|
5431
5795
|
aacSegmentCount,
|
|
5432
5796
|
describeFfmpegFailure,
|
|
5433
5797
|
extractAudioMetadata as extractAudioMetadata2,
|
|
5434
|
-
runFfmpeg as
|
|
5798
|
+
runFfmpeg as runFfmpeg4
|
|
5435
5799
|
} from "@hyperframes/engine";
|
|
5436
5800
|
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
5437
5801
|
function checkedDbfs(value) {
|
|
@@ -5455,7 +5819,7 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
5455
5819
|
else laterBlocks = Math.max(laterBlocks, block);
|
|
5456
5820
|
}
|
|
5457
5821
|
};
|
|
5458
|
-
const result = await
|
|
5822
|
+
const result = await runFfmpeg4(
|
|
5459
5823
|
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
5460
5824
|
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
5461
5825
|
);
|
|
@@ -5615,7 +5979,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5615
5979
|
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
5616
5980
|
};
|
|
5617
5981
|
} finally {
|
|
5618
|
-
for (const path of plan2.cleanupPaths)
|
|
5982
|
+
for (const path of plan2.cleanupPaths) removeScratch(path);
|
|
5619
5983
|
}
|
|
5620
5984
|
let loweredDb = 0;
|
|
5621
5985
|
if (probeTruePeak) {
|
|
@@ -5662,8 +6026,8 @@ async function constrainAacTruePeak(input) {
|
|
|
5662
6026
|
async function enforceAacTruePeak(input) {
|
|
5663
6027
|
let scratchDir;
|
|
5664
6028
|
try {
|
|
5665
|
-
scratchDir = mkdtempSync(
|
|
5666
|
-
const correctedPath =
|
|
6029
|
+
scratchDir = mkdtempSync(join10(dirname6(input.audioPath), ".true-peak-"));
|
|
6030
|
+
const correctedPath = join10(scratchDir, "audio.m4a");
|
|
5667
6031
|
const measurements = [];
|
|
5668
6032
|
let attenuationDb = 0;
|
|
5669
6033
|
let sourcePeakDbfs;
|
|
@@ -5678,7 +6042,7 @@ async function enforceAacTruePeak(input) {
|
|
|
5678
6042
|
}
|
|
5679
6043
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
5680
6044
|
if (measuredPath !== correctedPath) return { success: true };
|
|
5681
|
-
|
|
6045
|
+
renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
|
|
5682
6046
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
5683
6047
|
}
|
|
5684
6048
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -5719,7 +6083,7 @@ async function enforceAacTruePeak(input) {
|
|
|
5719
6083
|
)}`
|
|
5720
6084
|
};
|
|
5721
6085
|
} finally {
|
|
5722
|
-
if (scratchDir)
|
|
6086
|
+
if (scratchDir) removeScratch(scratchDir);
|
|
5723
6087
|
}
|
|
5724
6088
|
}
|
|
5725
6089
|
function failResult(outputPath, target, source, error) {
|
|
@@ -5796,7 +6160,7 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
5796
6160
|
};
|
|
5797
6161
|
}
|
|
5798
6162
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
5799
|
-
return toStepResult(await
|
|
6163
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
5800
6164
|
}
|
|
5801
6165
|
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
5802
6166
|
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
@@ -5864,9 +6228,17 @@ function assertAudioNotInterrupted(failures) {
|
|
|
5864
6228
|
])
|
|
5865
6229
|
});
|
|
5866
6230
|
}
|
|
5867
|
-
|
|
6231
|
+
function runAudioStage(input) {
|
|
6232
|
+
return collectScratchLeftovers(
|
|
6233
|
+
() => runAudioStageScoped(input),
|
|
6234
|
+
(left) => {
|
|
6235
|
+
if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
|
|
6236
|
+
}
|
|
6237
|
+
);
|
|
6238
|
+
}
|
|
6239
|
+
async function runAudioStageScoped(input) {
|
|
5868
6240
|
const stageStart = Date.now();
|
|
5869
|
-
const pcmSourcePath =
|
|
6241
|
+
const pcmSourcePath = join11(input.workDir, "audio-source.wav");
|
|
5870
6242
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
5871
6243
|
try {
|
|
5872
6244
|
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
@@ -5886,7 +6258,7 @@ async function runAudioStage(input) {
|
|
|
5886
6258
|
}
|
|
5887
6259
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
5888
6260
|
} finally {
|
|
5889
|
-
|
|
6261
|
+
removeScratch2(pcmSourcePath);
|
|
5890
6262
|
}
|
|
5891
6263
|
}
|
|
5892
6264
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -5904,7 +6276,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
5904
6276
|
assertNotAborted,
|
|
5905
6277
|
log
|
|
5906
6278
|
} = input;
|
|
5907
|
-
const audioOutputPath =
|
|
6279
|
+
const audioOutputPath = join11(workDir, MIXED_AUDIO_FILENAME);
|
|
5908
6280
|
let hasAudio = false;
|
|
5909
6281
|
let audioError;
|
|
5910
6282
|
let audioFailures;
|
|
@@ -5915,7 +6287,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
5915
6287
|
audioResult = await processCompositionAudio(
|
|
5916
6288
|
audios,
|
|
5917
6289
|
projectDir,
|
|
5918
|
-
|
|
6290
|
+
join11(workDir, "audio-work"),
|
|
5919
6291
|
audioOutputPath,
|
|
5920
6292
|
duration,
|
|
5921
6293
|
abortSignal,
|
|
@@ -5984,7 +6356,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
5984
6356
|
|
|
5985
6357
|
// src/services/render/stages/captureStage.ts
|
|
5986
6358
|
import { existsSync as existsSync8, readdirSync, statfsSync } from "node:fs";
|
|
5987
|
-
import { join as
|
|
6359
|
+
import { join as join12 } from "node:path";
|
|
5988
6360
|
import {
|
|
5989
6361
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
5990
6362
|
frameFileExtension,
|
|
@@ -6038,13 +6410,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
6038
6410
|
if (!existsSync8(workDir)) return total;
|
|
6039
6411
|
for (const attempt of readdirSync(workDir)) {
|
|
6040
6412
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
6041
|
-
total += sampleDirectoryBytes(
|
|
6413
|
+
total += sampleDirectoryBytes(join12(workDir, attempt));
|
|
6042
6414
|
continue;
|
|
6043
6415
|
}
|
|
6044
6416
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
6045
|
-
const attemptDir =
|
|
6417
|
+
const attemptDir = join12(workDir, attempt);
|
|
6046
6418
|
for (const name of readdirSync(attemptDir)) {
|
|
6047
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
6419
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join12(attemptDir, name));
|
|
6048
6420
|
}
|
|
6049
6421
|
}
|
|
6050
6422
|
return total;
|
|
@@ -6345,7 +6717,7 @@ import {
|
|
|
6345
6717
|
initializeSession as initializeSession7,
|
|
6346
6718
|
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
6347
6719
|
} from "@hyperframes/engine";
|
|
6348
|
-
import { fpsToNumber as
|
|
6720
|
+
import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
|
|
6349
6721
|
|
|
6350
6722
|
// src/services/hdrImageTransferCache.ts
|
|
6351
6723
|
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
@@ -6370,24 +6742,24 @@ import {
|
|
|
6370
6742
|
|
|
6371
6743
|
// src/services/render/stages/encodeStage.ts
|
|
6372
6744
|
import {
|
|
6373
|
-
copyFileSync as
|
|
6745
|
+
copyFileSync as copyFileSync4,
|
|
6374
6746
|
existsSync as existsSync9,
|
|
6375
|
-
mkdirSync as
|
|
6747
|
+
mkdirSync as mkdirSync5,
|
|
6376
6748
|
readdirSync as readdirSync2,
|
|
6377
|
-
readFileSync as
|
|
6378
|
-
rmSync as
|
|
6379
|
-
statSync as
|
|
6380
|
-
writeFileSync as
|
|
6749
|
+
readFileSync as readFileSync5,
|
|
6750
|
+
rmSync as rmSync4,
|
|
6751
|
+
statSync as statSync4,
|
|
6752
|
+
writeFileSync as writeFileSync3
|
|
6381
6753
|
} from "node:fs";
|
|
6382
6754
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
6383
|
-
import { dirname as
|
|
6755
|
+
import { dirname as dirname7, join as join13 } from "node:path";
|
|
6384
6756
|
import {
|
|
6385
6757
|
encodeFramesChunkedConcat,
|
|
6386
6758
|
encodeFramesFromDir,
|
|
6387
6759
|
formatFfmpegError,
|
|
6388
6760
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
6389
6761
|
resolveConfig,
|
|
6390
|
-
runFfmpeg as
|
|
6762
|
+
runFfmpeg as runFfmpeg6,
|
|
6391
6763
|
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
6392
6764
|
} from "@hyperframes/engine";
|
|
6393
6765
|
import {
|
|
@@ -6463,17 +6835,17 @@ function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
|
6463
6835
|
}
|
|
6464
6836
|
async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
6465
6837
|
const { outputPath } = argsInput;
|
|
6466
|
-
const result = await
|
|
6838
|
+
const result = await runFfmpeg6(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
|
|
6467
6839
|
if (!result.success) {
|
|
6468
6840
|
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
6469
6841
|
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
6470
6842
|
}
|
|
6471
|
-
const gif =
|
|
6843
|
+
const gif = readFileSync5(outputPath);
|
|
6472
6844
|
if (!clearGifFramesBeforeNext(gif)) {
|
|
6473
6845
|
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
6474
6846
|
return gifEncodeFailure(startTime, outputPath, error);
|
|
6475
6847
|
}
|
|
6476
|
-
|
|
6848
|
+
writeFileSync3(outputPath, gif);
|
|
6477
6849
|
return null;
|
|
6478
6850
|
}
|
|
6479
6851
|
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
@@ -6495,17 +6867,17 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
6495
6867
|
try {
|
|
6496
6868
|
const run = { signal: input.signal, timeout: input.timeout };
|
|
6497
6869
|
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
6498
|
-
const result = await
|
|
6870
|
+
const result = await runFfmpeg6(args, run);
|
|
6499
6871
|
if (!result.success) {
|
|
6500
6872
|
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
6501
6873
|
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
6502
6874
|
}
|
|
6503
6875
|
}
|
|
6504
|
-
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(
|
|
6876
|
+
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync5(outputPath)) !== false) {
|
|
6505
6877
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
6506
6878
|
if (failure) return failure;
|
|
6507
6879
|
}
|
|
6508
|
-
const fileSize = existsSync9(outputPath) ?
|
|
6880
|
+
const fileSize = existsSync9(outputPath) ? statSync4(outputPath).size : 0;
|
|
6509
6881
|
return {
|
|
6510
6882
|
success: true,
|
|
6511
6883
|
outputPath,
|
|
@@ -6514,7 +6886,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
6514
6886
|
fileSize
|
|
6515
6887
|
};
|
|
6516
6888
|
} finally {
|
|
6517
|
-
|
|
6889
|
+
rmSync4(input.palettePath, { force: true });
|
|
6518
6890
|
}
|
|
6519
6891
|
}
|
|
6520
6892
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -6538,6 +6910,8 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
6538
6910
|
preset: preset.preset,
|
|
6539
6911
|
quality: input.effectiveQuality,
|
|
6540
6912
|
bitrate: input.effectiveBitrate,
|
|
6913
|
+
maxBitrate: input.maxBitrate,
|
|
6914
|
+
bufferSize: input.bufferSize,
|
|
6541
6915
|
pixelFormat: preset.pixelFormat,
|
|
6542
6916
|
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
6543
6917
|
useGpu: job.config.useGpu,
|
|
@@ -6586,10 +6960,10 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
6586
6960
|
const stream = await encoder;
|
|
6587
6961
|
for (let next = 0; next < totalFrames; next++) {
|
|
6588
6962
|
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
6589
|
-
await new Promise((
|
|
6963
|
+
await new Promise((resolve10) => wake = resolve10);
|
|
6590
6964
|
}
|
|
6591
6965
|
if (signal.aborted) return;
|
|
6592
|
-
const mergedPath =
|
|
6966
|
+
const mergedPath = join13(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
6593
6967
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
6594
6968
|
landed.delete(next);
|
|
6595
6969
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -6664,7 +7038,7 @@ async function runEncodeStage(input) {
|
|
|
6664
7038
|
const stage5Start = Date.now();
|
|
6665
7039
|
if (isPngSequence) {
|
|
6666
7040
|
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
6667
|
-
if (!existsSync9(outputPath))
|
|
7041
|
+
if (!existsSync9(outputPath)) mkdirSync5(outputPath, { recursive: true });
|
|
6668
7042
|
const captured = readdirSync2(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
6669
7043
|
if (captured.length === 0) {
|
|
6670
7044
|
throw new Error(
|
|
@@ -6672,11 +7046,11 @@ async function runEncodeStage(input) {
|
|
|
6672
7046
|
);
|
|
6673
7047
|
}
|
|
6674
7048
|
captured.forEach((name, i) => {
|
|
6675
|
-
const dst =
|
|
6676
|
-
|
|
7049
|
+
const dst = join13(outputPath, formatExportFrameName(i, "png"));
|
|
7050
|
+
copyFileSync4(join13(framesDir, name), dst);
|
|
6677
7051
|
});
|
|
6678
7052
|
if (hasAudio && audioOutputPath && existsSync9(audioOutputPath)) {
|
|
6679
|
-
|
|
7053
|
+
copyFileSync4(audioOutputPath, join13(outputPath, MIXED_AUDIO_FILENAME2));
|
|
6680
7054
|
log.info(
|
|
6681
7055
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
6682
7056
|
);
|
|
@@ -6696,7 +7070,7 @@ async function runEncodeStage(input) {
|
|
|
6696
7070
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
6697
7071
|
fps: job.config.fps,
|
|
6698
7072
|
loop,
|
|
6699
|
-
palettePath:
|
|
7073
|
+
palettePath: join13(dirname7(videoOnlyPath), "gif-palette.png"),
|
|
6700
7074
|
preserveAlpha: needsAlpha,
|
|
6701
7075
|
signal: abortSignal,
|
|
6702
7076
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -6755,9 +7129,9 @@ function sampleDirectoryBytes(dir) {
|
|
|
6755
7129
|
continue;
|
|
6756
7130
|
}
|
|
6757
7131
|
for (const name of entries) {
|
|
6758
|
-
const full =
|
|
7132
|
+
const full = join14(current, name);
|
|
6759
7133
|
try {
|
|
6760
|
-
const st =
|
|
7134
|
+
const st = statSync5(full);
|
|
6761
7135
|
if (st.isDirectory()) {
|
|
6762
7136
|
stack.push(full);
|
|
6763
7137
|
} else if (st.isFile()) {
|
|
@@ -6841,8 +7215,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
6841
7215
|
const ranges = [];
|
|
6842
7216
|
let rangeStart = null;
|
|
6843
7217
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
6844
|
-
const framePath =
|
|
6845
|
-
const missing = !existsSync10(framePath) ||
|
|
7218
|
+
const framePath = join14(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
7219
|
+
const missing = !existsSync10(framePath) || statSync5(framePath).size <= 8;
|
|
6846
7220
|
if (missing && rangeStart === null) {
|
|
6847
7221
|
rangeStart = frameIndex;
|
|
6848
7222
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -6858,7 +7232,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
6858
7232
|
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
6859
7233
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
6860
7234
|
const batches = [];
|
|
6861
|
-
const outputDirFor = (batchIndex, workerId) =>
|
|
7235
|
+
const outputDirFor = (batchIndex, workerId) => join14(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
6862
7236
|
if (stride > 1) {
|
|
6863
7237
|
const spans = /* @__PURE__ */ new Map();
|
|
6864
7238
|
for (const range of ranges) {
|
|
@@ -6926,7 +7300,7 @@ function isRecoverableParallelCaptureError(error) {
|
|
|
6926
7300
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
6927
7301
|
let captured = 0;
|
|
6928
7302
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
6929
|
-
const framePath =
|
|
7303
|
+
const framePath = join14(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
6930
7304
|
if (existsSync10(framePath)) captured++;
|
|
6931
7305
|
}
|
|
6932
7306
|
return captured;
|
|
@@ -6952,7 +7326,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
6952
7326
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
6953
7327
|
});
|
|
6954
7328
|
pendingTransientRetry = false;
|
|
6955
|
-
const attemptWorkDir =
|
|
7329
|
+
const attemptWorkDir = join14(options.workDir, `capture-attempt-${attempt}`);
|
|
6956
7330
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
6957
7331
|
missingRanges,
|
|
6958
7332
|
currentWorkers,
|
|
@@ -7143,7 +7517,7 @@ function explainStreamingEncodeGate(cfg, outputFormat, workerCount, durationSeco
|
|
|
7143
7517
|
var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
|
|
7144
7518
|
|
|
7145
7519
|
// src/services/distributed/planFrameCost.ts
|
|
7146
|
-
import { join as
|
|
7520
|
+
import { join as join17 } from "node:path";
|
|
7147
7521
|
import {
|
|
7148
7522
|
closeCaptureSession as closeCaptureSession9,
|
|
7149
7523
|
createCaptureSession as createCaptureSession9,
|
|
@@ -7153,7 +7527,7 @@ import {
|
|
|
7153
7527
|
|
|
7154
7528
|
// src/services/distributed/chunkFrames.ts
|
|
7155
7529
|
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "node:fs";
|
|
7156
|
-
import { extname as extname2, join as
|
|
7530
|
+
import { extname as extname2, join as join16 } from "node:path";
|
|
7157
7531
|
import {
|
|
7158
7532
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
7159
7533
|
createFrameLookupTable as createFrameLookupTable2,
|
|
@@ -7163,8 +7537,8 @@ import {
|
|
|
7163
7537
|
|
|
7164
7538
|
// src/services/distributed/shared.ts
|
|
7165
7539
|
import { execFile as execFileCallback } from "node:child_process";
|
|
7166
|
-
import { dirname as
|
|
7167
|
-
import { existsSync as existsSync11, readFileSync as
|
|
7540
|
+
import { dirname as dirname9, join as join15 } from "node:path";
|
|
7541
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
|
|
7168
7542
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7169
7543
|
import { promisify as promisify2 } from "node:util";
|
|
7170
7544
|
import {
|
|
@@ -7179,7 +7553,7 @@ function isPlanAudioArtifactPath(path) {
|
|
|
7179
7553
|
}
|
|
7180
7554
|
function resolvePlanAudioPath(planDir) {
|
|
7181
7555
|
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
7182
|
-
const candidate =
|
|
7556
|
+
const candidate = join15(planDir, name);
|
|
7183
7557
|
if (existsSync11(candidate)) return candidate;
|
|
7184
7558
|
}
|
|
7185
7559
|
return null;
|
|
@@ -7436,13 +7810,13 @@ function buildSyntheticRenderJob(input) {
|
|
|
7436
7810
|
var cachedProducerVersion = null;
|
|
7437
7811
|
function readProducerVersion() {
|
|
7438
7812
|
if (cachedProducerVersion !== null) return cachedProducerVersion;
|
|
7439
|
-
const startDir =
|
|
7813
|
+
const startDir = dirname9(fileURLToPath3(import.meta.url));
|
|
7440
7814
|
let current = startDir;
|
|
7441
7815
|
for (let i = 0; i < 10; i++) {
|
|
7442
|
-
const candidate =
|
|
7816
|
+
const candidate = join15(current, "package.json");
|
|
7443
7817
|
if (existsSync11(candidate)) {
|
|
7444
7818
|
try {
|
|
7445
|
-
const pkg = JSON.parse(
|
|
7819
|
+
const pkg = JSON.parse(readFileSync7(candidate, "utf-8"));
|
|
7446
7820
|
if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
|
|
7447
7821
|
cachedProducerVersion = pkg.version;
|
|
7448
7822
|
return pkg.version;
|
|
@@ -7450,7 +7824,7 @@ function readProducerVersion() {
|
|
|
7450
7824
|
} catch {
|
|
7451
7825
|
}
|
|
7452
7826
|
}
|
|
7453
|
-
const parent =
|
|
7827
|
+
const parent = dirname9(current);
|
|
7454
7828
|
if (parent === current) break;
|
|
7455
7829
|
current = parent;
|
|
7456
7830
|
}
|
|
@@ -7493,7 +7867,7 @@ function frameNumberFromFileName(name) {
|
|
|
7493
7867
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
7494
7868
|
const result = [];
|
|
7495
7869
|
for (const v of videos) {
|
|
7496
|
-
const outputDir =
|
|
7870
|
+
const outputDir = join16(planDir, "video-frames", v.videoId);
|
|
7497
7871
|
if (!existsSync12(outputDir)) {
|
|
7498
7872
|
throw new Error(
|
|
7499
7873
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
@@ -7512,7 +7886,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
7512
7886
|
if (!frameName) continue;
|
|
7513
7887
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
7514
7888
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
7515
|
-
framePaths.set(frameIndex,
|
|
7889
|
+
framePaths.set(frameIndex, join16(outputDir, frameName));
|
|
7516
7890
|
}
|
|
7517
7891
|
result.push({
|
|
7518
7892
|
videoId: v.videoId,
|
|
@@ -7602,14 +7976,14 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
7602
7976
|
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
7603
7977
|
return Promise.all(
|
|
7604
7978
|
deferred.map(async (video) => {
|
|
7605
|
-
const outputDir =
|
|
7979
|
+
const outputDir = join16(input.outputRoot, video.videoId);
|
|
7606
7980
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
7607
7981
|
if (indexes.length === 0) {
|
|
7608
7982
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
7609
7983
|
}
|
|
7610
7984
|
const range = video.deferredRange;
|
|
7611
7985
|
const remoteUrl = remoteSources[range.sourcePath];
|
|
7612
|
-
const sourcePath = remoteUrl ??
|
|
7986
|
+
const sourcePath = remoteUrl ?? join16(input.planDir, ...range.sourcePath.split("/"));
|
|
7613
7987
|
if (remoteUrl === void 0 && !existsSync12(sourcePath)) {
|
|
7614
7988
|
throw new RenderChunkValidationError(
|
|
7615
7989
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -7658,12 +8032,12 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7658
8032
|
planVideos,
|
|
7659
8033
|
frames: sampled,
|
|
7660
8034
|
fps: fpsRatio,
|
|
7661
|
-
outputRoot:
|
|
8035
|
+
outputRoot: join17(workDir, "frame-cost-video-frames"),
|
|
7662
8036
|
cfg: input.cfg
|
|
7663
8037
|
})
|
|
7664
8038
|
];
|
|
7665
8039
|
const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
7666
|
-
const compiledDir =
|
|
8040
|
+
const compiledDir = join17(planDir, "compiled");
|
|
7667
8041
|
const fileServer = await createFileServer({
|
|
7668
8042
|
projectDir: compiledDir,
|
|
7669
8043
|
compiledDir,
|
|
@@ -7675,7 +8049,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7675
8049
|
try {
|
|
7676
8050
|
session = await createCaptureSession9(
|
|
7677
8051
|
fileServer.url,
|
|
7678
|
-
|
|
8052
|
+
join17(workDir, "frame-cost"),
|
|
7679
8053
|
buildChunkCaptureOptions({
|
|
7680
8054
|
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
7681
8055
|
encoder: input,
|
|
@@ -7772,8 +8146,8 @@ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
|
|
|
7772
8146
|
}
|
|
7773
8147
|
|
|
7774
8148
|
// src/services/render/stages/freezePlan.ts
|
|
7775
|
-
import { existsSync as existsSync13, mkdirSync as
|
|
7776
|
-
import { join as
|
|
8149
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8150
|
+
import { join as join18, relative as relative3, resolve as resolve8 } from "node:path";
|
|
7777
8151
|
|
|
7778
8152
|
// src/services/distributed/planProtocol.ts
|
|
7779
8153
|
var PLAN_SCHEMA_VERSION = 1;
|
|
@@ -7871,12 +8245,12 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
|
|
|
7871
8245
|
}
|
|
7872
8246
|
|
|
7873
8247
|
// src/services/render/stages/planHash.ts
|
|
7874
|
-
import { createHash as
|
|
7875
|
-
import { closeSync as
|
|
8248
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
8249
|
+
import { closeSync as closeSync3, createReadStream as createReadStream3, openSync as openSync3, readSync as readSync2 } from "node:fs";
|
|
7876
8250
|
var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
|
|
7877
8251
|
var FIELD_DELIMITER = Buffer.from([0]);
|
|
7878
8252
|
function computePlanHash(input) {
|
|
7879
|
-
const hash =
|
|
8253
|
+
const hash = createHash5("sha256");
|
|
7880
8254
|
hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
|
|
7881
8255
|
hash.update(input.compositionHtml);
|
|
7882
8256
|
hash.update(FIELD_DELIMITER);
|
|
@@ -7923,27 +8297,27 @@ function canonicalJsonStringify(value) {
|
|
|
7923
8297
|
throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
|
|
7924
8298
|
}
|
|
7925
8299
|
function sha256Hex(bytes) {
|
|
7926
|
-
const h =
|
|
8300
|
+
const h = createHash5("sha256");
|
|
7927
8301
|
h.update(bytes);
|
|
7928
8302
|
return h.digest("hex");
|
|
7929
8303
|
}
|
|
7930
8304
|
function sha256File(path) {
|
|
7931
|
-
const hash =
|
|
8305
|
+
const hash = createHash5("sha256");
|
|
7932
8306
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
7933
|
-
const fd =
|
|
8307
|
+
const fd = openSync3(path, "r");
|
|
7934
8308
|
try {
|
|
7935
8309
|
let bytesRead = 0;
|
|
7936
8310
|
do {
|
|
7937
|
-
bytesRead =
|
|
8311
|
+
bytesRead = readSync2(fd, buffer, 0, buffer.byteLength, null);
|
|
7938
8312
|
if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
|
|
7939
8313
|
} while (bytesRead > 0);
|
|
7940
8314
|
} finally {
|
|
7941
|
-
|
|
8315
|
+
closeSync3(fd);
|
|
7942
8316
|
}
|
|
7943
8317
|
return hash.digest("hex");
|
|
7944
8318
|
}
|
|
7945
8319
|
async function sha256FileAsync(path) {
|
|
7946
|
-
const hash =
|
|
8320
|
+
const hash = createHash5("sha256");
|
|
7947
8321
|
for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
|
|
7948
8322
|
hash.update(chunk);
|
|
7949
8323
|
}
|
|
@@ -7987,11 +8361,11 @@ function stripUndefined(value) {
|
|
|
7987
8361
|
}
|
|
7988
8362
|
function listPlanFiles(planDir) {
|
|
7989
8363
|
const results = [];
|
|
7990
|
-
const rootResolved =
|
|
8364
|
+
const rootResolved = resolve8(planDir);
|
|
7991
8365
|
function walk(dir) {
|
|
7992
8366
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
7993
8367
|
for (const entry of entries) {
|
|
7994
|
-
const full =
|
|
8368
|
+
const full = join18(dir, entry.name);
|
|
7995
8369
|
if (entry.isDirectory()) {
|
|
7996
8370
|
walk(full);
|
|
7997
8371
|
} else if (entry.isFile()) {
|
|
@@ -8012,7 +8386,7 @@ function collectPlanAssetShas(planDir, digests) {
|
|
|
8012
8386
|
const assets = [];
|
|
8013
8387
|
for (const file of files) {
|
|
8014
8388
|
if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
|
|
8015
|
-
compositionHtml =
|
|
8389
|
+
compositionHtml = readFileSync8(file.absolutePath);
|
|
8016
8390
|
continue;
|
|
8017
8391
|
}
|
|
8018
8392
|
if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
|
|
@@ -8032,23 +8406,23 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
|
8032
8406
|
function writePlanRemoteFiles(planDir, files) {
|
|
8033
8407
|
if (files.length === 0) return;
|
|
8034
8408
|
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
8035
|
-
|
|
8409
|
+
writeFileSync5(join18(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
8036
8410
|
}
|
|
8037
8411
|
function readPlanRemoteFiles(planDir) {
|
|
8038
|
-
const path =
|
|
8039
|
-
return existsSync13(path) ? JSON.parse(
|
|
8412
|
+
const path = join18(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
8413
|
+
return existsSync13(path) ? JSON.parse(readFileSync8(path, "utf-8")) : [];
|
|
8040
8414
|
}
|
|
8041
8415
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
8042
|
-
const planJsonPath =
|
|
8043
|
-
const encoderJsonPath =
|
|
8416
|
+
const planJsonPath = join18(planDir, "plan.json");
|
|
8417
|
+
const encoderJsonPath = join18(planDir, "meta", "encoder.json");
|
|
8044
8418
|
if (!existsSync13(planJsonPath)) {
|
|
8045
8419
|
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
8046
8420
|
}
|
|
8047
8421
|
if (!existsSync13(encoderJsonPath)) {
|
|
8048
8422
|
throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
|
|
8049
8423
|
}
|
|
8050
|
-
const planJson = JSON.parse(
|
|
8051
|
-
const encoderConfigCanonicalJson =
|
|
8424
|
+
const planJson = JSON.parse(readFileSync8(planJsonPath, "utf-8"));
|
|
8425
|
+
const encoderConfigCanonicalJson = readFileSync8(encoderJsonPath, "utf-8");
|
|
8052
8426
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
8053
8427
|
return computePlanHash({
|
|
8054
8428
|
compositionHtml,
|
|
@@ -8077,18 +8451,18 @@ async function freezePlan(input) {
|
|
|
8077
8451
|
if (!existsSync13(planDir)) {
|
|
8078
8452
|
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
8079
8453
|
}
|
|
8080
|
-
const metaDir =
|
|
8081
|
-
if (!existsSync13(metaDir))
|
|
8082
|
-
|
|
8083
|
-
|
|
8454
|
+
const metaDir = join18(planDir, "meta");
|
|
8455
|
+
if (!existsSync13(metaDir)) mkdirSync7(metaDir, { recursive: true });
|
|
8456
|
+
writeFileSync5(
|
|
8457
|
+
join18(metaDir, "composition.json"),
|
|
8084
8458
|
`${JSON.stringify(composition, null, 2)}
|
|
8085
8459
|
`,
|
|
8086
8460
|
"utf-8"
|
|
8087
8461
|
);
|
|
8088
8462
|
const encoderForCanonical = stripUndefined(encoder);
|
|
8089
8463
|
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
8090
|
-
|
|
8091
|
-
|
|
8464
|
+
writeFileSync5(join18(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
8465
|
+
writeFileSync5(join18(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
8092
8466
|
`, "utf-8");
|
|
8093
8467
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
8094
8468
|
const planHash = computePlanHash({
|
|
@@ -8112,8 +8486,8 @@ async function freezePlan(input) {
|
|
|
8112
8486
|
duration: durationSeconds,
|
|
8113
8487
|
hasAudio
|
|
8114
8488
|
};
|
|
8115
|
-
const planJsonPath =
|
|
8116
|
-
|
|
8489
|
+
const planJsonPath = join18(planDir, "plan.json");
|
|
8490
|
+
writeFileSync5(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
8117
8491
|
`, "utf-8");
|
|
8118
8492
|
return { planJsonPath, planHash };
|
|
8119
8493
|
}
|
|
@@ -8154,16 +8528,16 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
8154
8528
|
}
|
|
8155
8529
|
|
|
8156
8530
|
// src/services/distributed/chunkAudio.ts
|
|
8157
|
-
import { closeSync as
|
|
8158
|
-
import { writeFileSync as
|
|
8159
|
-
import { join as
|
|
8160
|
-
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as
|
|
8531
|
+
import { closeSync as closeSync4, existsSync as existsSync14, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
8532
|
+
import { writeFileSync as writeFileSync6, writeSync } from "node:fs";
|
|
8533
|
+
import { join as join20 } from "node:path";
|
|
8534
|
+
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
|
|
8161
8535
|
|
|
8162
8536
|
// src/services/distributed/mp4Stitch.ts
|
|
8163
|
-
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as
|
|
8537
|
+
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync6 } from "node:fs";
|
|
8164
8538
|
import { open } from "node:fs/promises";
|
|
8165
|
-
import { renameWithRetry as
|
|
8166
|
-
import { dirname as
|
|
8539
|
+
import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
|
|
8540
|
+
import { dirname as dirname10, join as join19 } from "node:path";
|
|
8167
8541
|
import { finished, pipeline } from "node:stream/promises";
|
|
8168
8542
|
function boxAt(src, off, at, limit) {
|
|
8169
8543
|
let size = src.readUInt32BE(off);
|
|
@@ -8176,16 +8550,16 @@ function boxAt(src, off, at, limit) {
|
|
|
8176
8550
|
return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
|
|
8177
8551
|
}
|
|
8178
8552
|
function childBoxes(buf, parent) {
|
|
8179
|
-
const
|
|
8180
|
-
for (let at = parent.bodyStart; at < parent.end; at =
|
|
8181
|
-
|
|
8553
|
+
const boxes2 = [];
|
|
8554
|
+
for (let at = parent.bodyStart; at < parent.end; at = boxes2.at(-1).end) {
|
|
8555
|
+
boxes2.push(boxAt(buf, at, at, parent.end));
|
|
8182
8556
|
}
|
|
8183
|
-
return
|
|
8557
|
+
return boxes2;
|
|
8184
8558
|
}
|
|
8185
8559
|
function findChild(buf, parent, type) {
|
|
8186
8560
|
return childBoxes(buf, parent).find((box2) => box2.type === type);
|
|
8187
8561
|
}
|
|
8188
|
-
function
|
|
8562
|
+
function child2(buf, parent, type) {
|
|
8189
8563
|
const found = findChild(buf, parent, type);
|
|
8190
8564
|
if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
|
|
8191
8565
|
return found;
|
|
@@ -8237,7 +8611,7 @@ function rows(buf, table, columns, countAt = 4) {
|
|
|
8237
8611
|
function readMediaTime(moov, trak) {
|
|
8238
8612
|
const edts = findChild(moov, trak, "edts");
|
|
8239
8613
|
if (!edts) return 0;
|
|
8240
|
-
const elst =
|
|
8614
|
+
const elst = child2(moov, edts, "elst");
|
|
8241
8615
|
const wide = moov[elst.bodyStart] === 1;
|
|
8242
8616
|
const entries = moov.readUInt32BE(elst.bodyStart + 4);
|
|
8243
8617
|
for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
|
|
@@ -8252,7 +8626,7 @@ function readSizes(moov, stsz) {
|
|
|
8252
8626
|
if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
|
|
8253
8627
|
return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
|
|
8254
8628
|
}
|
|
8255
|
-
function
|
|
8629
|
+
function readSync3(moov, stbl, sampleCount) {
|
|
8256
8630
|
const stss = findChild(moov, stbl, "stss");
|
|
8257
8631
|
if (!stss) return null;
|
|
8258
8632
|
const sync = Array(sampleCount).fill(false);
|
|
@@ -8262,10 +8636,10 @@ function readSync2(moov, stbl, sampleCount) {
|
|
|
8262
8636
|
function readChunkOffsets(moov, stbl) {
|
|
8263
8637
|
const stco = findChild(moov, stbl, "stco");
|
|
8264
8638
|
if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
|
|
8265
|
-
return rows(moov,
|
|
8639
|
+
return rows(moov, child2(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
|
|
8266
8640
|
}
|
|
8267
8641
|
function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
8268
|
-
const stsc = rows(moov,
|
|
8642
|
+
const stsc = rows(moov, child2(moov, stbl, "stsc"), 3);
|
|
8269
8643
|
return stsc.flatMap(([firstChunk, perChunk, description], i) => {
|
|
8270
8644
|
if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
|
|
8271
8645
|
const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
|
|
@@ -8273,28 +8647,28 @@ function readSamplesPerChunk(moov, stbl, chunkCount) {
|
|
|
8273
8647
|
});
|
|
8274
8648
|
}
|
|
8275
8649
|
function readTrack(moov, trak) {
|
|
8276
|
-
const mdia =
|
|
8277
|
-
const hdlr =
|
|
8278
|
-
const stbl =
|
|
8650
|
+
const mdia = child2(moov, trak, "mdia");
|
|
8651
|
+
const hdlr = child2(moov, mdia, "hdlr");
|
|
8652
|
+
const stbl = child2(moov, child2(moov, mdia, "minf"), "stbl");
|
|
8279
8653
|
if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
|
|
8280
|
-
const stts = rows(moov,
|
|
8654
|
+
const stts = rows(moov, child2(moov, stbl, "stts"), 2);
|
|
8281
8655
|
const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
|
|
8282
|
-
const sizes = readSizes(moov,
|
|
8656
|
+
const sizes = readSizes(moov, child2(moov, stbl, "stsz"));
|
|
8283
8657
|
const chunkOffsets = readChunkOffsets(moov, stbl);
|
|
8284
8658
|
const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
|
|
8285
8659
|
if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
|
|
8286
8660
|
throw new Error("[mp4Stitch] sample tables disagree on the sample count");
|
|
8287
8661
|
}
|
|
8288
|
-
const stsd =
|
|
8662
|
+
const stsd = child2(moov, stbl, "stsd");
|
|
8289
8663
|
return {
|
|
8290
8664
|
trak,
|
|
8291
8665
|
handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
|
|
8292
|
-
timescale: timescaleOf(moov,
|
|
8666
|
+
timescale: timescaleOf(moov, child2(moov, mdia, "mdhd")),
|
|
8293
8667
|
mediaTime: readMediaTime(moov, trak),
|
|
8294
8668
|
stsd: moov.subarray(stsd.start, stsd.end),
|
|
8295
8669
|
durations,
|
|
8296
8670
|
sizes,
|
|
8297
|
-
sync:
|
|
8671
|
+
sync: readSync3(moov, stbl, sizes.length),
|
|
8298
8672
|
samplesPerChunk,
|
|
8299
8673
|
chunkOffsets
|
|
8300
8674
|
};
|
|
@@ -8435,9 +8809,9 @@ function buildStitchedHeader(layouts, audio) {
|
|
|
8435
8809
|
}
|
|
8436
8810
|
const template = layouts[0].moov;
|
|
8437
8811
|
const root = boxAt(template, 0, 0, template.length);
|
|
8438
|
-
const mvhd =
|
|
8812
|
+
const mvhd = child2(template, root, "mvhd");
|
|
8439
8813
|
const movieTimescale = timescaleOf(template, mvhd);
|
|
8440
|
-
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template,
|
|
8814
|
+
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child2(template, track.trak, "tkhd")));
|
|
8441
8815
|
const audioTrackId = Math.max(...chunkTrackIds) + 1;
|
|
8442
8816
|
const buildMoov = (dataStart2) => {
|
|
8443
8817
|
const traks = plan2.map((track, t) => {
|
|
@@ -8527,18 +8901,18 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
8527
8901
|
const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
|
|
8528
8902
|
const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
|
|
8529
8903
|
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
8530
|
-
|
|
8904
|
+
mkdirSync8(dirname10(outputPath), { recursive: true });
|
|
8531
8905
|
const workDir = mkdtempSync2(`${outputPath}.stitch-`);
|
|
8532
|
-
const out = createWriteStream(
|
|
8906
|
+
const out = createWriteStream(join19(workDir, "out.mp4"));
|
|
8533
8907
|
try {
|
|
8534
8908
|
out.write(header);
|
|
8535
8909
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
8536
8910
|
out.end();
|
|
8537
8911
|
await finished(out);
|
|
8538
|
-
|
|
8912
|
+
renameWithRetry6(join19(workDir, "out.mp4"), outputPath);
|
|
8539
8913
|
} finally {
|
|
8540
8914
|
out.destroy();
|
|
8541
|
-
|
|
8915
|
+
rmSync6(workDir, { recursive: true, force: true });
|
|
8542
8916
|
}
|
|
8543
8917
|
}
|
|
8544
8918
|
|
|
@@ -8627,9 +9001,9 @@ async function writeChunkAudioSlices(input) {
|
|
|
8627
9001
|
offset += track.sizes[offsets.length - 1];
|
|
8628
9002
|
}
|
|
8629
9003
|
});
|
|
8630
|
-
const bytes =
|
|
9004
|
+
const bytes = readFileSync9(input.audioPath);
|
|
8631
9005
|
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
8632
|
-
|
|
9006
|
+
mkdirSync9(join20(input.planDir, SLICE_DIR), { recursive: true });
|
|
8633
9007
|
input.chunks.forEach((_, index) => {
|
|
8634
9008
|
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
8635
9009
|
if (!cut) return;
|
|
@@ -8637,18 +9011,18 @@ async function writeChunkAudioSlices(input) {
|
|
|
8637
9011
|
const start = offsets[cut.firstPacket];
|
|
8638
9012
|
layout.partStarts[index] = start;
|
|
8639
9013
|
const end = offsets[last] + track.sizes[last];
|
|
8640
|
-
|
|
9014
|
+
writeFileSync6(join20(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
8641
9015
|
});
|
|
8642
9016
|
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
8643
|
-
|
|
8644
|
-
|
|
9017
|
+
writeFileSync6(join20(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
9018
|
+
writeFileSync6(join20(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
8645
9019
|
}
|
|
8646
9020
|
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
8647
|
-
const layoutPath =
|
|
9021
|
+
const layoutPath = join20(planDir, SLICE_LAYOUT);
|
|
8648
9022
|
if (!existsSync14(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
8649
|
-
const layout = JSON.parse(
|
|
8650
|
-
const frame =
|
|
8651
|
-
const fd =
|
|
9023
|
+
const layout = JSON.parse(readFileSync9(layoutPath, "utf-8"));
|
|
9024
|
+
const frame = readFileSync9(join20(planDir, SLICE_FRAME));
|
|
9025
|
+
const fd = openSync4(scratchPath, "w");
|
|
8652
9026
|
try {
|
|
8653
9027
|
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
8654
9028
|
writeSync(
|
|
@@ -8660,12 +9034,12 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
|
8660
9034
|
);
|
|
8661
9035
|
const start = layout.partStarts[chunkIndex];
|
|
8662
9036
|
if (start !== void 0) {
|
|
8663
|
-
const part =
|
|
9037
|
+
const part = readFileSync9(join20(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
8664
9038
|
writeSync(fd, part, 0, part.length, start);
|
|
8665
9039
|
}
|
|
8666
9040
|
ftruncateSync(fd, layout.sizeBytes);
|
|
8667
9041
|
} finally {
|
|
8668
|
-
|
|
9042
|
+
closeSync4(fd);
|
|
8669
9043
|
}
|
|
8670
9044
|
return scratchPath;
|
|
8671
9045
|
}
|
|
@@ -8685,7 +9059,7 @@ async function muxChunkWithAudio(input) {
|
|
|
8685
9059
|
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
8686
9060
|
appendRenderProvenanceArgs(args, input.outputPath);
|
|
8687
9061
|
args.push("-y", input.outputPath);
|
|
8688
|
-
const result = await
|
|
9062
|
+
const result = await runFfmpeg7(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
8689
9063
|
if (!result.success) {
|
|
8690
9064
|
throw new Error(
|
|
8691
9065
|
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
|
|
@@ -8714,9 +9088,9 @@ async function muxChunkWithAudio(input) {
|
|
|
8714
9088
|
}
|
|
8715
9089
|
|
|
8716
9090
|
// src/services/distributed/planSize.ts
|
|
8717
|
-
import { createHash as
|
|
9091
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
8718
9092
|
import { lstatSync, readdirSync as readdirSync6 } from "node:fs";
|
|
8719
|
-
import { extname as extname3, join as
|
|
9093
|
+
import { extname as extname3, join as join21, relative as relative4 } from "node:path";
|
|
8720
9094
|
var MAX_TOP_COMPONENTS = 10;
|
|
8721
9095
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
8722
9096
|
".aac",
|
|
@@ -8744,7 +9118,7 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
8744
9118
|
"plan.json": "metadata"
|
|
8745
9119
|
};
|
|
8746
9120
|
function hashComponent(value) {
|
|
8747
|
-
return
|
|
9121
|
+
return createHash6("sha256").update(value).digest("hex").slice(0, 12);
|
|
8748
9122
|
}
|
|
8749
9123
|
function classifyFile(segments, rootKind) {
|
|
8750
9124
|
const rootName = segments[0] ?? "";
|
|
@@ -8828,7 +9202,7 @@ function walkRegularFiles(dir, visit) {
|
|
|
8828
9202
|
return;
|
|
8829
9203
|
}
|
|
8830
9204
|
for (const entry of entries) {
|
|
8831
|
-
const full =
|
|
9205
|
+
const full = join21(dir, entry.name);
|
|
8832
9206
|
if (entry.isDirectory()) {
|
|
8833
9207
|
walkRegularFiles(full, visit);
|
|
8834
9208
|
} else if (entry.isFile()) {
|
|
@@ -9044,13 +9418,13 @@ function assertPositiveInteger(name, value) {
|
|
|
9044
9418
|
}
|
|
9045
9419
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
9046
9420
|
"plan.json",
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9421
|
+
join22("meta", "composition.json"),
|
|
9422
|
+
join22("meta", "encoder.json"),
|
|
9423
|
+
join22("meta", "chunks.json")
|
|
9050
9424
|
];
|
|
9051
9425
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
9052
9426
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
9053
|
-
|
|
9427
|
+
rmSync7(join22(planDir, relativePath), { force: true });
|
|
9054
9428
|
}
|
|
9055
9429
|
}
|
|
9056
9430
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -9110,6 +9484,9 @@ function buildLockedRenderConfig(input) {
|
|
|
9110
9484
|
preset,
|
|
9111
9485
|
crf: config.crf,
|
|
9112
9486
|
bitrate: config.bitrate,
|
|
9487
|
+
maxBitrate: config.maxBitrate,
|
|
9488
|
+
bufferSize: config.bufferSize,
|
|
9489
|
+
keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
|
|
9113
9490
|
...locksVp9CpuUsed,
|
|
9114
9491
|
// GOP === chunkSize so every chunk's first frame is an IDR keyframe and
|
|
9115
9492
|
// ffmpeg concat-copy round-trips losslessly.
|
|
@@ -9195,23 +9572,23 @@ function createVideoSourceShipper(input) {
|
|
|
9195
9572
|
const known = shipped.get(srcPath);
|
|
9196
9573
|
if (known) return known;
|
|
9197
9574
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
9198
|
-
const current = stagedRel === null ? srcPath :
|
|
9575
|
+
const current = stagedRel === null ? srcPath : join22(input.planDir, "video-frames", stagedRel);
|
|
9199
9576
|
let result = null;
|
|
9200
9577
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
9201
9578
|
const rel = pathInside(root, srcPath);
|
|
9202
|
-
if (rel !== null && (existsSync15(
|
|
9579
|
+
if (rel !== null && (existsSync15(join22(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
9203
9580
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
9204
9581
|
break;
|
|
9205
9582
|
}
|
|
9206
9583
|
}
|
|
9207
9584
|
if (result === null) {
|
|
9208
9585
|
result = `video-sources/${shipped.size}${extname4(current)}`;
|
|
9209
|
-
const destination =
|
|
9210
|
-
|
|
9586
|
+
const destination = join22(input.planDir, ...result.split("/"));
|
|
9587
|
+
mkdirSync10(join22(input.planDir, "video-sources"), { recursive: true });
|
|
9211
9588
|
try {
|
|
9212
9589
|
linkSync(current, destination);
|
|
9213
9590
|
} catch {
|
|
9214
|
-
|
|
9591
|
+
copyFileSync6(current, destination);
|
|
9215
9592
|
}
|
|
9216
9593
|
}
|
|
9217
9594
|
shipped.set(srcPath, result);
|
|
@@ -9227,7 +9604,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
9227
9604
|
useGpu: false,
|
|
9228
9605
|
browserGpuMode: "software"
|
|
9229
9606
|
});
|
|
9230
|
-
if (!existsSync15(planDir))
|
|
9607
|
+
if (!existsSync15(planDir)) mkdirSync10(planDir, { recursive: true });
|
|
9231
9608
|
const log = config.logger ?? defaultLogger;
|
|
9232
9609
|
const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
|
|
9233
9610
|
const abortSignal = config.abortSignal;
|
|
@@ -9256,15 +9633,15 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
9256
9633
|
variables: config.variables
|
|
9257
9634
|
});
|
|
9258
9635
|
const entryFile = config.entryFile ?? "index.html";
|
|
9259
|
-
const htmlPath =
|
|
9636
|
+
const htmlPath = join22(projectDir, entryFile);
|
|
9260
9637
|
if (!existsSync15(htmlPath)) {
|
|
9261
9638
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
9262
9639
|
}
|
|
9263
|
-
const workDir =
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
const compiledDir =
|
|
9267
|
-
|
|
9640
|
+
const workDir = join22(planDir, ".plan-work");
|
|
9641
|
+
rmSync7(workDir, { recursive: true, force: true });
|
|
9642
|
+
mkdirSync10(workDir, { recursive: true });
|
|
9643
|
+
const compiledDir = join22(workDir, "compiled");
|
|
9644
|
+
mkdirSync10(compiledDir, { recursive: true });
|
|
9268
9645
|
let stageStarted = Date.now();
|
|
9269
9646
|
cpSync2(projectDir, compiledDir, {
|
|
9270
9647
|
recursive: true,
|
|
@@ -9277,10 +9654,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
9277
9654
|
}
|
|
9278
9655
|
});
|
|
9279
9656
|
logPlanStage(log, "copy", stageStarted);
|
|
9280
|
-
const finalCompiledDir =
|
|
9657
|
+
const finalCompiledDir = join22(planDir, "compiled");
|
|
9281
9658
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
9282
9659
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
9283
|
-
const remoteLocalPath = (rel) =>
|
|
9660
|
+
const remoteLocalPath = (rel) => join22(projectDir, ...rel.split("/"));
|
|
9284
9661
|
for (const [, file] of remoteFiles) {
|
|
9285
9662
|
declareContentAddressedUrl(file.url, file.sha256);
|
|
9286
9663
|
if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
|
|
@@ -9354,7 +9731,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
9354
9731
|
validateDistributedDuration({
|
|
9355
9732
|
duration: probeResult.duration,
|
|
9356
9733
|
totalFrames,
|
|
9357
|
-
fps:
|
|
9734
|
+
fps: fpsToNumber9(job.config.fps)
|
|
9358
9735
|
});
|
|
9359
9736
|
if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
|
|
9360
9737
|
if (probeResult.probeSession) {
|
|
@@ -9466,18 +9843,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9466
9843
|
let stageStarted = Date.now();
|
|
9467
9844
|
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
9468
9845
|
logPlanStage(log, "extract", stageStarted);
|
|
9469
|
-
const stagedVideoFrames =
|
|
9470
|
-
const videoFramesDst =
|
|
9471
|
-
if (existsSync15(videoFramesDst))
|
|
9472
|
-
const videoSourcesDir =
|
|
9473
|
-
|
|
9846
|
+
const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
|
|
9847
|
+
const videoFramesDst = join22(planDir, "video-frames");
|
|
9848
|
+
if (existsSync15(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
|
|
9849
|
+
const videoSourcesDir = join22(planDir, "video-sources");
|
|
9850
|
+
rmSync7(videoSourcesDir, { recursive: true, force: true });
|
|
9474
9851
|
if (existsSync15(stagedVideoFrames)) {
|
|
9475
|
-
|
|
9852
|
+
renameWithRetry7(stagedVideoFrames, videoFramesDst);
|
|
9476
9853
|
} else {
|
|
9477
|
-
|
|
9854
|
+
mkdirSync10(videoFramesDst, { recursive: true });
|
|
9478
9855
|
}
|
|
9479
|
-
if (existsSync15(finalCompiledDir))
|
|
9480
|
-
|
|
9856
|
+
if (existsSync15(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
|
|
9857
|
+
renameWithRetry7(compiledDir, finalCompiledDir);
|
|
9481
9858
|
const shipVideoSource = createVideoSourceShipper({
|
|
9482
9859
|
planDir,
|
|
9483
9860
|
projectDir,
|
|
@@ -9506,7 +9883,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9506
9883
|
};
|
|
9507
9884
|
})
|
|
9508
9885
|
});
|
|
9509
|
-
|
|
9886
|
+
mkdirSync10(join22(planDir, "meta"), { recursive: true });
|
|
9510
9887
|
writePlanRemoteFiles(
|
|
9511
9888
|
planDir,
|
|
9512
9889
|
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
@@ -9515,8 +9892,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9515
9892
|
sizeBytes
|
|
9516
9893
|
}))
|
|
9517
9894
|
);
|
|
9518
|
-
|
|
9519
|
-
|
|
9895
|
+
writeFileSync7(
|
|
9896
|
+
join22(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
9520
9897
|
JSON.stringify(planVideosJson, null, 2),
|
|
9521
9898
|
"utf-8"
|
|
9522
9899
|
);
|
|
@@ -9595,9 +9972,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9595
9972
|
if (audioResult?.audioError) {
|
|
9596
9973
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
9597
9974
|
}
|
|
9598
|
-
const planAudioPath =
|
|
9975
|
+
const planAudioPath = join22(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
9599
9976
|
if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
|
|
9600
|
-
|
|
9977
|
+
renameWithRetry7(audioResult.audioOutputPath, planAudioPath);
|
|
9601
9978
|
}
|
|
9602
9979
|
if (chunksCarryAudio && existsSync15(planAudioPath)) {
|
|
9603
9980
|
if (options.sliceChunkAudio) {
|
|
@@ -9644,7 +10021,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
9644
10021
|
format: config.format
|
|
9645
10022
|
};
|
|
9646
10023
|
try {
|
|
9647
|
-
|
|
10024
|
+
rmSync7(workDir, { recursive: true, force: true });
|
|
9648
10025
|
} catch (err) {
|
|
9649
10026
|
log.warn("[plan] failed to remove temp work dir", {
|
|
9650
10027
|
workDir,
|
|
@@ -9711,10 +10088,10 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
|
|
|
9711
10088
|
applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
|
|
9712
10089
|
}
|
|
9713
10090
|
if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
|
|
9714
|
-
|
|
10091
|
+
renameWithRetry7(mixed.audioOutputPath, outputPath);
|
|
9715
10092
|
return { hasAudio: true };
|
|
9716
10093
|
} finally {
|
|
9717
|
-
|
|
10094
|
+
rmSync7(c.workDir, { recursive: true, force: true });
|
|
9718
10095
|
}
|
|
9719
10096
|
}
|
|
9720
10097
|
async function plan(projectDir, config, planDir, options = {}) {
|
|
@@ -9736,35 +10113,35 @@ async function plan(projectDir, config, planDir, options = {}) {
|
|
|
9736
10113
|
|
|
9737
10114
|
// src/services/distributed/planV2.ts
|
|
9738
10115
|
import {
|
|
9739
|
-
copyFileSync as
|
|
10116
|
+
copyFileSync as copyFileSync8,
|
|
9740
10117
|
existsSync as existsSync17,
|
|
9741
10118
|
lstatSync as lstatSync2,
|
|
9742
|
-
mkdirSync as
|
|
10119
|
+
mkdirSync as mkdirSync12,
|
|
9743
10120
|
mkdtempSync as mkdtempSync4,
|
|
9744
|
-
readFileSync as
|
|
10121
|
+
readFileSync as readFileSync10,
|
|
9745
10122
|
readdirSync as readdirSync7,
|
|
9746
|
-
rmSync as
|
|
9747
|
-
statSync as
|
|
9748
|
-
writeFileSync as
|
|
10123
|
+
rmSync as rmSync9,
|
|
10124
|
+
statSync as statSync7,
|
|
10125
|
+
writeFileSync as writeFileSync9
|
|
9749
10126
|
} from "node:fs";
|
|
9750
10127
|
import { tmpdir } from "node:os";
|
|
9751
|
-
import { dirname as
|
|
10128
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, join as join25, relative as relative6, resolve as resolve9, sep as sep3 } from "node:path";
|
|
9752
10129
|
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
9753
|
-
import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as
|
|
10130
|
+
import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry9 } from "@hyperframes/engine";
|
|
9754
10131
|
|
|
9755
10132
|
// src/services/distributed/planV2Publisher.ts
|
|
9756
10133
|
import {
|
|
9757
|
-
copyFileSync as
|
|
10134
|
+
copyFileSync as copyFileSync7,
|
|
9758
10135
|
existsSync as existsSync16,
|
|
9759
10136
|
linkSync as linkSync2,
|
|
9760
|
-
mkdirSync as
|
|
10137
|
+
mkdirSync as mkdirSync11,
|
|
9761
10138
|
mkdtempSync as mkdtempSync3,
|
|
9762
|
-
rmSync as
|
|
9763
|
-
statSync as
|
|
9764
|
-
writeFileSync as
|
|
10139
|
+
rmSync as rmSync8,
|
|
10140
|
+
statSync as statSync6,
|
|
10141
|
+
writeFileSync as writeFileSync8
|
|
9765
10142
|
} from "node:fs";
|
|
9766
|
-
import { renameWithRetry as
|
|
9767
|
-
import { dirname as
|
|
10143
|
+
import { renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
|
|
10144
|
+
import { dirname as dirname11, join as join24 } from "node:path";
|
|
9768
10145
|
|
|
9769
10146
|
// src/services/distributed/planV2Errors.ts
|
|
9770
10147
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -9779,7 +10156,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
9779
10156
|
};
|
|
9780
10157
|
|
|
9781
10158
|
// src/services/distributed/planV2Layout.ts
|
|
9782
|
-
import { join as
|
|
10159
|
+
import { join as join23 } from "node:path";
|
|
9783
10160
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
9784
10161
|
function assertPlanV2Sha256(value, field) {
|
|
9785
10162
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -9789,7 +10166,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
9789
10166
|
}
|
|
9790
10167
|
function planV2BlobPath(root, sha256) {
|
|
9791
10168
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
9792
|
-
return
|
|
10169
|
+
return join23(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
9793
10170
|
}
|
|
9794
10171
|
|
|
9795
10172
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -9829,12 +10206,12 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9829
10206
|
}
|
|
9830
10207
|
this.destinationDir = destinationDir;
|
|
9831
10208
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
9832
|
-
|
|
9833
|
-
this.temporaryDir = mkdtempSync3(
|
|
10209
|
+
mkdirSync11(dirname11(destinationDir), { recursive: true });
|
|
10210
|
+
this.temporaryDir = mkdtempSync3(join24(dirname11(destinationDir), ".plan-v2-publish-"));
|
|
9834
10211
|
}
|
|
9835
10212
|
async putBlob(blob) {
|
|
9836
10213
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
9837
|
-
const sourceSize =
|
|
10214
|
+
const sourceSize = statSync6(blob.sourcePath).size;
|
|
9838
10215
|
if (sourceSize !== blob.sizeBytes) {
|
|
9839
10216
|
throw new PlanV2IntegrityError(
|
|
9840
10217
|
`published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
|
|
@@ -9842,19 +10219,19 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9842
10219
|
}
|
|
9843
10220
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
9844
10221
|
if (existsSync16(destinationPath)) return;
|
|
9845
|
-
|
|
9846
|
-
const stagingDir = mkdtempSync3(
|
|
9847
|
-
const temporaryPath =
|
|
10222
|
+
mkdirSync11(dirname11(destinationPath), { recursive: true });
|
|
10223
|
+
const stagingDir = mkdtempSync3(join24(dirname11(destinationPath), ".plan-v2-blob-"));
|
|
10224
|
+
const temporaryPath = join24(stagingDir, "blob");
|
|
9848
10225
|
try {
|
|
9849
10226
|
try {
|
|
9850
10227
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
9851
10228
|
} catch (error) {
|
|
9852
10229
|
if (!canFallbackToCopy(error)) throw error;
|
|
9853
|
-
|
|
10230
|
+
copyFileSync7(blob.sourcePath, temporaryPath);
|
|
9854
10231
|
}
|
|
9855
|
-
|
|
10232
|
+
renameWithRetry8(temporaryPath, destinationPath);
|
|
9856
10233
|
} finally {
|
|
9857
|
-
|
|
10234
|
+
rmSync8(stagingDir, { recursive: true, force: true });
|
|
9858
10235
|
}
|
|
9859
10236
|
}
|
|
9860
10237
|
async commitManifest(manifestBytes) {
|
|
@@ -9865,13 +10242,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9865
10242
|
);
|
|
9866
10243
|
}
|
|
9867
10244
|
}
|
|
9868
|
-
|
|
9869
|
-
|
|
10245
|
+
writeFileSync8(join24(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
10246
|
+
renameWithRetry8(this.temporaryDir, this.destinationDir);
|
|
9870
10247
|
this.#committed = true;
|
|
9871
10248
|
}
|
|
9872
10249
|
async abort() {
|
|
9873
10250
|
if (!this.#committed) {
|
|
9874
|
-
|
|
10251
|
+
rmSync8(this.temporaryDir, { recursive: true, force: true });
|
|
9875
10252
|
}
|
|
9876
10253
|
}
|
|
9877
10254
|
};
|
|
@@ -9890,7 +10267,7 @@ function isNonNegativeIntegerArray(value) {
|
|
|
9890
10267
|
return Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0);
|
|
9891
10268
|
}
|
|
9892
10269
|
function readJsonFile(path, label) {
|
|
9893
|
-
const contents =
|
|
10270
|
+
const contents = readFileSync10(path, "utf-8");
|
|
9894
10271
|
try {
|
|
9895
10272
|
const parsed = JSON.parse(contents);
|
|
9896
10273
|
return parsed;
|
|
@@ -9907,10 +10284,10 @@ function assertSafeRelativePath(path) {
|
|
|
9907
10284
|
}
|
|
9908
10285
|
function listFiles(root) {
|
|
9909
10286
|
const files = [];
|
|
9910
|
-
const rootResolved =
|
|
10287
|
+
const rootResolved = resolve9(root);
|
|
9911
10288
|
function walk(dir) {
|
|
9912
10289
|
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
9913
|
-
const absolutePath =
|
|
10290
|
+
const absolutePath = join25(dir, entry.name);
|
|
9914
10291
|
if (entry.isDirectory()) {
|
|
9915
10292
|
walk(absolutePath);
|
|
9916
10293
|
} else if (entry.isFile()) {
|
|
@@ -9937,13 +10314,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
9937
10314
|
}
|
|
9938
10315
|
}
|
|
9939
10316
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
9940
|
-
const videoRoot =
|
|
10317
|
+
const videoRoot = join25(executionPlanDir, "video-frames");
|
|
9941
10318
|
if (!existsSync17(videoRoot)) return;
|
|
9942
10319
|
for (const videoEntry of readdirSync7(videoRoot, { withFileTypes: true })) {
|
|
9943
10320
|
if (!videoEntry.isDirectory()) continue;
|
|
9944
|
-
const videoDir =
|
|
10321
|
+
const videoDir = join25(videoRoot, videoEntry.name);
|
|
9945
10322
|
if (readdirSync7(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
9946
|
-
const sentinelPath =
|
|
10323
|
+
const sentinelPath = join25(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
9947
10324
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
9948
10325
|
}
|
|
9949
10326
|
}
|
|
@@ -9953,8 +10330,8 @@ function isExtractionCacheCompleteSentinelPath(path) {
|
|
|
9953
10330
|
return segments.length === 3 && segments[0] === "video-frames" && segments[1] !== "" && segments[2] === EXTRACTION_CACHE_COMPLETE_SENTINEL;
|
|
9954
10331
|
}
|
|
9955
10332
|
function resolveExtractedVideoOutputDir(planDir, videoId) {
|
|
9956
|
-
const videoRoot =
|
|
9957
|
-
const outputDir =
|
|
10333
|
+
const videoRoot = resolve9(planDir, "video-frames");
|
|
10334
|
+
const outputDir = resolve9(videoRoot, videoId);
|
|
9958
10335
|
if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep3}`)) {
|
|
9959
10336
|
throw new PlanV2IntegrityError(`unsafe extracted video id: ${JSON.stringify(videoId)}`);
|
|
9960
10337
|
}
|
|
@@ -9990,7 +10367,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9990
10367
|
if (video.deferredRange) {
|
|
9991
10368
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
9992
10369
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
9993
|
-
framePaths2.set(index,
|
|
10370
|
+
framePaths2.set(index, join25(outputDir, String(index)));
|
|
9994
10371
|
}
|
|
9995
10372
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
9996
10373
|
}
|
|
@@ -9998,7 +10375,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9998
10375
|
const framePaths = /* @__PURE__ */ new Map();
|
|
9999
10376
|
for (const frameName of frameNames) {
|
|
10000
10377
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
10001
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
10378
|
+
assertValidExtractionCacheCompleteSentinel(join25(outputDir, frameName));
|
|
10002
10379
|
continue;
|
|
10003
10380
|
}
|
|
10004
10381
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -10017,7 +10394,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
10017
10394
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
10018
10395
|
);
|
|
10019
10396
|
}
|
|
10020
|
-
framePaths.set(frameIndex,
|
|
10397
|
+
framePaths.set(frameIndex, join25(outputDir, frameName));
|
|
10021
10398
|
}
|
|
10022
10399
|
return {
|
|
10023
10400
|
...video,
|
|
@@ -10038,11 +10415,11 @@ function parsePlanVideosJson2(value) {
|
|
|
10038
10415
|
}
|
|
10039
10416
|
}
|
|
10040
10417
|
function materializeExtractedVideoDirectories(planDir) {
|
|
10041
|
-
const videosPath =
|
|
10418
|
+
const videosPath = join25(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
10042
10419
|
if (!existsSync17(videosPath)) return;
|
|
10043
10420
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
10044
10421
|
for (const video of videos.extracted) {
|
|
10045
|
-
|
|
10422
|
+
mkdirSync12(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
|
|
10046
10423
|
}
|
|
10047
10424
|
}
|
|
10048
10425
|
function parseChunkSlices(value) {
|
|
@@ -10072,13 +10449,13 @@ function parseChunkSlices(value) {
|
|
|
10072
10449
|
});
|
|
10073
10450
|
}
|
|
10074
10451
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
10075
|
-
const videoRoot =
|
|
10452
|
+
const videoRoot = join25(executionPlanDir, "video-frames");
|
|
10076
10453
|
const hasExtractedFrames = existsSync17(videoRoot) && listFiles(videoRoot).length > 0;
|
|
10077
|
-
const videosPath =
|
|
10454
|
+
const videosPath = join25(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
10078
10455
|
if (!existsSync17(videosPath)) {
|
|
10079
10456
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
10080
10457
|
}
|
|
10081
|
-
const chunksPath =
|
|
10458
|
+
const chunksPath = join25(executionPlanDir, "meta", "chunks.json");
|
|
10082
10459
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
10083
10460
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
10084
10461
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -10115,7 +10492,7 @@ function collectChunkVideoDependencies(input) {
|
|
|
10115
10492
|
for (let frame = chunk.startFrame; frame < chunk.endFrame; frame++) {
|
|
10116
10493
|
const globalTime = frame * input.fpsDen / input.fpsNum;
|
|
10117
10494
|
for (const [videoId, payload] of input.table.getActiveFramePayloads(globalTime)) {
|
|
10118
|
-
const path = input.deferredSources.get(videoId) ?? relative6(
|
|
10495
|
+
const path = input.deferredSources.get(videoId) ?? relative6(resolve9(input.executionPlanDir), payload.framePath).split(sep3).join("/");
|
|
10119
10496
|
assertSafeRelativePath(path);
|
|
10120
10497
|
const indexes = mutable.get(path) ?? /* @__PURE__ */ new Set();
|
|
10121
10498
|
indexes.add(chunk.index);
|
|
@@ -10133,21 +10510,21 @@ function computeManifestHash(manifest) {
|
|
|
10133
10510
|
}
|
|
10134
10511
|
function writeBlob(sourcePath, destinationPath) {
|
|
10135
10512
|
if (existsSync17(destinationPath)) return;
|
|
10136
|
-
|
|
10137
|
-
const temporaryDir = mkdtempSync4(
|
|
10138
|
-
const temporaryPath =
|
|
10513
|
+
mkdirSync12(dirname12(destinationPath), { recursive: true });
|
|
10514
|
+
const temporaryDir = mkdtempSync4(join25(dirname12(destinationPath), ".plan-v2-blob-"));
|
|
10515
|
+
const temporaryPath = join25(temporaryDir, "blob");
|
|
10139
10516
|
try {
|
|
10140
|
-
|
|
10141
|
-
|
|
10517
|
+
copyFileSync8(sourcePath, temporaryPath);
|
|
10518
|
+
renameWithRetry9(temporaryPath, destinationPath);
|
|
10142
10519
|
} finally {
|
|
10143
|
-
|
|
10520
|
+
rmSync9(temporaryDir, { recursive: true, force: true });
|
|
10144
10521
|
}
|
|
10145
10522
|
}
|
|
10146
10523
|
function publishBlobFor(absolutePath, digests) {
|
|
10147
10524
|
return {
|
|
10148
10525
|
sourcePath: absolutePath,
|
|
10149
10526
|
sha256: planFileSha256(absolutePath, digests),
|
|
10150
|
-
sizeBytes:
|
|
10527
|
+
sizeBytes: statSync7(absolutePath).size
|
|
10151
10528
|
};
|
|
10152
10529
|
}
|
|
10153
10530
|
async function putBlobs(publisher, blobs) {
|
|
@@ -10171,7 +10548,7 @@ async function putTasks(tasks) {
|
|
|
10171
10548
|
if (failure) throw failure.reason;
|
|
10172
10549
|
}
|
|
10173
10550
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
10174
|
-
const executionPlanPath =
|
|
10551
|
+
const executionPlanPath = join25(executionPlanDir, "plan.json");
|
|
10175
10552
|
if (!existsSync17(executionPlanPath)) {
|
|
10176
10553
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
10177
10554
|
}
|
|
@@ -10199,7 +10576,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
10199
10576
|
}
|
|
10200
10577
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
10201
10578
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
10202
|
-
const encoder = readJsonFile(
|
|
10579
|
+
const encoder = readJsonFile(join25(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
10203
10580
|
const files = listFiles(executionPlanDir);
|
|
10204
10581
|
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
10205
10582
|
for (const file of files) {
|
|
@@ -10259,21 +10636,21 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
10259
10636
|
if (publication.storedBlobs.length > 0) {
|
|
10260
10637
|
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
10261
10638
|
}
|
|
10262
|
-
|
|
10263
|
-
const tempDir = mkdtempSync4(
|
|
10639
|
+
mkdirSync12(dirname12(planV2Dir), { recursive: true });
|
|
10640
|
+
const tempDir = mkdtempSync4(join25(dirname12(planV2Dir), ".plan-v2-build-"));
|
|
10264
10641
|
try {
|
|
10265
10642
|
for (const blob of publication.blobs) {
|
|
10266
10643
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
10267
10644
|
}
|
|
10268
|
-
|
|
10269
|
-
|
|
10645
|
+
writeFileSync9(
|
|
10646
|
+
join25(tempDir, "plan.json"),
|
|
10270
10647
|
canonicalJsonStringify(publication.manifest),
|
|
10271
10648
|
"utf-8"
|
|
10272
10649
|
);
|
|
10273
|
-
|
|
10650
|
+
renameWithRetry9(tempDir, planV2Dir);
|
|
10274
10651
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
10275
10652
|
} catch (error) {
|
|
10276
|
-
|
|
10653
|
+
rmSync9(tempDir, { recursive: true, force: true });
|
|
10277
10654
|
throw error;
|
|
10278
10655
|
}
|
|
10279
10656
|
}
|
|
@@ -10313,7 +10690,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
|
10313
10690
|
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
10314
10691
|
}
|
|
10315
10692
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
10316
|
-
const stagingRoot = mkdtempSync4(
|
|
10693
|
+
const stagingRoot = mkdtempSync4(join25(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
|
|
10317
10694
|
const digests = /* @__PURE__ */ new Map();
|
|
10318
10695
|
try {
|
|
10319
10696
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -10324,7 +10701,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
10324
10701
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
10325
10702
|
whileAudioMixes: () => putBlobs(
|
|
10326
10703
|
publisher,
|
|
10327
|
-
listFiles(
|
|
10704
|
+
listFiles(join25(stagingRoot, "compiled")).map(
|
|
10328
10705
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
10329
10706
|
)
|
|
10330
10707
|
),
|
|
@@ -10340,7 +10717,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
10340
10717
|
}
|
|
10341
10718
|
throw error;
|
|
10342
10719
|
} finally {
|
|
10343
|
-
|
|
10720
|
+
rmSync9(stagingRoot, { recursive: true, force: true });
|
|
10344
10721
|
}
|
|
10345
10722
|
}
|
|
10346
10723
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -10349,14 +10726,14 @@ async function planV2(projectDir, config, planV2Dir) {
|
|
|
10349
10726
|
}
|
|
10350
10727
|
const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
|
|
10351
10728
|
const manifest = await planV2WithPublisher(projectDir, config, publisher, {
|
|
10352
|
-
stagingParentDir:
|
|
10729
|
+
stagingParentDir: dirname12(planV2Dir)
|
|
10353
10730
|
});
|
|
10354
10731
|
return resultFromManifest(planV2Dir, manifest);
|
|
10355
10732
|
}
|
|
10356
10733
|
function resultFromManifest(planV2Dir, manifest) {
|
|
10357
10734
|
return {
|
|
10358
10735
|
planDir: planV2Dir,
|
|
10359
|
-
manifestPath:
|
|
10736
|
+
manifestPath: join25(planV2Dir, "plan.json"),
|
|
10360
10737
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
10361
10738
|
planHash: manifest.planHash,
|
|
10362
10739
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -10492,7 +10869,7 @@ function parsePlanV2Manifest(value) {
|
|
|
10492
10869
|
return parsed;
|
|
10493
10870
|
}
|
|
10494
10871
|
function readPlanV2Manifest(planV2Dir) {
|
|
10495
|
-
const manifestPath =
|
|
10872
|
+
const manifestPath = join25(planV2Dir, "plan.json");
|
|
10496
10873
|
if (!existsSync17(manifestPath)) {
|
|
10497
10874
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
10498
10875
|
}
|
|
@@ -10515,7 +10892,7 @@ function verifyBlob(planV2Dir, artifact) {
|
|
|
10515
10892
|
if (!existsSync17(sourcePath)) {
|
|
10516
10893
|
throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
|
|
10517
10894
|
}
|
|
10518
|
-
const stats =
|
|
10895
|
+
const stats = statSync7(sourcePath);
|
|
10519
10896
|
if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
|
|
10520
10897
|
throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
|
|
10521
10898
|
}
|
|
@@ -10537,17 +10914,17 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
10537
10914
|
artifact,
|
|
10538
10915
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
10539
10916
|
}));
|
|
10540
|
-
|
|
10541
|
-
const tempDir = mkdtempSync4(
|
|
10917
|
+
mkdirSync12(dirname12(destinationDir), { recursive: true });
|
|
10918
|
+
const tempDir = mkdtempSync4(join25(dirname12(destinationDir), ".plan-v2-materialize-"));
|
|
10542
10919
|
try {
|
|
10543
10920
|
for (const { artifact, sourcePath } of verified) {
|
|
10544
|
-
const destinationPath =
|
|
10545
|
-
|
|
10546
|
-
|
|
10921
|
+
const destinationPath = join25(tempDir, ...artifact.path.split("/"));
|
|
10922
|
+
mkdirSync12(dirname12(destinationPath), { recursive: true });
|
|
10923
|
+
copyFileSync8(sourcePath, destinationPath);
|
|
10547
10924
|
}
|
|
10548
10925
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
10549
|
-
|
|
10550
|
-
|
|
10926
|
+
writeFileSync9(
|
|
10927
|
+
join25(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
10551
10928
|
canonicalJsonStringify({
|
|
10552
10929
|
manifest,
|
|
10553
10930
|
target,
|
|
@@ -10555,9 +10932,9 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
10555
10932
|
}),
|
|
10556
10933
|
"utf-8"
|
|
10557
10934
|
);
|
|
10558
|
-
|
|
10935
|
+
renameWithRetry9(tempDir, destinationDir);
|
|
10559
10936
|
} catch (error) {
|
|
10560
|
-
|
|
10937
|
+
rmSync9(tempDir, { recursive: true, force: true });
|
|
10561
10938
|
throw error;
|
|
10562
10939
|
}
|
|
10563
10940
|
return {
|
|
@@ -10570,7 +10947,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
10570
10947
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
10571
10948
|
// before the container change carries the legacy name, and materializing it
|
|
10572
10949
|
// under the new one would point at a file that was never written.
|
|
10573
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
10950
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join25(destinationDir, audioArtifact.path) : null : null
|
|
10574
10951
|
};
|
|
10575
10952
|
}
|
|
10576
10953
|
function assertRemoteSources(remoteSources, artifacts) {
|
|
@@ -10589,7 +10966,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
10589
10966
|
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
10590
10967
|
}
|
|
10591
10968
|
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
10592
|
-
const markerPath =
|
|
10969
|
+
const markerPath = join25(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
10593
10970
|
if (!existsSync17(markerPath)) return null;
|
|
10594
10971
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
10595
10972
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
@@ -10607,8 +10984,8 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
10607
10984
|
assertRemoteSources(remoteSources, required);
|
|
10608
10985
|
for (const artifact of required) {
|
|
10609
10986
|
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
10610
|
-
const path =
|
|
10611
|
-
if (!existsSync17(path) ||
|
|
10987
|
+
const path = join25(planDir, ...artifact.path.split("/"));
|
|
10988
|
+
if (!existsSync17(path) || statSync7(path).size !== artifact.sizeBytes) {
|
|
10612
10989
|
throw new PlanV2IntegrityError(
|
|
10613
10990
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
10614
10991
|
);
|
|
@@ -10621,33 +10998,34 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
10621
10998
|
}
|
|
10622
10999
|
|
|
10623
11000
|
// src/services/distributed/planV2Execution.ts
|
|
10624
|
-
import { mkdtempSync as mkdtempSync6, rmSync as
|
|
11001
|
+
import { mkdtempSync as mkdtempSync6, rmSync as rmSync12 } from "node:fs";
|
|
10625
11002
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
10626
|
-
import { join as
|
|
11003
|
+
import { join as join28 } from "node:path";
|
|
10627
11004
|
|
|
10628
11005
|
// src/services/distributed/assemble.ts
|
|
10629
11006
|
import {
|
|
10630
11007
|
cpSync as cpSync3,
|
|
10631
11008
|
existsSync as existsSync18,
|
|
10632
|
-
mkdirSync as
|
|
11009
|
+
mkdirSync as mkdirSync13,
|
|
10633
11010
|
mkdtempSync as mkdtempSync5,
|
|
10634
|
-
readFileSync as
|
|
11011
|
+
readFileSync as readFileSync11,
|
|
10635
11012
|
readdirSync as readdirSync8,
|
|
10636
|
-
rmSync as
|
|
10637
|
-
statSync as
|
|
10638
|
-
writeFileSync as
|
|
11013
|
+
rmSync as rmSync10,
|
|
11014
|
+
statSync as statSync8,
|
|
11015
|
+
writeFileSync as writeFileSync10
|
|
10639
11016
|
} from "node:fs";
|
|
10640
|
-
import { dirname as
|
|
11017
|
+
import { dirname as dirname13, join as join26 } from "node:path";
|
|
10641
11018
|
import {
|
|
11019
|
+
appendRateCapArgs as appendRateCapArgs2,
|
|
10642
11020
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
10643
11021
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
10644
11022
|
getEncoderPreset as getEncoderPreset5,
|
|
10645
11023
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
10646
11024
|
muxVideoWithAudio as muxVideoWithAudio2,
|
|
10647
11025
|
resolveConfig as resolveConfig4,
|
|
10648
|
-
runFfmpeg as
|
|
11026
|
+
runFfmpeg as runFfmpeg8,
|
|
10649
11027
|
appendSoftwareEncoderThreadsArg,
|
|
10650
|
-
renameWithRetry as
|
|
11028
|
+
renameWithRetry as renameWithRetry10
|
|
10651
11029
|
} from "@hyperframes/engine";
|
|
10652
11030
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
10653
11031
|
function cfrRateControlArgs(locked) {
|
|
@@ -10673,13 +11051,13 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10673
11051
|
outputPath,
|
|
10674
11052
|
durationMs: Date.now() - start,
|
|
10675
11053
|
framesEncoded: plan2.totalFrames,
|
|
10676
|
-
fileSize:
|
|
11054
|
+
fileSize: statSync8(outputPath).size
|
|
10677
11055
|
};
|
|
10678
11056
|
}
|
|
10679
11057
|
const format = plan2.dimensions.format;
|
|
10680
11058
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
10681
|
-
if (!existsSync18(
|
|
10682
|
-
|
|
11059
|
+
if (!existsSync18(dirname13(outputPath))) {
|
|
11060
|
+
mkdirSync13(dirname13(outputPath), { recursive: true });
|
|
10683
11061
|
}
|
|
10684
11062
|
const workDir = mkdtempSync5(`${outputPath}.assemble-work-`);
|
|
10685
11063
|
const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
@@ -10701,10 +11079,10 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10701
11079
|
};
|
|
10702
11080
|
try {
|
|
10703
11081
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
10704
|
-
const finalPath =
|
|
11082
|
+
const finalPath = join26(workDir, `output.${format}`);
|
|
10705
11083
|
const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
|
|
10706
11084
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
10707
|
-
|
|
11085
|
+
renameWithRetry10(finalPath, outputPath);
|
|
10708
11086
|
log.info("[assemble] stage timings", {
|
|
10709
11087
|
...stageMs,
|
|
10710
11088
|
chunks: chunkPaths.length,
|
|
@@ -10712,7 +11090,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10712
11090
|
});
|
|
10713
11091
|
} finally {
|
|
10714
11092
|
try {
|
|
10715
|
-
|
|
11093
|
+
rmSync10(workDir, { recursive: true, force: true });
|
|
10716
11094
|
} catch (err) {
|
|
10717
11095
|
log.warn("[assemble] failed to remove work dir", {
|
|
10718
11096
|
workDir,
|
|
@@ -10720,7 +11098,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10720
11098
|
});
|
|
10721
11099
|
}
|
|
10722
11100
|
}
|
|
10723
|
-
const fileSize = existsSync18(outputPath) ?
|
|
11101
|
+
const fileSize = existsSync18(outputPath) ? statSync8(outputPath).size : 0;
|
|
10724
11102
|
return {
|
|
10725
11103
|
outputPath,
|
|
10726
11104
|
durationMs: Date.now() - start,
|
|
@@ -10729,18 +11107,18 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10729
11107
|
};
|
|
10730
11108
|
}
|
|
10731
11109
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
10732
|
-
const planJsonPath =
|
|
10733
|
-
const chunksJsonPath =
|
|
11110
|
+
const planJsonPath = join26(planDir, "plan.json");
|
|
11111
|
+
const chunksJsonPath = join26(planDir, "meta", "chunks.json");
|
|
10734
11112
|
if (!existsSync18(planJsonPath)) {
|
|
10735
11113
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
10736
11114
|
}
|
|
10737
|
-
const plan2 = JSON.parse(
|
|
11115
|
+
const plan2 = JSON.parse(readFileSync11(planJsonPath, "utf-8"));
|
|
10738
11116
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
|
|
10739
11117
|
validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
|
|
10740
11118
|
if (!existsSync18(chunksJsonPath)) {
|
|
10741
11119
|
throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
|
|
10742
11120
|
}
|
|
10743
|
-
const chunks = JSON.parse(
|
|
11121
|
+
const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
|
|
10744
11122
|
if (chunkPaths.length !== chunks.length) {
|
|
10745
11123
|
throw new Error(
|
|
10746
11124
|
`[assemble] chunkPaths length (${chunkPaths.length}) does not match chunks.json length (${chunks.length}). Adapters must pass one path per chunk, ordered by index.`
|
|
@@ -10774,11 +11152,14 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
10774
11152
|
"-r",
|
|
10775
11153
|
fpsArg
|
|
10776
11154
|
];
|
|
11155
|
+
if (encoder.keyframeIntervalFrames !== void 0)
|
|
11156
|
+
args.push("-g", String(encoder.keyframeIntervalFrames));
|
|
11157
|
+
appendRateCapArgs2(args, encoder);
|
|
10777
11158
|
appendSoftwareEncoderThreadsArg(args, "libx264");
|
|
10778
11159
|
return args;
|
|
10779
11160
|
}
|
|
10780
11161
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
10781
|
-
const cfrOutputPath =
|
|
11162
|
+
const cfrOutputPath = join26(ctx.workDir, `cfr.${ctx.format}`);
|
|
10782
11163
|
const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
|
|
10783
11164
|
appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
|
|
10784
11165
|
cfrArgs.push("-y", cfrOutputPath);
|
|
@@ -10805,7 +11186,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
10805
11186
|
);
|
|
10806
11187
|
return;
|
|
10807
11188
|
}
|
|
10808
|
-
const paddedAudioPath =
|
|
11189
|
+
const paddedAudioPath = join26(ctx.workDir, "audio-padded.m4a");
|
|
10809
11190
|
const padTrimResult = await ctx.timed(
|
|
10810
11191
|
"audioMs",
|
|
10811
11192
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -10845,9 +11226,9 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
10845
11226
|
}
|
|
10846
11227
|
}
|
|
10847
11228
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
10848
|
-
const listPath =
|
|
11229
|
+
const listPath = join26(workDir, "concat-list.txt");
|
|
10849
11230
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
10850
|
-
|
|
11231
|
+
writeFileSync10(listPath, `${body}
|
|
10851
11232
|
`, "utf-8");
|
|
10852
11233
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
10853
11234
|
}
|
|
@@ -10862,7 +11243,7 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
10862
11243
|
};
|
|
10863
11244
|
}
|
|
10864
11245
|
async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
10865
|
-
const result = await
|
|
11246
|
+
const result = await runFfmpeg8(args, { signal, timeout });
|
|
10866
11247
|
if (!result.success) {
|
|
10867
11248
|
throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
|
|
10868
11249
|
error: describeFfmpegFailure4(result, timeout),
|
|
@@ -10883,13 +11264,13 @@ function stitchesSeparateAudio(planDir) {
|
|
|
10883
11264
|
return lockedAudio(planDir).separateAudio === true;
|
|
10884
11265
|
}
|
|
10885
11266
|
function planFormat(planDir) {
|
|
10886
|
-
const plan2 = JSON.parse(
|
|
11267
|
+
const plan2 = JSON.parse(readFileSync11(join26(planDir, "plan.json"), "utf-8"));
|
|
10887
11268
|
return plan2.dimensions.format;
|
|
10888
11269
|
}
|
|
10889
11270
|
function lockedAudio(planDir) {
|
|
10890
|
-
const encoderJsonPath =
|
|
11271
|
+
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
10891
11272
|
if (!existsSync18(encoderJsonPath)) return {};
|
|
10892
|
-
return JSON.parse(
|
|
11273
|
+
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
10893
11274
|
}
|
|
10894
11275
|
function readCfrEncoder(planDir, format) {
|
|
10895
11276
|
if (format !== "mp4") {
|
|
@@ -10897,11 +11278,11 @@ function readCfrEncoder(planDir, format) {
|
|
|
10897
11278
|
`[assemble] cfr=true is only supported for format="mp4" (got "${format}"). Stream-copy paths for webm and mov already produce exact avg_frame_rate; cfr re-encode is not needed.`
|
|
10898
11279
|
);
|
|
10899
11280
|
}
|
|
10900
|
-
const encoderJsonPath =
|
|
11281
|
+
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
10901
11282
|
if (!existsSync18(encoderJsonPath)) {
|
|
10902
11283
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
10903
11284
|
}
|
|
10904
|
-
const encoderJson = JSON.parse(
|
|
11285
|
+
const encoderJson = JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
10905
11286
|
if (encoderJson.encoder === "libx265-software") {
|
|
10906
11287
|
throw new Error(
|
|
10907
11288
|
`[assemble] cfr=true is not yet supported with codec: "h265". The cfr re-encode pass uses libx264 and would silently transcode the h265 chunks. Either disable cfr or render with codec: "h264".`
|
|
@@ -10910,11 +11291,11 @@ function readCfrEncoder(planDir, format) {
|
|
|
10910
11291
|
return encoderJson;
|
|
10911
11292
|
}
|
|
10912
11293
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
10913
|
-
if (existsSync18(outputPath))
|
|
10914
|
-
|
|
11294
|
+
if (existsSync18(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
|
|
11295
|
+
mkdirSync13(outputPath, { recursive: true });
|
|
10915
11296
|
let globalIdx = 0;
|
|
10916
11297
|
for (const chunkDir of chunkPaths) {
|
|
10917
|
-
if (!
|
|
11298
|
+
if (!statSync8(chunkDir).isDirectory()) {
|
|
10918
11299
|
throw new Error(
|
|
10919
11300
|
`[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
|
|
10920
11301
|
);
|
|
@@ -10924,8 +11305,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10924
11305
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
10925
11306
|
}
|
|
10926
11307
|
for (const frame of frames) {
|
|
10927
|
-
const dst =
|
|
10928
|
-
cpSync3(
|
|
11308
|
+
const dst = join26(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
11309
|
+
cpSync3(join26(chunkDir, frame), dst);
|
|
10929
11310
|
globalIdx += 1;
|
|
10930
11311
|
}
|
|
10931
11312
|
}
|
|
@@ -10935,13 +11316,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10935
11316
|
);
|
|
10936
11317
|
}
|
|
10937
11318
|
if (audioPath !== null && existsSync18(audioPath)) {
|
|
10938
|
-
const sidecar =
|
|
11319
|
+
const sidecar = join26(outputPath, MIXED_AUDIO_FILENAME4);
|
|
10939
11320
|
cpSync3(audioPath, sidecar);
|
|
10940
11321
|
}
|
|
10941
11322
|
let fileSize = 0;
|
|
10942
11323
|
for (const name of readdirSync8(outputPath)) {
|
|
10943
11324
|
try {
|
|
10944
|
-
fileSize +=
|
|
11325
|
+
fileSize += statSync8(join26(outputPath, name)).size;
|
|
10945
11326
|
} catch {
|
|
10946
11327
|
}
|
|
10947
11328
|
}
|
|
@@ -10955,9 +11336,9 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10955
11336
|
|
|
10956
11337
|
// src/services/distributed/renderChunk.ts
|
|
10957
11338
|
import { randomBytes } from "node:crypto";
|
|
10958
|
-
import { existsSync as existsSync19, mkdirSync as
|
|
11339
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
|
|
10959
11340
|
import { cpus } from "node:os";
|
|
10960
|
-
import { join as
|
|
11341
|
+
import { join as join27 } from "node:path";
|
|
10961
11342
|
import {
|
|
10962
11343
|
assertSwiftShader,
|
|
10963
11344
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -11076,10 +11457,10 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
|
|
|
11076
11457
|
return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
|
|
11077
11458
|
}
|
|
11078
11459
|
function hashChunkOutput(outputPath, kind) {
|
|
11079
|
-
if (kind === "file") return sha256Hex(
|
|
11460
|
+
if (kind === "file") return sha256Hex(readFileSync12(outputPath));
|
|
11080
11461
|
const entries = readdirSync9(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
11081
11462
|
const lines = entries.map(
|
|
11082
|
-
(name) => `${name}\0${sha256Hex(
|
|
11463
|
+
(name) => `${name}\0${sha256Hex(readFileSync12(join27(outputPath, name)))}`
|
|
11083
11464
|
);
|
|
11084
11465
|
return sha256Hex(lines.join("\0"));
|
|
11085
11466
|
}
|
|
@@ -11096,16 +11477,16 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
11096
11477
|
async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
11097
11478
|
const start = Date.now();
|
|
11098
11479
|
const log = defaultLogger;
|
|
11099
|
-
const planJsonPath =
|
|
11100
|
-
const encoderJsonPath =
|
|
11101
|
-
const chunksJsonPath =
|
|
11480
|
+
const planJsonPath = join27(planDir, "plan.json");
|
|
11481
|
+
const encoderJsonPath = join27(planDir, "meta", "encoder.json");
|
|
11482
|
+
const chunksJsonPath = join27(planDir, "meta", "chunks.json");
|
|
11102
11483
|
if (!existsSync19(planJsonPath)) {
|
|
11103
11484
|
throw new RenderChunkValidationError(
|
|
11104
11485
|
MISSING_PLAN_ARTIFACT,
|
|
11105
11486
|
`[renderChunk] planDir is missing required artifact: ${planJsonPath}`
|
|
11106
11487
|
);
|
|
11107
11488
|
}
|
|
11108
|
-
const plan2 = JSON.parse(
|
|
11489
|
+
const plan2 = JSON.parse(readFileSync12(planJsonPath, "utf-8"));
|
|
11109
11490
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk);
|
|
11110
11491
|
const planHashStarted = Date.now();
|
|
11111
11492
|
const v2Materialization = readValidatedPlanV2Materialization(planDir, {
|
|
@@ -11122,13 +11503,13 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11122
11503
|
);
|
|
11123
11504
|
}
|
|
11124
11505
|
}
|
|
11125
|
-
const encoder = JSON.parse(
|
|
11126
|
-
const chunks = JSON.parse(
|
|
11127
|
-
const videosJsonPath =
|
|
11506
|
+
const encoder = JSON.parse(readFileSync12(encoderJsonPath, "utf-8"));
|
|
11507
|
+
const chunks = JSON.parse(readFileSync12(chunksJsonPath, "utf-8"));
|
|
11508
|
+
const videosJsonPath = join27(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
11128
11509
|
let planVideos = null;
|
|
11129
11510
|
if (existsSync19(videosJsonPath)) {
|
|
11130
11511
|
try {
|
|
11131
|
-
planVideos = validatePlanVideosForChunk(JSON.parse(
|
|
11512
|
+
planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync12(videosJsonPath, "utf-8")));
|
|
11132
11513
|
} catch (err) {
|
|
11133
11514
|
if (err instanceof RenderChunkValidationError) throw err;
|
|
11134
11515
|
throw new RenderChunkValidationError(
|
|
@@ -11156,7 +11537,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11156
11537
|
}
|
|
11157
11538
|
const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
|
|
11158
11539
|
const framesInChunk = range.endFrame - range.startFrame;
|
|
11159
|
-
const compiledDir =
|
|
11540
|
+
const compiledDir = join27(planDir, "compiled");
|
|
11160
11541
|
if (!existsSync19(compiledDir)) {
|
|
11161
11542
|
throw new RenderChunkValidationError(
|
|
11162
11543
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -11213,9 +11594,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11213
11594
|
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
|
|
11214
11595
|
};
|
|
11215
11596
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
11216
|
-
|
|
11217
|
-
const framesDir =
|
|
11218
|
-
|
|
11597
|
+
mkdirSync14(workDir, { recursive: true });
|
|
11598
|
+
const framesDir = join27(workDir, "captured-frames");
|
|
11599
|
+
mkdirSync14(framesDir, { recursive: true });
|
|
11219
11600
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
11220
11601
|
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
11221
11602
|
const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
@@ -11239,7 +11620,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11239
11620
|
dimensions: plan2.dimensions,
|
|
11240
11621
|
encoder,
|
|
11241
11622
|
videoCount: planVideos?.videos.length ?? 0,
|
|
11242
|
-
html:
|
|
11623
|
+
html: readFileSync12(join27(compiledDir, "index.html"), "utf-8"),
|
|
11243
11624
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
11244
11625
|
skipReadinessVideoIds: deferredVideos.filter(
|
|
11245
11626
|
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
@@ -11368,10 +11749,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11368
11749
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
11369
11750
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
11370
11751
|
if (isPngSequence) {
|
|
11371
|
-
if (!existsSync19(outputChunkPath))
|
|
11752
|
+
if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
|
|
11372
11753
|
} else {
|
|
11373
|
-
const outDir =
|
|
11374
|
-
if (!existsSync19(outDir))
|
|
11754
|
+
const outDir = join27(outputChunkPath, "..");
|
|
11755
|
+
if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
|
|
11375
11756
|
}
|
|
11376
11757
|
const encodeInput = {
|
|
11377
11758
|
job,
|
|
@@ -11410,7 +11791,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11410
11791
|
// first frame is an IDR keyframe and concat-copy at assemble time
|
|
11411
11792
|
// round-trips losslessly.
|
|
11412
11793
|
lockGopForChunkConcat: !isPngSequence,
|
|
11413
|
-
gopSize: framesInChunk
|
|
11794
|
+
gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
|
|
11795
|
+
maxBitrate: encoder.maxBitrate,
|
|
11796
|
+
bufferSize: encoder.bufferSize
|
|
11414
11797
|
};
|
|
11415
11798
|
const captureStarted = Date.now();
|
|
11416
11799
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
@@ -11482,8 +11865,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11482
11865
|
return observedChunkCaptureMode(capturePerfs);
|
|
11483
11866
|
},
|
|
11484
11867
|
resetForScreenshotRetry: () => {
|
|
11485
|
-
|
|
11486
|
-
|
|
11868
|
+
rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
11869
|
+
mkdirSync14(framesDir, { recursive: true });
|
|
11487
11870
|
capturePerfs.length = 0;
|
|
11488
11871
|
captureAttempts.length = 0;
|
|
11489
11872
|
job.framesRendered = 0;
|
|
@@ -11525,8 +11908,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11525
11908
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
11526
11909
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
11527
11910
|
});
|
|
11528
|
-
|
|
11529
|
-
|
|
11911
|
+
rmSync11(videoOnlyPath, { force: true });
|
|
11912
|
+
rmSync11(audioScratchPath, { force: true });
|
|
11530
11913
|
}
|
|
11531
11914
|
encodeStageMs = Date.now() - encodeStarted;
|
|
11532
11915
|
} finally {
|
|
@@ -11573,11 +11956,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11573
11956
|
producerVersion: plan2.producerVersion,
|
|
11574
11957
|
ffmpegVersion
|
|
11575
11958
|
};
|
|
11576
|
-
|
|
11959
|
+
writeFileSync11(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
11577
11960
|
`, "utf-8");
|
|
11578
11961
|
try {
|
|
11579
|
-
|
|
11580
|
-
|
|
11962
|
+
rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
11963
|
+
rmSync11(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
11581
11964
|
} catch (err) {
|
|
11582
11965
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
11583
11966
|
workDir,
|
|
@@ -11606,18 +11989,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
11606
11989
|
|
|
11607
11990
|
// src/services/distributed/planV2Execution.ts
|
|
11608
11991
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
11609
|
-
const workRoot = mkdtempSync6(
|
|
11610
|
-
const materializedPlanDir =
|
|
11992
|
+
const workRoot = mkdtempSync6(join28(tmpdir2(), "hf-plan-v2-chunk-"));
|
|
11993
|
+
const materializedPlanDir = join28(workRoot, "plan");
|
|
11611
11994
|
try {
|
|
11612
11995
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
11613
11996
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
11614
11997
|
} finally {
|
|
11615
|
-
|
|
11998
|
+
rmSync12(workRoot, { recursive: true, force: true });
|
|
11616
11999
|
}
|
|
11617
12000
|
}
|
|
11618
12001
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
11619
|
-
const workRoot = mkdtempSync6(
|
|
11620
|
-
const materializedPlanDir =
|
|
12002
|
+
const workRoot = mkdtempSync6(join28(tmpdir2(), "hf-plan-v2-assembler-"));
|
|
12003
|
+
const materializedPlanDir = join28(workRoot, "plan");
|
|
11621
12004
|
try {
|
|
11622
12005
|
const materialized = materializePlanV2Target(
|
|
11623
12006
|
planV2Dir,
|
|
@@ -11632,7 +12015,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
11632
12015
|
options
|
|
11633
12016
|
);
|
|
11634
12017
|
} finally {
|
|
11635
|
-
|
|
12018
|
+
rmSync12(workRoot, { recursive: true, force: true });
|
|
11636
12019
|
}
|
|
11637
12020
|
}
|
|
11638
12021
|
|
|
@@ -11725,6 +12108,20 @@ function validateDistributedRenderConfig(config) {
|
|
|
11725
12108
|
`must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
|
|
11726
12109
|
);
|
|
11727
12110
|
}
|
|
12111
|
+
try {
|
|
12112
|
+
validateDeliveryEncoderConfig({
|
|
12113
|
+
format: config.format,
|
|
12114
|
+
fps: { num: config.fps, den: 1 },
|
|
12115
|
+
keyframeIntervalSeconds: config.keyframeIntervalSeconds,
|
|
12116
|
+
maxBitrate: config.maxBitrate,
|
|
12117
|
+
bufferSize: config.bufferSize
|
|
12118
|
+
});
|
|
12119
|
+
} catch (error) {
|
|
12120
|
+
throw new InvalidConfigError(
|
|
12121
|
+
"config.maxBitrate",
|
|
12122
|
+
error instanceof Error ? error.message : String(error)
|
|
12123
|
+
);
|
|
12124
|
+
}
|
|
11728
12125
|
if (config.chunkSize !== void 0) {
|
|
11729
12126
|
if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
|
|
11730
12127
|
throw new InvalidConfigError(
|
|
@@ -11881,9 +12278,9 @@ function validateIntDimension(field, value) {
|
|
|
11881
12278
|
}
|
|
11882
12279
|
|
|
11883
12280
|
// src/services/distributed/projectHash.ts
|
|
11884
|
-
import { closeSync as
|
|
11885
|
-
import { createHash as
|
|
11886
|
-
import { join as
|
|
12281
|
+
import { closeSync as closeSync5, openSync as openSync5, readdirSync as readdirSync10, readSync as readSync4, realpathSync as realpathSync2, statSync as statSync9 } from "node:fs";
|
|
12282
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
12283
|
+
import { join as join29 } from "node:path";
|
|
11887
12284
|
var SITE_ID_SCHEME = "site-v2\0";
|
|
11888
12285
|
var READ_CHUNK_BYTES = 256 * 1024;
|
|
11889
12286
|
function listSiteFiles(projectDir) {
|
|
@@ -11892,11 +12289,11 @@ function listSiteFiles(projectDir) {
|
|
|
11892
12289
|
function walk(dir, rel) {
|
|
11893
12290
|
for (const name of readdirSync10(dir).sort()) {
|
|
11894
12291
|
if (rel === "" && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(name)) continue;
|
|
11895
|
-
const full =
|
|
12292
|
+
const full = join29(dir, name);
|
|
11896
12293
|
const relPath = rel === "" ? name : `${rel}/${name}`;
|
|
11897
12294
|
let stat;
|
|
11898
12295
|
try {
|
|
11899
|
-
stat =
|
|
12296
|
+
stat = statSync9(full);
|
|
11900
12297
|
} catch (err) {
|
|
11901
12298
|
throw new Error(`[listSiteFiles] cannot read ${relPath} (dangling symlink?)`, {
|
|
11902
12299
|
cause: err
|
|
@@ -11917,26 +12314,26 @@ function listSiteFiles(projectDir) {
|
|
|
11917
12314
|
return files;
|
|
11918
12315
|
}
|
|
11919
12316
|
function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
|
|
11920
|
-
const hash =
|
|
12317
|
+
const hash = createHash7("sha256").update(SITE_ID_SCHEME);
|
|
11921
12318
|
const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
11922
12319
|
for (const rel of files) {
|
|
11923
12320
|
hash.update(rel);
|
|
11924
12321
|
hash.update("\0");
|
|
11925
|
-
const fd =
|
|
12322
|
+
const fd = openSync5(join29(projectDir, rel), "r");
|
|
11926
12323
|
try {
|
|
11927
12324
|
let read;
|
|
11928
|
-
while ((read =
|
|
12325
|
+
while ((read = readSync4(fd, chunk, 0, chunk.length, null)) > 0) {
|
|
11929
12326
|
hash.update(chunk.subarray(0, read));
|
|
11930
12327
|
}
|
|
11931
12328
|
} finally {
|
|
11932
|
-
|
|
12329
|
+
closeSync5(fd);
|
|
11933
12330
|
}
|
|
11934
12331
|
}
|
|
11935
12332
|
return hash.digest("hex").slice(0, 16);
|
|
11936
12333
|
}
|
|
11937
12334
|
|
|
11938
12335
|
// src/services/distributed/audioSidecar.ts
|
|
11939
|
-
import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as
|
|
12336
|
+
import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg9 } from "@hyperframes/engine";
|
|
11940
12337
|
import { extname as extname5 } from "node:path";
|
|
11941
12338
|
var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
|
|
11942
12339
|
var AUDIO_SIDECAR_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v"]);
|
|
@@ -11947,7 +12344,7 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
|
|
|
11947
12344
|
const audio = await extractAudioMetadata4(inputPath, { signal }).catch(() => null);
|
|
11948
12345
|
if (!audio) return await ffmpegIsMissing(signal) ? "unavailable" : "none";
|
|
11949
12346
|
if (audio.audioStreamCount !== 1 || (audio.audioStreamLeadSeconds ?? 0) > 0) return "none";
|
|
11950
|
-
const result = await
|
|
12347
|
+
const result = await runFfmpeg9(
|
|
11951
12348
|
["-v", "error", "-y", "-i", inputPath, "-map", "0:a:0", "-vn", "-c:a", "copy"].concat([
|
|
11952
12349
|
"-movflags",
|
|
11953
12350
|
"+faststart",
|
|
@@ -11961,10 +12358,10 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
|
|
|
11961
12358
|
return isMissing(result) ? "unavailable" : "none";
|
|
11962
12359
|
}
|
|
11963
12360
|
var isMissing = (result) => result.error?.code === "ENOENT";
|
|
11964
|
-
var ffmpegIsMissing = async (signal) => isMissing(await
|
|
12361
|
+
var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg9(["-version"], { signal }));
|
|
11965
12362
|
|
|
11966
12363
|
// src/services/distributed/probeFacts.ts
|
|
11967
|
-
import { writeFileSync as
|
|
12364
|
+
import { writeFileSync as writeFileSync12 } from "node:fs";
|
|
11968
12365
|
import { extname as extname6 } from "node:path";
|
|
11969
12366
|
import { buildProbeFactsText } from "@hyperframes/engine";
|
|
11970
12367
|
var PROBE_FACTS_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
|
|
@@ -11973,7 +12370,7 @@ function wantsProbeFacts(path) {
|
|
|
11973
12370
|
}
|
|
11974
12371
|
async function buildProbeFacts(inputPath, outputPath, signal) {
|
|
11975
12372
|
try {
|
|
11976
|
-
|
|
12373
|
+
writeFileSync12(outputPath, await buildProbeFactsText(inputPath, signal));
|
|
11977
12374
|
return "built";
|
|
11978
12375
|
} catch (error) {
|
|
11979
12376
|
if (signal?.aborted) throw error;
|