@hyperframes/producer 0.9.15 → 0.9.17
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 +489 -396
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +68 -68
- package/dist/index.js +1045 -671
- package/dist/public-server.js +739 -329
- package/dist/services/distributed/chunkFrames.d.ts +1 -2
- package/dist/services/render/deferredVideoFrames.d.ts +42 -0
- package/dist/services/render/observability.d.ts +2 -0
- package/dist/services/render/segmentVideoFrames.d.ts +41 -0
- package/dist/services/render/stages/captureSegmentedStage.d.ts +5 -2
- package/dist/services/render/stages/captureStreamingStage.d.ts +7 -0
- package/dist/services/render/stages/compileStage.d.ts +12 -3
- package/dist/services/render/videoFrameWindow.d.ts +12 -0
- package/dist/services/renderOrchestrator.d.ts +2 -5
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
// src/services/renderOrchestrator.ts
|
|
10
10
|
import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
|
|
11
11
|
import {
|
|
12
|
-
existsSync as
|
|
13
|
-
mkdirSync as
|
|
14
|
-
readdirSync as
|
|
15
|
-
rmSync as
|
|
16
|
-
statSync as
|
|
17
|
-
writeFileSync as
|
|
12
|
+
existsSync as existsSync20,
|
|
13
|
+
mkdirSync as mkdirSync12,
|
|
14
|
+
readdirSync as readdirSync6,
|
|
15
|
+
rmSync as rmSync12,
|
|
16
|
+
statSync as statSync9,
|
|
17
|
+
writeFileSync as writeFileSync7,
|
|
18
18
|
copyFileSync as copyFileSync5,
|
|
19
19
|
appendFileSync
|
|
20
20
|
} from "fs";
|
|
@@ -29,16 +29,14 @@ import {
|
|
|
29
29
|
toFps as toFps2
|
|
30
30
|
} from "@hyperframes/core";
|
|
31
31
|
import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
32
|
-
import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
|
|
33
32
|
import { HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE } from "@hyperframes/parsers";
|
|
34
|
-
import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
|
|
35
33
|
import {
|
|
36
34
|
extractMediaMetadata as extractMediaMetadata4,
|
|
37
35
|
resolveConfig as resolveConfig2,
|
|
38
36
|
closeCaptureSession as closeCaptureSession8,
|
|
39
37
|
createCaptureSession as createCaptureSession8,
|
|
40
38
|
initializeSession as initializeSession8,
|
|
41
|
-
createVideoFrameInjector,
|
|
39
|
+
createVideoFrameInjector as createVideoFrameInjector2,
|
|
42
40
|
getEncoderPreset as getEncoderPreset3,
|
|
43
41
|
distributeFrames as distributeFrames2,
|
|
44
42
|
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
@@ -62,13 +60,14 @@ import {
|
|
|
62
60
|
isDrawElementCaptureError,
|
|
63
61
|
getDrawElementVerificationDetails,
|
|
64
62
|
augmentProtocolTimeoutError,
|
|
65
|
-
augmentPageNavigationTimeoutError
|
|
63
|
+
augmentPageNavigationTimeoutError,
|
|
64
|
+
displaySize
|
|
66
65
|
} from "@hyperframes/engine";
|
|
67
66
|
import {
|
|
68
67
|
referenceBackgroundResult,
|
|
69
68
|
createReferenceBackgroundRecorder
|
|
70
69
|
} from "@hyperframes/engine";
|
|
71
|
-
import { join as
|
|
70
|
+
import { join as join31, dirname as dirname13, resolve as resolve8 } from "path";
|
|
72
71
|
import { totalmem as totalmem2 } from "node:os";
|
|
73
72
|
import { randomUUID } from "crypto";
|
|
74
73
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -2017,6 +2016,7 @@ import {
|
|
|
2017
2016
|
getCapturePerfSummary as getCapturePerfSummary2,
|
|
2018
2017
|
initializeSession as initializeSession2,
|
|
2019
2018
|
prepareCaptureSessionForReuse as prepareCaptureSessionForReuse2,
|
|
2019
|
+
resolveDeVerifyMinDb as resolveDeVerifyMinDb2,
|
|
2020
2020
|
shouldDisableBrowserPoolForParallelWorker,
|
|
2021
2021
|
spawnStreamingEncoder as spawnStreamingEncoder2
|
|
2022
2022
|
} from "@hyperframes/engine";
|
|
@@ -3887,42 +3887,47 @@ function createDrainFrameGuard(args) {
|
|
|
3887
3887
|
sizes.push(buf.length);
|
|
3888
3888
|
if (sizes.length > 60) sizes.shift();
|
|
3889
3889
|
}
|
|
3890
|
-
|
|
3891
|
-
if (truth) {
|
|
3892
|
-
let db;
|
|
3893
|
-
try {
|
|
3894
|
-
db = await psnrDb(buf, truth);
|
|
3895
|
-
} catch (err) {
|
|
3896
|
-
log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
|
|
3897
|
-
frame: idx,
|
|
3898
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3899
|
-
});
|
|
3900
|
-
return buf;
|
|
3901
|
-
}
|
|
3902
|
-
if (db < verifyMinDb) {
|
|
3903
|
-
const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
|
|
3904
|
-
if (dumpDir) {
|
|
3905
|
-
await Promise.all([
|
|
3906
|
-
writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
|
|
3907
|
-
writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
|
|
3908
|
-
]).catch(() => {
|
|
3909
|
-
});
|
|
3910
|
-
}
|
|
3911
|
-
throw new DrawElementVerificationError(
|
|
3912
|
-
`drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
|
|
3913
|
-
{ kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
|
|
3914
|
-
);
|
|
3915
|
-
}
|
|
3916
|
-
stats.verifyChecked += 1;
|
|
3917
|
-
stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
|
|
3918
|
-
log.info("[Render] drawElement self-verify passed", {
|
|
3919
|
-
frame: idx,
|
|
3920
|
-
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
|
|
3921
|
-
});
|
|
3922
|
-
}
|
|
3890
|
+
await verifyDrawElementSample(session, idx, buf, { log, stats, verifyMinDb });
|
|
3923
3891
|
return buf;
|
|
3924
3892
|
};
|
|
3925
3893
|
}
|
|
3894
|
+
async function verifyDrawElementSample(session, idx, buf, ctx) {
|
|
3895
|
+
const { log, stats, verifyMinDb } = ctx;
|
|
3896
|
+
const truth = session.deVerifyFrames?.get(idx);
|
|
3897
|
+
if (!truth) return;
|
|
3898
|
+
let db;
|
|
3899
|
+
try {
|
|
3900
|
+
db = await psnrDb(buf, truth);
|
|
3901
|
+
} catch (err) {
|
|
3902
|
+
log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
|
|
3903
|
+
frame: idx,
|
|
3904
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3905
|
+
});
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
if (db < verifyMinDb) {
|
|
3909
|
+
const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
|
|
3910
|
+
if (dumpDir) {
|
|
3911
|
+
await Promise.all([
|
|
3912
|
+
writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
|
|
3913
|
+
writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
|
|
3914
|
+
]).catch(() => {
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
throw new DrawElementVerificationError(
|
|
3918
|
+
`drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
|
|
3919
|
+
{ kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
|
|
3920
|
+
);
|
|
3921
|
+
}
|
|
3922
|
+
if (stats) {
|
|
3923
|
+
stats.verifyChecked += 1;
|
|
3924
|
+
stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
|
|
3925
|
+
}
|
|
3926
|
+
log.info("[Render] drawElement self-verify passed", {
|
|
3927
|
+
frame: idx,
|
|
3928
|
+
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3926
3931
|
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, resume, risk) {
|
|
3927
3932
|
let prev = null;
|
|
3928
3933
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
@@ -4386,6 +4391,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4386
4391
|
);
|
|
4387
4392
|
} else {
|
|
4388
4393
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4394
|
+
const verifyMinDb = resolveDeVerifyMinDb();
|
|
4389
4395
|
let lastProgressAt = Date.now();
|
|
4390
4396
|
for (let i = resume.from; i < totalFrames; i++) {
|
|
4391
4397
|
assertNotAborted();
|
|
@@ -4402,6 +4408,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4402
4408
|
{ session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
|
|
4403
4409
|
abortSignal
|
|
4404
4410
|
);
|
|
4411
|
+
await verifyDrawElementSample(session, i, buffer, { log, verifyMinDb });
|
|
4405
4412
|
await reorderBuffer.waitForFrame(i);
|
|
4406
4413
|
ensureFrameWritten(await currentEncoder.writeFrame(buffer), i, currentEncoder);
|
|
4407
4414
|
reorderBuffer.advanceTo(i + 1);
|
|
@@ -4510,6 +4517,10 @@ async function captureSegmentFrames(ctx, segment, encoder) {
|
|
|
4510
4517
|
},
|
|
4511
4518
|
ctx.abortSignal
|
|
4512
4519
|
);
|
|
4520
|
+
await verifyDrawElementSample(ctx.session, i, buffer, {
|
|
4521
|
+
log: ctx.log,
|
|
4522
|
+
verifyMinDb: ctx.verifyMinDb
|
|
4523
|
+
});
|
|
4513
4524
|
ensureFrameWritten(await encoder.writeFrame(buffer), i, encoder);
|
|
4514
4525
|
ctx.job.framesRendered = i + 1;
|
|
4515
4526
|
lastProgressAt = Date.now();
|
|
@@ -4524,6 +4535,25 @@ async function captureSegmentFrames(ctx, segment, encoder) {
|
|
|
4524
4535
|
}
|
|
4525
4536
|
}
|
|
4526
4537
|
async function captureSegment(ctx, segment) {
|
|
4538
|
+
const { frames } = ctx;
|
|
4539
|
+
if (!frames) return captureSegmentToEncoder(ctx, segment);
|
|
4540
|
+
try {
|
|
4541
|
+
reportFrameProgress(
|
|
4542
|
+
ctx.job,
|
|
4543
|
+
`Reading video for segment ${segment.index + 1}/${ctx.segmentCount}`,
|
|
4544
|
+
Math.round(25 + segment.startFrame / ctx.totalFrames * 55),
|
|
4545
|
+
ctx.onProgress,
|
|
4546
|
+
segment.startFrame,
|
|
4547
|
+
ctx.totalFrames
|
|
4548
|
+
);
|
|
4549
|
+
await frames.prepare(segment);
|
|
4550
|
+
ctx.assertNotAborted();
|
|
4551
|
+
return await captureSegmentToEncoder(ctx, segment);
|
|
4552
|
+
} finally {
|
|
4553
|
+
frames.release();
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
async function captureSegmentToEncoder(ctx, segment) {
|
|
4527
4557
|
const segmentPath = segmentOutputPath(ctx.segmentDir, segment.index);
|
|
4528
4558
|
let encoder;
|
|
4529
4559
|
try {
|
|
@@ -4576,16 +4606,16 @@ var DEFAULT_DEPS = {
|
|
|
4576
4606
|
function defaultSessionFactory(input) {
|
|
4577
4607
|
let probeSession = input.probeSession;
|
|
4578
4608
|
return {
|
|
4579
|
-
create: async () => {
|
|
4609
|
+
create: async (injector = input.createRenderVideoFrameInjector) => {
|
|
4580
4610
|
const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
|
|
4581
4611
|
if (probeSession && probeCannotCaptureEveryFrame) {
|
|
4582
4612
|
await closeCaptureSession2(probeSession).catch(() => {
|
|
4583
4613
|
});
|
|
4584
4614
|
probeSession = null;
|
|
4585
4615
|
}
|
|
4586
|
-
const session = probeSession ?? await openSegmentedSession(input);
|
|
4616
|
+
const session = probeSession ?? await openSegmentedSession(input, injector);
|
|
4587
4617
|
if (probeSession) {
|
|
4588
|
-
openSegmentedSessionReuse(input, probeSession);
|
|
4618
|
+
openSegmentedSessionReuse(input, probeSession, injector);
|
|
4589
4619
|
probeSession = null;
|
|
4590
4620
|
}
|
|
4591
4621
|
try {
|
|
@@ -4600,12 +4630,12 @@ function defaultSessionFactory(input) {
|
|
|
4600
4630
|
}
|
|
4601
4631
|
};
|
|
4602
4632
|
}
|
|
4603
|
-
function openSegmentedSessionReuse(input, session) {
|
|
4604
|
-
prepareCaptureSessionForReuse2(session, input.framesDir,
|
|
4633
|
+
function openSegmentedSessionReuse(input, session, injector) {
|
|
4634
|
+
prepareCaptureSessionForReuse2(session, input.framesDir, injector());
|
|
4605
4635
|
}
|
|
4606
|
-
async function openSegmentedSession(input) {
|
|
4636
|
+
async function openSegmentedSession(input, injector) {
|
|
4607
4637
|
const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
|
|
4608
|
-
const videoInjector =
|
|
4638
|
+
const videoInjector = injector();
|
|
4609
4639
|
const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
|
|
4610
4640
|
const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
|
|
4611
4641
|
parallel: (input.workerCount ?? 1) > 1
|
|
@@ -4618,6 +4648,19 @@ async function openSegmentedSession(input) {
|
|
|
4618
4648
|
ownBrowser ? { ...captureCfg, enableBrowserPool: false } : captureCfg
|
|
4619
4649
|
);
|
|
4620
4650
|
}
|
|
4651
|
+
function removeJoinedSegments(run, segmentPaths) {
|
|
4652
|
+
if (!run.input.segmentVideoFrames || run.input.job.config.keepSegments === true) return;
|
|
4653
|
+
for (const path of segmentPaths) {
|
|
4654
|
+
try {
|
|
4655
|
+
run.deps.removeFile(path);
|
|
4656
|
+
} catch (err) {
|
|
4657
|
+
run.input.log.warn("[Render] could not remove a joined segment file", {
|
|
4658
|
+
path,
|
|
4659
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4621
4664
|
async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal, cfg) {
|
|
4622
4665
|
const result = await concat(segmentPaths, videoOnlyPath, abortSignal, cfg);
|
|
4623
4666
|
if (result.success) return;
|
|
@@ -4629,12 +4672,14 @@ async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal,
|
|
|
4629
4672
|
async function createSegmentWorker(run, id) {
|
|
4630
4673
|
const { input, deps } = run;
|
|
4631
4674
|
const factory = input.sessionFactoryForWorker?.(id) ?? run.sessionFactory;
|
|
4675
|
+
const frames = input.segmentVideoFrames?.forWorker();
|
|
4632
4676
|
return {
|
|
4633
4677
|
id,
|
|
4634
4678
|
factory,
|
|
4635
4679
|
sessionSegments: 0,
|
|
4636
4680
|
ctx: {
|
|
4637
|
-
session: await factory.create(),
|
|
4681
|
+
session: await factory.create(frames && (() => frames.hook)),
|
|
4682
|
+
frames,
|
|
4638
4683
|
job: input.job,
|
|
4639
4684
|
cfg: input.cfg,
|
|
4640
4685
|
totalFrames: input.totalFrames,
|
|
@@ -4645,6 +4690,8 @@ async function createSegmentWorker(run, id) {
|
|
|
4645
4690
|
spawnEncoder: deps.spawnEncoder,
|
|
4646
4691
|
captureFrame: deps.captureFrame,
|
|
4647
4692
|
stallTimeoutMs: resolveCaptureStallTimeoutMs(),
|
|
4693
|
+
log: input.log,
|
|
4694
|
+
verifyMinDb: resolveDeVerifyMinDb2(),
|
|
4648
4695
|
abortSignal: input.abortSignal,
|
|
4649
4696
|
assertNotAborted: input.assertNotAborted,
|
|
4650
4697
|
onProgress: input.onProgress,
|
|
@@ -4664,7 +4711,8 @@ async function recycleWorkerSession(run, worker, why) {
|
|
|
4664
4711
|
run.input.dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
|
|
4665
4712
|
const memory = worker.ctx.session.chromeMemory?.stats();
|
|
4666
4713
|
await run.deps.closeSession(worker.ctx.session);
|
|
4667
|
-
|
|
4714
|
+
const { frames } = worker.ctx;
|
|
4715
|
+
worker.ctx.session = await worker.factory.create(frames && (() => frames.hook));
|
|
4668
4716
|
worker.sessionSegments = 0;
|
|
4669
4717
|
if (why === "cadence") run.totals.browserRecycles += 1;
|
|
4670
4718
|
run.input.log.info(`[Render] segment browser recycled (${why})`, {
|
|
@@ -4732,6 +4780,7 @@ async function concatResumedRun(run, segmentPaths) {
|
|
|
4732
4780
|
input.abortSignal,
|
|
4733
4781
|
input.cfg
|
|
4734
4782
|
);
|
|
4783
|
+
removeJoinedSegments(run, segmentPaths);
|
|
4735
4784
|
return {
|
|
4736
4785
|
success: true,
|
|
4737
4786
|
encodeMs: 0,
|
|
@@ -4806,6 +4855,7 @@ async function runCaptureSegmentedStage(input) {
|
|
|
4806
4855
|
input.abortSignal,
|
|
4807
4856
|
input.cfg
|
|
4808
4857
|
);
|
|
4858
|
+
removeJoinedSegments(run, segmentPaths);
|
|
4809
4859
|
return {
|
|
4810
4860
|
success: true,
|
|
4811
4861
|
encodeMs: run.totals.encodeMs,
|
|
@@ -5237,10 +5287,273 @@ function checkPageNeverReady(input) {
|
|
|
5237
5287
|
return new PageNeverReadyError(error);
|
|
5238
5288
|
}
|
|
5239
5289
|
|
|
5290
|
+
// src/services/render/deferredVideoFrames.ts
|
|
5291
|
+
import { mkdirSync as mkdirSync4, rmSync as rmSync7 } from "node:fs";
|
|
5292
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
5293
|
+
import {
|
|
5294
|
+
createFrameLookupTable,
|
|
5295
|
+
extractVideoFramesRange,
|
|
5296
|
+
runVideoExtractionWithRetry
|
|
5297
|
+
} from "@hyperframes/engine";
|
|
5298
|
+
function contiguousRuns(sortedIndexes) {
|
|
5299
|
+
const runs2 = [];
|
|
5300
|
+
for (const index of sortedIndexes) {
|
|
5301
|
+
const last = runs2[runs2.length - 1];
|
|
5302
|
+
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
5303
|
+
else runs2.push({ firstFrame: index, frames: 1 });
|
|
5304
|
+
}
|
|
5305
|
+
return runs2;
|
|
5306
|
+
}
|
|
5307
|
+
function shownDeferredFrameIndexes(source, frames, fps) {
|
|
5308
|
+
const shown = /* @__PURE__ */ new Map();
|
|
5309
|
+
const deferred = source.extracted.filter((video) => video.deferredRange);
|
|
5310
|
+
if (deferred.length === 0) return shown;
|
|
5311
|
+
const table = createFrameLookupTable(
|
|
5312
|
+
source.videos,
|
|
5313
|
+
deferred.map((video) => ({
|
|
5314
|
+
...video,
|
|
5315
|
+
outputDir: "",
|
|
5316
|
+
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
5317
|
+
})),
|
|
5318
|
+
resolveRenderFpsConfig(fps).value
|
|
5319
|
+
);
|
|
5320
|
+
for (const frame of frames) {
|
|
5321
|
+
const globalTime = frame * fps.den / fps.num;
|
|
5322
|
+
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
5323
|
+
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
5324
|
+
indexes.add(payload.frameIndex);
|
|
5325
|
+
shown.set(videoId, indexes);
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
return shown;
|
|
5329
|
+
}
|
|
5330
|
+
async function extractDeferredFrames(input) {
|
|
5331
|
+
const deferred = input.extracted.filter((video) => video.deferredRange);
|
|
5332
|
+
if (deferred.length === 0) return [];
|
|
5333
|
+
const shown = shownDeferredFrameIndexes(input, input.frames, input.fps);
|
|
5334
|
+
return Promise.all(
|
|
5335
|
+
deferred.map(async (video) => {
|
|
5336
|
+
const outputDir = join12(input.outputRoot, video.videoId);
|
|
5337
|
+
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
5338
|
+
if (indexes.length === 0) {
|
|
5339
|
+
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
5340
|
+
}
|
|
5341
|
+
const range = video.deferredRange;
|
|
5342
|
+
const extracted = await extractVideoFramesRange(
|
|
5343
|
+
input.sourceOf(video),
|
|
5344
|
+
video.videoId,
|
|
5345
|
+
range.startTime,
|
|
5346
|
+
range.durationSeconds,
|
|
5347
|
+
{
|
|
5348
|
+
fps: input.fps,
|
|
5349
|
+
outputDir: input.outputRoot,
|
|
5350
|
+
format: range.format,
|
|
5351
|
+
frameRanges: contiguousRuns(indexes),
|
|
5352
|
+
shownBox: input.shownBoxes?.get(video.videoId)
|
|
5353
|
+
},
|
|
5354
|
+
input.signal,
|
|
5355
|
+
input.cfg
|
|
5356
|
+
);
|
|
5357
|
+
const missing = indexes.find((index) => !extracted.framePaths.has(index));
|
|
5358
|
+
if (missing !== void 0) throw input.onMissingFrame(video, missing);
|
|
5359
|
+
return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
|
|
5360
|
+
})
|
|
5361
|
+
);
|
|
5362
|
+
}
|
|
5363
|
+
async function extractAllDeferredFrames(extracted, input) {
|
|
5364
|
+
const framesTotal = extracted.reduce(
|
|
5365
|
+
(sum2, video) => sum2 + (video.deferredRange ? video.totalFrames : 0),
|
|
5366
|
+
0
|
|
5367
|
+
);
|
|
5368
|
+
let framesDone = 0;
|
|
5369
|
+
for (const video of extracted) {
|
|
5370
|
+
const range = video.deferredRange;
|
|
5371
|
+
if (!range) continue;
|
|
5372
|
+
const { result: filled } = await runVideoExtractionWithRetry(
|
|
5373
|
+
() => extractVideoFramesRange(
|
|
5374
|
+
input.sourceOf?.(video) ?? video.srcPath,
|
|
5375
|
+
video.videoId,
|
|
5376
|
+
range.startTime,
|
|
5377
|
+
range.durationSeconds,
|
|
5378
|
+
{ fps: input.fps, outputDir: dirname5(video.outputDir), format: range.format },
|
|
5379
|
+
input.signal,
|
|
5380
|
+
input.cfg
|
|
5381
|
+
),
|
|
5382
|
+
{
|
|
5383
|
+
signal: input.signal,
|
|
5384
|
+
maxTransientRetries: input.maxTransientRetries,
|
|
5385
|
+
onRetry: () => {
|
|
5386
|
+
rmSync7(video.outputDir, { recursive: true, force: true });
|
|
5387
|
+
mkdirSync4(video.outputDir, { recursive: true });
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
);
|
|
5391
|
+
if (filled.framePaths.size < video.totalFrames) {
|
|
5392
|
+
throw new Error(
|
|
5393
|
+
`[Render] video ${JSON.stringify(video.videoId)} has ${filled.framePaths.size} frames; it was expected to have ${video.totalFrames} frames.`
|
|
5394
|
+
);
|
|
5395
|
+
}
|
|
5396
|
+
video.framePaths = filled.framePaths;
|
|
5397
|
+
delete video.deferredRange;
|
|
5398
|
+
framesDone += video.totalFrames;
|
|
5399
|
+
input.onProgress?.(framesDone, framesTotal);
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
|
|
5403
|
+
// src/services/render/segmentVideoFrames.ts
|
|
5404
|
+
import { mkdirSync as mkdirSync5, rmSync as rmSync8 } from "node:fs";
|
|
5405
|
+
import { join as join13 } from "node:path";
|
|
5406
|
+
import {
|
|
5407
|
+
createFrameLookupTable as createFrameLookupTable2,
|
|
5408
|
+
createVideoFrameInjector
|
|
5409
|
+
} from "@hyperframes/engine";
|
|
5410
|
+
function segmentFramesDir(rootDir, index) {
|
|
5411
|
+
return join13(rootDir, "segment-frames", `segment_${String(index).padStart(5, "0")}`);
|
|
5412
|
+
}
|
|
5413
|
+
function createSegmentVideoFrames(input) {
|
|
5414
|
+
const extractFrames = input.extractFrames ?? extractDeferredFrames;
|
|
5415
|
+
const sourceOf = input.sourceOf ?? ((video) => video.srcPath);
|
|
5416
|
+
const fullyExtracted = input.extracted.filter((video) => !video.deferredRange);
|
|
5417
|
+
const tableFor = async (frames, dir) => {
|
|
5418
|
+
const sliced = await extractFrames({
|
|
5419
|
+
videos: input.videos,
|
|
5420
|
+
extracted: input.extracted,
|
|
5421
|
+
frames,
|
|
5422
|
+
fps: input.fps,
|
|
5423
|
+
outputRoot: dir,
|
|
5424
|
+
cfg: input.cfg,
|
|
5425
|
+
signal: input.abortSignal,
|
|
5426
|
+
sourceOf,
|
|
5427
|
+
onMissingFrame: (video, index) => new Error(
|
|
5428
|
+
`[Render] video ${JSON.stringify(video.videoId)} has no frame ${index}; it was expected to have ${video.totalFrames} frames.`
|
|
5429
|
+
)
|
|
5430
|
+
});
|
|
5431
|
+
return createFrameLookupTable2(
|
|
5432
|
+
input.videos,
|
|
5433
|
+
[...fullyExtracted, ...sliced],
|
|
5434
|
+
resolveRenderFpsConfig(input.fps).value
|
|
5435
|
+
);
|
|
5436
|
+
};
|
|
5437
|
+
const injectorFor = async (frames, dir) => createVideoFrameInjector(await tableFor(frames, dir), input.injectorOptions);
|
|
5438
|
+
let onDemandCalls = 0;
|
|
5439
|
+
const onDemandHook = () => Object.assign(
|
|
5440
|
+
async (page, time, heldVideoTime) => {
|
|
5441
|
+
const dir = join13(
|
|
5442
|
+
input.rootDir,
|
|
5443
|
+
"segment-frames",
|
|
5444
|
+
`on-demand_${String(onDemandCalls++).padStart(5, "0")}`
|
|
5445
|
+
);
|
|
5446
|
+
try {
|
|
5447
|
+
const frames = [time, heldVideoTime].filter((t) => t !== void 0).map((t) => Math.round(t * input.fps.num / input.fps.den));
|
|
5448
|
+
const injector = await injectorFor(frames, dir);
|
|
5449
|
+
await injector?.(page, time, heldVideoTime);
|
|
5450
|
+
} finally {
|
|
5451
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
5452
|
+
}
|
|
5453
|
+
},
|
|
5454
|
+
{ fresh: onDemandHook }
|
|
5455
|
+
);
|
|
5456
|
+
return {
|
|
5457
|
+
async forFrames(frames, name) {
|
|
5458
|
+
const dir = join13(input.rootDir, "segment-frames", name);
|
|
5459
|
+
const release = () => rmSync8(dir, { recursive: true, force: true });
|
|
5460
|
+
try {
|
|
5461
|
+
release();
|
|
5462
|
+
mkdirSync5(dir, { recursive: true });
|
|
5463
|
+
const table = await tableFor([...frames], dir);
|
|
5464
|
+
return { injector: () => createVideoFrameInjector(table, input.injectorOptions), release };
|
|
5465
|
+
} catch (error) {
|
|
5466
|
+
release();
|
|
5467
|
+
throw error;
|
|
5468
|
+
}
|
|
5469
|
+
},
|
|
5470
|
+
forWorker() {
|
|
5471
|
+
let injector = null;
|
|
5472
|
+
let folder = null;
|
|
5473
|
+
const onDemand = onDemandHook();
|
|
5474
|
+
const release = () => {
|
|
5475
|
+
injector = null;
|
|
5476
|
+
if (folder) rmSync8(folder, { recursive: true, force: true });
|
|
5477
|
+
folder = null;
|
|
5478
|
+
};
|
|
5479
|
+
return {
|
|
5480
|
+
hook: Object.assign(
|
|
5481
|
+
(page, time, heldVideoTime) => (injector ?? onDemand)(page, time, heldVideoTime),
|
|
5482
|
+
{ fresh: onDemandHook }
|
|
5483
|
+
),
|
|
5484
|
+
release,
|
|
5485
|
+
async prepare(segment) {
|
|
5486
|
+
release();
|
|
5487
|
+
const dir = segmentFramesDir(input.rootDir, segment.index);
|
|
5488
|
+
folder = dir;
|
|
5489
|
+
try {
|
|
5490
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
5491
|
+
mkdirSync5(dir, { recursive: true });
|
|
5492
|
+
injector = await injectorFor(
|
|
5493
|
+
Array.from(
|
|
5494
|
+
{ length: segment.endFrame - segment.startFrame },
|
|
5495
|
+
(_, i) => segment.startFrame + i
|
|
5496
|
+
),
|
|
5497
|
+
dir
|
|
5498
|
+
);
|
|
5499
|
+
} catch (error) {
|
|
5500
|
+
release();
|
|
5501
|
+
throw error;
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
};
|
|
5505
|
+
}
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
// src/services/render/videoFrameWindow.ts
|
|
5510
|
+
import { readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
5511
|
+
import { join as join14 } from "node:path";
|
|
5512
|
+
function shouldWindowVideoFrames(args) {
|
|
5513
|
+
return args.segmented && args.env.HF_VIDEO_FRAME_WINDOW?.trim().toLowerCase() === "true";
|
|
5514
|
+
}
|
|
5515
|
+
function directoryBytes(path) {
|
|
5516
|
+
let bytes = 0;
|
|
5517
|
+
let entries;
|
|
5518
|
+
try {
|
|
5519
|
+
entries = readdirSync3(path, { withFileTypes: true });
|
|
5520
|
+
} catch {
|
|
5521
|
+
return 0;
|
|
5522
|
+
}
|
|
5523
|
+
for (const entry of entries) {
|
|
5524
|
+
const child3 = join14(path, entry.name);
|
|
5525
|
+
try {
|
|
5526
|
+
bytes += entry.isDirectory() ? directoryBytes(child3) : statSync4(child3).size;
|
|
5527
|
+
} catch {
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
return bytes;
|
|
5531
|
+
}
|
|
5532
|
+
function trackSegmentFramePeak(frames, rootDir, onPeak) {
|
|
5533
|
+
let peak = 0;
|
|
5534
|
+
return {
|
|
5535
|
+
forFrames: (frameIndexes, name) => frames.forFrames(frameIndexes, name),
|
|
5536
|
+
forWorker() {
|
|
5537
|
+
const worker = frames.forWorker();
|
|
5538
|
+
return {
|
|
5539
|
+
...worker,
|
|
5540
|
+
async prepare(segment) {
|
|
5541
|
+
await worker.prepare(segment);
|
|
5542
|
+
const held = directoryBytes(join14(rootDir, "segment-frames"));
|
|
5543
|
+
if (held > peak) {
|
|
5544
|
+
peak = held;
|
|
5545
|
+
onPeak(peak);
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
};
|
|
5549
|
+
}
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5240
5553
|
// src/services/render/segmentManifest.ts
|
|
5241
5554
|
import { createHash as createHash2 } from "node:crypto";
|
|
5242
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
5243
|
-
import { join as
|
|
5555
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5556
|
+
import { join as join15 } from "node:path";
|
|
5244
5557
|
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
5245
5558
|
var MANIFEST_FILENAME = "segments.json";
|
|
5246
5559
|
function computeSegmentPlanHash(input) {
|
|
@@ -5268,7 +5581,7 @@ function computeSegmentPlanHash(input) {
|
|
|
5268
5581
|
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
5269
5582
|
}
|
|
5270
5583
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
5271
|
-
return
|
|
5584
|
+
return join15(projectRendersDir, ".hf-segments", planHash);
|
|
5272
5585
|
}
|
|
5273
5586
|
function hasNumber(value, key) {
|
|
5274
5587
|
return key in value && typeof Reflect.get(value, key) === "number";
|
|
@@ -5286,7 +5599,7 @@ function isManifest(value) {
|
|
|
5286
5599
|
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
5287
5600
|
}
|
|
5288
5601
|
function readSegmentManifest(segmentDir) {
|
|
5289
|
-
const path =
|
|
5602
|
+
const path = join15(segmentDir, MANIFEST_FILENAME);
|
|
5290
5603
|
if (!existsSync7(path)) return null;
|
|
5291
5604
|
try {
|
|
5292
5605
|
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
@@ -5296,10 +5609,10 @@ function readSegmentManifest(segmentDir) {
|
|
|
5296
5609
|
}
|
|
5297
5610
|
}
|
|
5298
5611
|
function writeSegmentManifest(segmentDir, manifest) {
|
|
5299
|
-
|
|
5300
|
-
const tmp =
|
|
5612
|
+
mkdirSync6(segmentDir, { recursive: true });
|
|
5613
|
+
const tmp = join15(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
5301
5614
|
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
5302
|
-
renameWithRetry2(tmp,
|
|
5615
|
+
renameWithRetry2(tmp, join15(segmentDir, MANIFEST_FILENAME));
|
|
5303
5616
|
}
|
|
5304
5617
|
async function probeSegmentFrameCount(path, probe) {
|
|
5305
5618
|
try {
|
|
@@ -5317,7 +5630,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
|
5317
5630
|
if (manifest.planHash !== expectedHash) return ok;
|
|
5318
5631
|
for (const entry of manifest.completed) {
|
|
5319
5632
|
if (!existsSync7(entry.path)) continue;
|
|
5320
|
-
const size =
|
|
5633
|
+
const size = statSync5(entry.path).size;
|
|
5321
5634
|
if (size <= 0 || size !== entry.bytes) continue;
|
|
5322
5635
|
const frames = await probeFrames(entry.path);
|
|
5323
5636
|
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
@@ -5860,7 +6173,7 @@ function computeVideoFrameCoverage(videos, extracted, fps) {
|
|
|
5860
6173
|
for (const video of videos) {
|
|
5861
6174
|
const entry = byId.get(video.id);
|
|
5862
6175
|
const expectedFrames = expectedFramesForVideo(video, entry, fps);
|
|
5863
|
-
const capturedFrames = entry ? entry.framePaths.size : 0;
|
|
6176
|
+
const capturedFrames = entry ? entry.deferredRange ? entry.totalFrames : entry.framePaths.size : 0;
|
|
5864
6177
|
const ratio = expectedFrames === 0 ? 1 : capturedFrames / expectedFrames;
|
|
5865
6178
|
reports.push({
|
|
5866
6179
|
videoId: video.id,
|
|
@@ -5895,14 +6208,17 @@ function countAuthoredTimedClips(html) {
|
|
|
5895
6208
|
}
|
|
5896
6209
|
|
|
5897
6210
|
// src/services/render/stages/compileStage.ts
|
|
5898
|
-
import {
|
|
6211
|
+
import { existsSync as existsSync11, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6212
|
+
import { join as join19 } from "node:path";
|
|
5899
6213
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
6214
|
+
import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
|
|
6215
|
+
import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
|
|
5900
6216
|
|
|
5901
6217
|
// src/services/htmlCompiler.ts
|
|
5902
6218
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
5903
6219
|
import { createHash as createHash6 } from "node:crypto";
|
|
5904
|
-
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as
|
|
5905
|
-
import { join as
|
|
6220
|
+
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
|
|
6221
|
+
import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
|
|
5906
6222
|
import { parseHTML as parseHTML5 } from "linkedom";
|
|
5907
6223
|
import {
|
|
5908
6224
|
COMPOSITION_SOURCE_URL,
|
|
@@ -6148,8 +6464,8 @@ import {
|
|
|
6148
6464
|
|
|
6149
6465
|
// src/services/animatedGifPrep.ts
|
|
6150
6466
|
import { createHash as createHash4 } from "node:crypto";
|
|
6151
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as
|
|
6152
|
-
import { dirname as
|
|
6467
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
|
|
6468
|
+
import { dirname as dirname6, isAbsolute as isAbsolute2, join as join16, resolve as resolve5 } from "node:path";
|
|
6153
6469
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
6154
6470
|
import { parseAnimatedGifMetadata } from "@hyperframes/core";
|
|
6155
6471
|
import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
|
|
@@ -6198,7 +6514,7 @@ function resolveLocalImageSourcePath(src, options) {
|
|
|
6198
6514
|
}
|
|
6199
6515
|
function isUsableFile(path) {
|
|
6200
6516
|
try {
|
|
6201
|
-
const stat =
|
|
6517
|
+
const stat = statSync6(path);
|
|
6202
6518
|
return stat.isFile() && stat.size > 0;
|
|
6203
6519
|
} catch {
|
|
6204
6520
|
return false;
|
|
@@ -6305,11 +6621,11 @@ async function runAnimatedGifTranscode(request) {
|
|
|
6305
6621
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
6306
6622
|
}
|
|
6307
6623
|
async function ensurePreparedWebm(input) {
|
|
6308
|
-
if (!existsSync8(
|
|
6309
|
-
|
|
6624
|
+
if (!existsSync8(dirname6(input.cachePath))) {
|
|
6625
|
+
mkdirSync7(dirname6(input.cachePath), { recursive: true });
|
|
6310
6626
|
}
|
|
6311
|
-
if (!existsSync8(
|
|
6312
|
-
|
|
6627
|
+
if (!existsSync8(dirname6(input.outputPath))) {
|
|
6628
|
+
mkdirSync7(dirname6(input.outputPath), { recursive: true });
|
|
6313
6629
|
}
|
|
6314
6630
|
if (!isUsableFile(input.cachePath)) {
|
|
6315
6631
|
const tmpPath = `${input.cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -6333,10 +6649,10 @@ async function ensurePreparedWebm(input) {
|
|
|
6333
6649
|
if (!isUsableFile(input.cachePath)) {
|
|
6334
6650
|
renameWithRetry3(tmpPath, input.cachePath);
|
|
6335
6651
|
} else {
|
|
6336
|
-
|
|
6652
|
+
rmSync9(tmpPath, { force: true });
|
|
6337
6653
|
}
|
|
6338
6654
|
} catch (error) {
|
|
6339
|
-
|
|
6655
|
+
rmSync9(tmpPath, { force: true });
|
|
6340
6656
|
throw error;
|
|
6341
6657
|
}
|
|
6342
6658
|
}
|
|
@@ -6392,7 +6708,7 @@ function replaceImageWithVideo(input) {
|
|
|
6392
6708
|
return video;
|
|
6393
6709
|
}
|
|
6394
6710
|
async function prepareAnimatedGifInputs(html, options) {
|
|
6395
|
-
const outputDir = options.outputDir ??
|
|
6711
|
+
const outputDir = options.outputDir ?? join16(options.downloadDir, PREPARED_GIF_SUBDIR);
|
|
6396
6712
|
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
|
|
6397
6713
|
const cacheDir = options.cacheDir ?? outputDir;
|
|
6398
6714
|
const { document: document2 } = parseHTML3(html);
|
|
@@ -6419,8 +6735,8 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6419
6735
|
);
|
|
6420
6736
|
const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
|
|
6421
6737
|
const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
|
|
6422
|
-
const cachePath =
|
|
6423
|
-
const outputPath =
|
|
6738
|
+
const cachePath = join16(cacheDir, filename);
|
|
6739
|
+
const outputPath = join16(outputDir, filename);
|
|
6424
6740
|
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
6425
6741
|
await ensurePreparedWebm({
|
|
6426
6742
|
sourcePath,
|
|
@@ -6457,15 +6773,15 @@ import { createHash as createHash5 } from "node:crypto";
|
|
|
6457
6773
|
import {
|
|
6458
6774
|
closeSync as closeSync4,
|
|
6459
6775
|
copyFileSync as copyFileSync3,
|
|
6460
|
-
mkdirSync as
|
|
6776
|
+
mkdirSync as mkdirSync8,
|
|
6461
6777
|
openSync as openSync4,
|
|
6462
6778
|
readFileSync as readFileSync6,
|
|
6463
6779
|
readSync as readSync3,
|
|
6464
|
-
rmSync as
|
|
6465
|
-
statSync as
|
|
6780
|
+
rmSync as rmSync10,
|
|
6781
|
+
statSync as statSync7,
|
|
6466
6782
|
writeFileSync as writeFileSync4
|
|
6467
6783
|
} from "node:fs";
|
|
6468
|
-
import { dirname as
|
|
6784
|
+
import { dirname as dirname7, join as join17, resolve as resolve6 } from "node:path";
|
|
6469
6785
|
import { parseHTML as parseHTML4 } from "linkedom";
|
|
6470
6786
|
import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
|
|
6471
6787
|
|
|
@@ -6705,14 +7021,14 @@ function resolveImageSource(src, options) {
|
|
|
6705
7021
|
}
|
|
6706
7022
|
function isUsableFile2(path) {
|
|
6707
7023
|
try {
|
|
6708
|
-
return
|
|
7024
|
+
return statSync7(path).size > 0;
|
|
6709
7025
|
} catch {
|
|
6710
7026
|
return false;
|
|
6711
7027
|
}
|
|
6712
7028
|
}
|
|
6713
7029
|
async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
6714
|
-
|
|
6715
|
-
|
|
7030
|
+
mkdirSync8(dirname7(cachePath), { recursive: true });
|
|
7031
|
+
mkdirSync8(dirname7(outputPath), { recursive: true });
|
|
6716
7032
|
if (!isUsableFile2(cachePath)) {
|
|
6717
7033
|
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
6718
7034
|
const inlinePath = `${tmpPath}.heic`;
|
|
@@ -6730,8 +7046,8 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
|
6730
7046
|
const reason = error instanceof Error ? error.message : String(error);
|
|
6731
7047
|
throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
|
|
6732
7048
|
} finally {
|
|
6733
|
-
|
|
6734
|
-
|
|
7049
|
+
rmSync10(tmpPath, { force: true });
|
|
7050
|
+
rmSync10(inlinePath, { force: true });
|
|
6735
7051
|
}
|
|
6736
7052
|
}
|
|
6737
7053
|
if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
|
|
@@ -6739,7 +7055,7 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
|
6739
7055
|
}
|
|
6740
7056
|
}
|
|
6741
7057
|
async function prepareHeifImageInputs(html, options) {
|
|
6742
|
-
const outputDir = options.outputDir ??
|
|
7058
|
+
const outputDir = options.outputDir ?? join17(options.downloadDir, PREPARED_HEIF_SUBDIR);
|
|
6743
7059
|
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
|
|
6744
7060
|
const cacheDir = options.cacheDir ?? outputDir;
|
|
6745
7061
|
const preparedAssets = /* @__PURE__ */ new Map();
|
|
@@ -6760,7 +7076,7 @@ async function prepareHeifImageInputs(html, options) {
|
|
|
6760
7076
|
await Promise.all(
|
|
6761
7077
|
[...unique].map(
|
|
6762
7078
|
([filename, source]) => withMediaProbeSlot(
|
|
6763
|
-
() => ensurePreparedJpeg(source,
|
|
7079
|
+
() => ensurePreparedJpeg(source, join17(cacheDir, filename), join17(outputDir, filename), options)
|
|
6764
7080
|
).catch((error) => {
|
|
6765
7081
|
if (!options.onFailure) throw error;
|
|
6766
7082
|
failed.add(filename);
|
|
@@ -6772,8 +7088,8 @@ async function prepareHeifImageInputs(html, options) {
|
|
|
6772
7088
|
if (failed.has(filename)) continue;
|
|
6773
7089
|
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
6774
7090
|
img.setAttribute("src", outputSrc);
|
|
6775
|
-
preparedAssets.set(outputSrc,
|
|
6776
|
-
preparedImages.push({ sourceSrc: src, outputSrc, outputPath:
|
|
7091
|
+
preparedAssets.set(outputSrc, join17(outputDir, filename));
|
|
7092
|
+
preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join17(outputDir, filename) });
|
|
6777
7093
|
}
|
|
6778
7094
|
return { html: document2.toString(), preparedAssets, preparedImages };
|
|
6779
7095
|
}
|
|
@@ -7097,8 +7413,8 @@ function detectShaderTransitionUsage(html) {
|
|
|
7097
7413
|
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
7098
7414
|
let filePath = src;
|
|
7099
7415
|
if (isHttpUrl(src)) {
|
|
7100
|
-
const remoteDir =
|
|
7101
|
-
if (!existsSync10(remoteDir))
|
|
7416
|
+
const remoteDir = join18(downloadDir, REMOTE_MEDIA_SUBDIR);
|
|
7417
|
+
if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
|
|
7102
7418
|
try {
|
|
7103
7419
|
filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
|
|
7104
7420
|
onTelemetry: logRemoteDownloadTelemetry
|
|
@@ -7108,7 +7424,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
7108
7424
|
}
|
|
7109
7425
|
} else {
|
|
7110
7426
|
filePath = decodedUrlPath(src);
|
|
7111
|
-
if (!filePath.startsWith("/")) filePath =
|
|
7427
|
+
if (!filePath.startsWith("/")) filePath = join18(baseDir, filePath);
|
|
7112
7428
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
7113
7429
|
}
|
|
7114
7430
|
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
@@ -7269,7 +7585,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
7269
7585
|
workItems.map(async (item) => {
|
|
7270
7586
|
const { html: compiledSub } = await compileHtmlFile(
|
|
7271
7587
|
rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
|
|
7272
|
-
|
|
7588
|
+
dirname8(item.filePath),
|
|
7273
7589
|
downloadDir,
|
|
7274
7590
|
void 0,
|
|
7275
7591
|
urlByAbsentLocalPath,
|
|
@@ -7699,7 +8015,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
|
|
|
7699
8015
|
var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
7700
8016
|
async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
|
|
7701
8017
|
if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
|
|
7702
|
-
if (!existsSync10(remoteDir))
|
|
8018
|
+
if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
|
|
7703
8019
|
const urlToLocal = /* @__PURE__ */ new Map();
|
|
7704
8020
|
await Promise.all(
|
|
7705
8021
|
[...urlSet].map(async (url) => {
|
|
@@ -7746,7 +8062,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
7746
8062
|
return downloadAndRewriteUrls(
|
|
7747
8063
|
urlSet,
|
|
7748
8064
|
html,
|
|
7749
|
-
|
|
8065
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7750
8066
|
"Remote media download failed",
|
|
7751
8067
|
"Localized remote media source(s)"
|
|
7752
8068
|
);
|
|
@@ -7761,7 +8077,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
7761
8077
|
return downloadAndRewriteUrls(
|
|
7762
8078
|
urlSet,
|
|
7763
8079
|
html,
|
|
7764
|
-
|
|
8080
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7765
8081
|
"Remote image download failed",
|
|
7766
8082
|
"Localized remote image source(s)"
|
|
7767
8083
|
);
|
|
@@ -7777,7 +8093,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
7777
8093
|
return downloadAndRewriteUrls(
|
|
7778
8094
|
urlSet,
|
|
7779
8095
|
html,
|
|
7780
|
-
|
|
8096
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7781
8097
|
"Remote background-image download failed",
|
|
7782
8098
|
"Localized remote background-image(s)",
|
|
7783
8099
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -7887,7 +8203,7 @@ function projectStylesheetFontFamilies(html, projectDir) {
|
|
|
7887
8203
|
const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
7888
8204
|
const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
|
|
7889
8205
|
const path = decodedUrlPath(url);
|
|
7890
|
-
const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(
|
|
8206
|
+
const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname8(sheet), path);
|
|
7891
8207
|
return isPathInside2(file, projectDir) && existsSync10(file);
|
|
7892
8208
|
});
|
|
7893
8209
|
if (family && shipsFile) families.push(family);
|
|
@@ -7932,7 +8248,7 @@ function localizeFontFaceUrls(html, downloadDir) {
|
|
|
7932
8248
|
return downloadAndRewriteUrls(
|
|
7933
8249
|
collectFontFaceUrls(html),
|
|
7934
8250
|
html,
|
|
7935
|
-
|
|
8251
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7936
8252
|
"Remote font download failed",
|
|
7937
8253
|
"Localized remote font face(s)",
|
|
7938
8254
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -8604,11 +8920,36 @@ function adaptAspectAgnosticResolution(requested, aspectAgnostic, width, height,
|
|
|
8604
8920
|
});
|
|
8605
8921
|
return flipped;
|
|
8606
8922
|
}
|
|
8923
|
+
function resolveEntryHtmlPath(input) {
|
|
8924
|
+
const { projectDir, workDir, entryFile, log } = input;
|
|
8925
|
+
const htmlPath = join19(projectDir, entryFile);
|
|
8926
|
+
const rawEntry = readTextFile2(htmlPath);
|
|
8927
|
+
if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
|
|
8928
|
+
const projectIndexPath = join19(projectDir, "index.html");
|
|
8929
|
+
if (!existsSync11(projectIndexPath)) {
|
|
8930
|
+
throw new Error(
|
|
8931
|
+
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
8932
|
+
);
|
|
8933
|
+
}
|
|
8934
|
+
const standaloneHtml = extractStandaloneEntryFromIndex(
|
|
8935
|
+
readTextFile2(projectIndexPath),
|
|
8936
|
+
entryFile,
|
|
8937
|
+
rawEntry
|
|
8938
|
+
);
|
|
8939
|
+
if (!standaloneHtml) {
|
|
8940
|
+
throw new Error(
|
|
8941
|
+
`Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
|
|
8942
|
+
);
|
|
8943
|
+
}
|
|
8944
|
+
const wrapperPath = join19(workDir, "standalone-entry.html");
|
|
8945
|
+
writeFileSync5(wrapperPath, standaloneHtml, "utf-8");
|
|
8946
|
+
log.info("Extracted standalone entry from index.html host context", { entryFile });
|
|
8947
|
+
return wrapperPath;
|
|
8948
|
+
}
|
|
8607
8949
|
async function runCompileStage(input) {
|
|
8608
8950
|
const {
|
|
8609
8951
|
projectDir,
|
|
8610
8952
|
workDir,
|
|
8611
|
-
htmlPath,
|
|
8612
8953
|
entryFile,
|
|
8613
8954
|
job,
|
|
8614
8955
|
cfg,
|
|
@@ -8619,8 +8960,9 @@ async function runCompileStage(input) {
|
|
|
8619
8960
|
allowSystemFontCapture,
|
|
8620
8961
|
abortSignal
|
|
8621
8962
|
} = input;
|
|
8963
|
+
const htmlPath = resolveEntryHtmlPath({ projectDir, workDir, entryFile, log });
|
|
8622
8964
|
const compileStart = Date.now();
|
|
8623
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
8965
|
+
const compiled = await compileForRender(projectDir, htmlPath, join19(workDir, "downloads"), {
|
|
8624
8966
|
log,
|
|
8625
8967
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
8626
8968
|
allowSystemFontCapture,
|
|
@@ -8628,7 +8970,7 @@ async function runCompileStage(input) {
|
|
|
8628
8970
|
variables: input.variables,
|
|
8629
8971
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
8630
8972
|
pageMotionBlur: input.pageMotionBlur,
|
|
8631
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
8973
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join19(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
8632
8974
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
8633
8975
|
});
|
|
8634
8976
|
assertNotAborted();
|
|
@@ -8685,7 +9027,7 @@ async function runCompileStage(input) {
|
|
|
8685
9027
|
};
|
|
8686
9028
|
await preflightCompositionAssetMediaTypes({
|
|
8687
9029
|
projectDir,
|
|
8688
|
-
compiledDir:
|
|
9030
|
+
compiledDir: join19(workDir, "compiled"),
|
|
8689
9031
|
composition,
|
|
8690
9032
|
signal: abortSignal
|
|
8691
9033
|
});
|
|
@@ -8731,7 +9073,7 @@ async function runCompileStage(input) {
|
|
|
8731
9073
|
|
|
8732
9074
|
// src/services/render/stages/probeStage.ts
|
|
8733
9075
|
import { createHash as createHash7 } from "node:crypto";
|
|
8734
|
-
import { basename as basename5, join as
|
|
9076
|
+
import { basename as basename5, join as join20 } from "node:path";
|
|
8735
9077
|
import { parseHTML as parseHTML6 } from "linkedom";
|
|
8736
9078
|
import {
|
|
8737
9079
|
closeCaptureSession as closeCaptureSession4,
|
|
@@ -8940,7 +9282,7 @@ async function runProbeStage(input) {
|
|
|
8940
9282
|
});
|
|
8941
9283
|
fileServer = await createFileServer({
|
|
8942
9284
|
projectDir,
|
|
8943
|
-
compiledDir:
|
|
9285
|
+
compiledDir: join20(workDir, "compiled"),
|
|
8944
9286
|
port: 0,
|
|
8945
9287
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
8946
9288
|
fps: job.config.fps
|
|
@@ -8970,7 +9312,7 @@ async function runProbeStage(input) {
|
|
|
8970
9312
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
8971
9313
|
probeSession = await createCaptureSession4(
|
|
8972
9314
|
fileServer.url,
|
|
8973
|
-
|
|
9315
|
+
join20(workDir, "probe"),
|
|
8974
9316
|
captureOpts,
|
|
8975
9317
|
null,
|
|
8976
9318
|
probeCfg
|
|
@@ -9055,7 +9397,7 @@ async function runProbeStage(input) {
|
|
|
9055
9397
|
});
|
|
9056
9398
|
probeSession = await createCaptureSession4(
|
|
9057
9399
|
fileServer.url,
|
|
9058
|
-
|
|
9400
|
+
join20(workDir, "probe-screenshot"),
|
|
9059
9401
|
captureOpts,
|
|
9060
9402
|
null,
|
|
9061
9403
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -9093,7 +9435,7 @@ async function runProbeStage(input) {
|
|
|
9093
9435
|
compiled,
|
|
9094
9436
|
resolutions,
|
|
9095
9437
|
projectDir,
|
|
9096
|
-
|
|
9438
|
+
join20(workDir, "downloads")
|
|
9097
9439
|
);
|
|
9098
9440
|
assertNotAborted();
|
|
9099
9441
|
composition.videos = compiled.videos;
|
|
@@ -9284,7 +9626,7 @@ async function runProbeStage(input) {
|
|
|
9284
9626
|
}
|
|
9285
9627
|
await preflightCompositionAssetMediaTypes({
|
|
9286
9628
|
projectDir,
|
|
9287
|
-
compiledDir:
|
|
9629
|
+
compiledDir: join20(workDir, "compiled"),
|
|
9288
9630
|
composition,
|
|
9289
9631
|
signal: abortSignal
|
|
9290
9632
|
});
|
|
@@ -9450,11 +9792,11 @@ function validateDistributedDuration(input) {
|
|
|
9450
9792
|
}
|
|
9451
9793
|
|
|
9452
9794
|
// src/services/render/stages/extractVideosStage.ts
|
|
9453
|
-
import { existsSync as
|
|
9454
|
-
import { join as
|
|
9795
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
9796
|
+
import { join as join21 } from "node:path";
|
|
9455
9797
|
import {
|
|
9456
9798
|
classifyVideoExtractionError,
|
|
9457
|
-
createFrameLookupTable,
|
|
9799
|
+
createFrameLookupTable as createFrameLookupTable3,
|
|
9458
9800
|
detectTransfer,
|
|
9459
9801
|
extractAllVideoFrames,
|
|
9460
9802
|
extractMediaMetadata as extractMediaMetadata3,
|
|
@@ -9466,7 +9808,7 @@ import {
|
|
|
9466
9808
|
resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
|
|
9467
9809
|
isHttpUrl as isHttpUrl2,
|
|
9468
9810
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
9469
|
-
runVideoExtractionWithRetry,
|
|
9811
|
+
runVideoExtractionWithRetry as runVideoExtractionWithRetry2,
|
|
9470
9812
|
safeVideoExtractionSourceIdentity,
|
|
9471
9813
|
videoAudioId as videoAudioId2
|
|
9472
9814
|
} from "@hyperframes/engine";
|
|
@@ -9725,9 +10067,9 @@ async function runExtractVideosStage(input) {
|
|
|
9725
10067
|
const probeFailures = await Promise.all(
|
|
9726
10068
|
composition.videos.map(async (v) => {
|
|
9727
10069
|
const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
|
|
9728
|
-
if (!
|
|
10070
|
+
if (!existsSync12(videoPath)) return null;
|
|
9729
10071
|
try {
|
|
9730
|
-
const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await
|
|
10072
|
+
const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
|
|
9731
10073
|
signal: abortSignal,
|
|
9732
10074
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
9733
10075
|
onRetry: () => {
|
|
@@ -9763,7 +10105,7 @@ async function runExtractVideosStage(input) {
|
|
|
9763
10105
|
const probed = await Promise.all(
|
|
9764
10106
|
composition.images.map(async (img) => {
|
|
9765
10107
|
const imgPath = resolveProjectRelativeSrc3(img.src, projectDir, compiledDir);
|
|
9766
|
-
if (!
|
|
10108
|
+
if (!existsSync12(imgPath)) return null;
|
|
9767
10109
|
const colorSpace = await probeColorSpaceSafely(imgPath, log);
|
|
9768
10110
|
if (isHdrColorSpace2(colorSpace)) {
|
|
9769
10111
|
nativeHdrImageIds.add(img.id);
|
|
@@ -9797,14 +10139,14 @@ async function runExtractVideosStage(input) {
|
|
|
9797
10139
|
// because short boundary counts can differ by one frame.
|
|
9798
10140
|
{
|
|
9799
10141
|
fps: job.config.fps,
|
|
9800
|
-
outputDir:
|
|
10142
|
+
outputDir: join21(compiledDir, "__hyperframes_video_frames"),
|
|
9801
10143
|
format: job.config.videoFrameFormat ?? "auto",
|
|
9802
10144
|
toneMapHdrToSdr: rendersSdrOnly(job.config, composition),
|
|
9803
10145
|
timelineEnd: composition.duration,
|
|
9804
10146
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
9805
10147
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
9806
10148
|
deferRangeExtraction,
|
|
9807
|
-
contentKeyedDirs: [
|
|
10149
|
+
contentKeyedDirs: [join21(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
9808
10150
|
urlByAbsentLocalPath,
|
|
9809
10151
|
onProgress: ({ planned = 0, done = 0 }) => {
|
|
9810
10152
|
framesPlanned += planned;
|
|
@@ -9824,7 +10166,7 @@ async function runExtractVideosStage(input) {
|
|
|
9824
10166
|
materializeSymlinks
|
|
9825
10167
|
});
|
|
9826
10168
|
if (extractionResult.extracted.length > 0) {
|
|
9827
|
-
frameLookup =
|
|
10169
|
+
frameLookup = createFrameLookupTable3(
|
|
9828
10170
|
composition.videos,
|
|
9829
10171
|
extractionResult.extracted,
|
|
9830
10172
|
resolveRenderFpsConfig(job.config.fps).value
|
|
@@ -9883,7 +10225,7 @@ async function appendVideoAudioFromProbe(input) {
|
|
|
9883
10225
|
videos.map(async (video) => {
|
|
9884
10226
|
const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
|
|
9885
10227
|
const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
|
|
9886
|
-
if (!isHttpUrl2(readFrom) && !
|
|
10228
|
+
if (!isHttpUrl2(readFrom) && !existsSync12(readFrom)) return null;
|
|
9887
10229
|
const metadata = await extractMediaMetadata3(readFrom);
|
|
9888
10230
|
const window2 = await resolveFinalFrameExtractionWindow2(
|
|
9889
10231
|
readFrom,
|
|
@@ -9903,7 +10245,7 @@ async function appendVideoAudioFromProbe(input) {
|
|
|
9903
10245
|
}
|
|
9904
10246
|
|
|
9905
10247
|
// src/services/render/videoLayerPassthroughPlan.ts
|
|
9906
|
-
import { existsSync as
|
|
10248
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
9907
10249
|
import {
|
|
9908
10250
|
buildVideoLayerInput,
|
|
9909
10251
|
isHttpUrl as isHttpUrl3,
|
|
@@ -9971,7 +10313,7 @@ async function planVideoLayerPassthrough(input) {
|
|
|
9971
10313
|
const { video } = q;
|
|
9972
10314
|
if (isHttpUrl3(video.src)) return { reason: "remote_source" };
|
|
9973
10315
|
const path = resolveProjectRelativeSrc4(video.src, input.projectDir, input.compiledDir);
|
|
9974
|
-
if (!
|
|
10316
|
+
if (!existsSync13(path)) return { reason: "source_missing" };
|
|
9975
10317
|
const layout = await probeVideoLayout(input.page, video.id, input.canvas, input.duration);
|
|
9976
10318
|
if ("reason" in layout) return layout;
|
|
9977
10319
|
const layer = await buildVideoLayerInput(
|
|
@@ -9986,8 +10328,8 @@ async function planVideoLayerPassthrough(input) {
|
|
|
9986
10328
|
}
|
|
9987
10329
|
|
|
9988
10330
|
// src/services/render/stages/audioStage.ts
|
|
9989
|
-
import { existsSync as
|
|
9990
|
-
import { join as
|
|
10331
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
10332
|
+
import { join as join24 } from "node:path";
|
|
9991
10333
|
import {
|
|
9992
10334
|
MIXED_AUDIO_FILENAME,
|
|
9993
10335
|
audioFailureDiagnostics,
|
|
@@ -9997,8 +10339,8 @@ import {
|
|
|
9997
10339
|
} from "@hyperframes/engine";
|
|
9998
10340
|
|
|
9999
10341
|
// src/services/render/audioDeliveryLimit.ts
|
|
10000
|
-
import { existsSync as
|
|
10001
|
-
import { dirname as
|
|
10342
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
10343
|
+
import { dirname as dirname10, join as join23 } from "node:path";
|
|
10002
10344
|
import {
|
|
10003
10345
|
applyAudioFxChain,
|
|
10004
10346
|
describeFfmpegFailure as describeFfmpegFailure3,
|
|
@@ -10009,7 +10351,7 @@ import {
|
|
|
10009
10351
|
// src/services/render/audioPadTrim.ts
|
|
10010
10352
|
import { spawn } from "node:child_process";
|
|
10011
10353
|
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
10012
|
-
import { dirname as
|
|
10354
|
+
import { dirname as dirname9, join as join22 } from "node:path";
|
|
10013
10355
|
import {
|
|
10014
10356
|
DEFAULT_CONFIG,
|
|
10015
10357
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -10288,7 +10630,7 @@ async function constrainAacTruePeak(input) {
|
|
|
10288
10630
|
}
|
|
10289
10631
|
async function cutSource(input, scratchDir, attenuationDb) {
|
|
10290
10632
|
if (!input.sourcePath || !input.rewriteSource) return true;
|
|
10291
|
-
const cutPath =
|
|
10633
|
+
const cutPath = join22(scratchDir, "source.wav");
|
|
10292
10634
|
try {
|
|
10293
10635
|
const result = await input.rewriteSource(input.sourcePath, cutPath, gainFilter(attenuationDb));
|
|
10294
10636
|
if (!result.success) throw new Error(result.error);
|
|
@@ -10302,8 +10644,8 @@ async function cutSource(input, scratchDir, attenuationDb) {
|
|
|
10302
10644
|
async function enforceAacTruePeak(input) {
|
|
10303
10645
|
let scratchDir;
|
|
10304
10646
|
try {
|
|
10305
|
-
scratchDir = mkdtempSync3(
|
|
10306
|
-
const correctedPath =
|
|
10647
|
+
scratchDir = mkdtempSync3(join22(dirname9(input.audioPath), ".true-peak-"));
|
|
10648
|
+
const correctedPath = join22(scratchDir, "audio.m4a");
|
|
10307
10649
|
const measurements = [];
|
|
10308
10650
|
let attenuationDb = 0;
|
|
10309
10651
|
let sourcePeakDbfs;
|
|
@@ -10494,31 +10836,57 @@ async function runFfprobeJson(args, signal) {
|
|
|
10494
10836
|
}
|
|
10495
10837
|
|
|
10496
10838
|
// src/services/render/audioDeliveryLimit.ts
|
|
10497
|
-
var
|
|
10498
|
-
var
|
|
10839
|
+
var LIMIT_PASSES = 3;
|
|
10840
|
+
var LOWEST_CEILING_DBTP = AAC_DELIVERY_TRUE_PEAK_DBFS - 2;
|
|
10841
|
+
var PASS_HEADROOM_DB = 0.05;
|
|
10842
|
+
var limitChain = (ceiling) => ({
|
|
10499
10843
|
version: 1,
|
|
10500
|
-
nodes: [{ type: "truepeak", id: "delivery", params: { ceiling
|
|
10501
|
-
};
|
|
10844
|
+
nodes: [{ type: "truepeak", id: "delivery", params: { ceiling } }]
|
|
10845
|
+
});
|
|
10502
10846
|
async function limitDeliveryPeaks(input, deps = {
|
|
10503
10847
|
measure: measureTruePeakDbfs,
|
|
10504
10848
|
applyChain: applyAudioFxChain,
|
|
10505
10849
|
encode: encodeAac2,
|
|
10506
10850
|
rename: renameWithRetry6,
|
|
10507
|
-
sourceExists:
|
|
10851
|
+
sourceExists: existsSync14
|
|
10508
10852
|
}) {
|
|
10509
10853
|
const { audioPath, sourcePath, signal, timeoutMs } = input;
|
|
10510
10854
|
if (!deps.sourceExists(sourcePath)) return { success: true };
|
|
10511
10855
|
const peak = await deps.measure(audioPath, { timeoutMs, signal });
|
|
10512
10856
|
if (peak <= AAC_DELIVERY_TRUE_PEAK_DBFS) return { success: true };
|
|
10513
|
-
const
|
|
10857
|
+
const alternating = ["a", "b"].map(
|
|
10858
|
+
(k) => join23(dirname10(sourcePath), `audio-source-limited-${k}.wav`)
|
|
10859
|
+
);
|
|
10860
|
+
let encodedFrom;
|
|
10861
|
+
let ceiling = AAC_DELIVERY_TRUE_PEAK_DBFS;
|
|
10862
|
+
for (let pass = 0; pass < LIMIT_PASSES; pass++) {
|
|
10863
|
+
const limitedPath = alternating[pass % 2];
|
|
10864
|
+
const result = await limitPass(input, deps, ceiling, limitedPath);
|
|
10865
|
+
if ("encodeFailure" in result) return result.encodeFailure;
|
|
10866
|
+
if ("limitFailed" in result) break;
|
|
10867
|
+
encodedFrom = limitedPath;
|
|
10868
|
+
if (result.peak <= AAC_DELIVERY_TRUE_PEAK_DBFS || ceiling <= LOWEST_CEILING_DBTP) break;
|
|
10869
|
+
ceiling = Math.max(
|
|
10870
|
+
ceiling - (result.peak - AAC_DELIVERY_TRUE_PEAK_DBFS) - PASS_HEADROOM_DB,
|
|
10871
|
+
LOWEST_CEILING_DBTP
|
|
10872
|
+
);
|
|
10873
|
+
}
|
|
10874
|
+
if (encodedFrom) deps.rename(encodedFrom, sourcePath, { copyFallback: true });
|
|
10875
|
+
return { success: true };
|
|
10876
|
+
}
|
|
10877
|
+
async function limitPass(input, deps, ceiling, limitedPath) {
|
|
10878
|
+
const { audioPath, sourcePath, signal, timeoutMs } = input;
|
|
10514
10879
|
try {
|
|
10515
|
-
await deps.applyChain(sourcePath,
|
|
10880
|
+
await deps.applyChain(sourcePath, limitChain(ceiling), limitedPath, {
|
|
10881
|
+
trackId: "delivery",
|
|
10882
|
+
signal
|
|
10883
|
+
});
|
|
10516
10884
|
} catch (error) {
|
|
10517
10885
|
if (signal?.aborted) throw error;
|
|
10518
10886
|
input.warn?.("[audio] true-peak limit failed, the whole-mix cut applies", {
|
|
10519
10887
|
error: error instanceof Error ? error.message : String(error)
|
|
10520
10888
|
});
|
|
10521
|
-
return {
|
|
10889
|
+
return { limitFailed: true };
|
|
10522
10890
|
}
|
|
10523
10891
|
const encoded = await deps.encode(limitedPath, audioPath, input.targetDurationSeconds, {
|
|
10524
10892
|
signal,
|
|
@@ -10526,13 +10894,14 @@ async function limitDeliveryPeaks(input, deps = {
|
|
|
10526
10894
|
});
|
|
10527
10895
|
if (!encoded.success) {
|
|
10528
10896
|
return {
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10897
|
+
encodeFailure: {
|
|
10898
|
+
success: false,
|
|
10899
|
+
error: `[audioDeliveryLimit] ${describeFfmpegFailure3(encoded, timeoutMs)}`,
|
|
10900
|
+
failureReason: encoded.failureReason
|
|
10901
|
+
}
|
|
10532
10902
|
};
|
|
10533
10903
|
}
|
|
10534
|
-
deps.
|
|
10535
|
-
return { success: true };
|
|
10904
|
+
return { peak: await deps.measure(audioPath, { timeoutMs, signal }) };
|
|
10536
10905
|
}
|
|
10537
10906
|
|
|
10538
10907
|
// src/services/render/stages/audioStage.ts
|
|
@@ -10558,7 +10927,7 @@ function runAudioStage(input) {
|
|
|
10558
10927
|
}
|
|
10559
10928
|
async function runAudioStageScoped(input) {
|
|
10560
10929
|
const stageStart = Date.now();
|
|
10561
|
-
const pcmSourcePath =
|
|
10930
|
+
const pcmSourcePath = join24(input.workDir, "audio-source.wav");
|
|
10562
10931
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
10563
10932
|
let deliverySourcePath;
|
|
10564
10933
|
try {
|
|
@@ -10574,7 +10943,7 @@ async function runAudioStageScoped(input) {
|
|
|
10574
10943
|
);
|
|
10575
10944
|
audioLoweredDb = guard.loweredDb;
|
|
10576
10945
|
input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
|
|
10577
|
-
if (input.keepDeliverySource && !guard.sourceStale &&
|
|
10946
|
+
if (input.keepDeliverySource && !guard.sourceStale && existsSync15(pcmSourcePath)) {
|
|
10578
10947
|
deliverySourcePath = pcmSourcePath;
|
|
10579
10948
|
}
|
|
10580
10949
|
}
|
|
@@ -10623,7 +10992,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
10623
10992
|
assertNotAborted,
|
|
10624
10993
|
log
|
|
10625
10994
|
} = input;
|
|
10626
|
-
const audioOutputPath =
|
|
10995
|
+
const audioOutputPath = join24(workDir, MIXED_AUDIO_FILENAME);
|
|
10627
10996
|
let hasAudio = false;
|
|
10628
10997
|
let audioError;
|
|
10629
10998
|
let audioFailures;
|
|
@@ -10634,7 +11003,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
10634
11003
|
audioResult = await processCompositionAudio(
|
|
10635
11004
|
audios,
|
|
10636
11005
|
projectDir,
|
|
10637
|
-
|
|
11006
|
+
join24(workDir, "audio-work"),
|
|
10638
11007
|
audioOutputPath,
|
|
10639
11008
|
duration,
|
|
10640
11009
|
abortSignal,
|
|
@@ -10702,8 +11071,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
10702
11071
|
}
|
|
10703
11072
|
|
|
10704
11073
|
// src/services/render/stages/captureStage.ts
|
|
10705
|
-
import { existsSync as
|
|
10706
|
-
import { join as
|
|
11074
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
|
|
11075
|
+
import { join as join25 } from "node:path";
|
|
10707
11076
|
import {
|
|
10708
11077
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
10709
11078
|
frameFileExtension,
|
|
@@ -10773,16 +11142,16 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
|
|
|
10773
11142
|
}
|
|
10774
11143
|
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
10775
11144
|
let total = sampleDirectoryBytes(framesDir);
|
|
10776
|
-
if (!
|
|
10777
|
-
for (const attempt of
|
|
11145
|
+
if (!existsSync16(workDir)) return total;
|
|
11146
|
+
for (const attempt of readdirSync4(workDir)) {
|
|
10778
11147
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
10779
|
-
total += sampleDirectoryBytes(
|
|
11148
|
+
total += sampleDirectoryBytes(join25(workDir, attempt));
|
|
10780
11149
|
continue;
|
|
10781
11150
|
}
|
|
10782
11151
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
10783
|
-
const attemptDir =
|
|
10784
|
-
for (const name of
|
|
10785
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
11152
|
+
const attemptDir = join25(workDir, attempt);
|
|
11153
|
+
for (const name of readdirSync4(attemptDir)) {
|
|
11154
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join25(attemptDir, name));
|
|
10786
11155
|
}
|
|
10787
11156
|
}
|
|
10788
11157
|
return total;
|
|
@@ -11076,8 +11445,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
|
|
|
11076
11445
|
}
|
|
11077
11446
|
|
|
11078
11447
|
// src/services/render/stages/captureHdrStage.ts
|
|
11079
|
-
import { existsSync as
|
|
11080
|
-
import { join as
|
|
11448
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "node:fs";
|
|
11449
|
+
import { join as join29 } from "node:path";
|
|
11081
11450
|
import {
|
|
11082
11451
|
closeCaptureSession as closeCaptureSession7,
|
|
11083
11452
|
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
@@ -11149,7 +11518,7 @@ function createHdrImageTransferCache(options = {}) {
|
|
|
11149
11518
|
}
|
|
11150
11519
|
|
|
11151
11520
|
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
11152
|
-
import { join as
|
|
11521
|
+
import { join as join26 } from "node:path";
|
|
11153
11522
|
import {
|
|
11154
11523
|
TRANSITIONS,
|
|
11155
11524
|
crossfade
|
|
@@ -11274,7 +11643,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
11274
11643
|
);
|
|
11275
11644
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
11276
11645
|
writeFileExclusiveSync(
|
|
11277
|
-
|
|
11646
|
+
join26(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
11278
11647
|
normalCanvas
|
|
11279
11648
|
);
|
|
11280
11649
|
}
|
|
@@ -11306,7 +11675,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
11306
11675
|
}
|
|
11307
11676
|
|
|
11308
11677
|
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
11309
|
-
import { join as
|
|
11678
|
+
import { join as join28 } from "node:path";
|
|
11310
11679
|
import {
|
|
11311
11680
|
TRANSITIONS as TRANSITIONS2,
|
|
11312
11681
|
closeCaptureSession as closeCaptureSession6,
|
|
@@ -11320,9 +11689,9 @@ import {
|
|
|
11320
11689
|
// src/services/shaderTransitionWorkerPool.ts
|
|
11321
11690
|
import { Worker } from "node:worker_threads";
|
|
11322
11691
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
11323
|
-
import { dirname as
|
|
11692
|
+
import { dirname as dirname11, join as join27 } from "node:path";
|
|
11324
11693
|
import { createRequire } from "node:module";
|
|
11325
|
-
import { existsSync as
|
|
11694
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
11326
11695
|
import { cpus as cpus2 } from "node:os";
|
|
11327
11696
|
function resolveWorkerEntry(explicit) {
|
|
11328
11697
|
if (explicit && explicit.length > 0) {
|
|
@@ -11333,10 +11702,10 @@ function resolveWorkerEntry(explicit) {
|
|
|
11333
11702
|
const isTs = override.endsWith(".ts");
|
|
11334
11703
|
return { path: override, isTs };
|
|
11335
11704
|
}
|
|
11336
|
-
const moduleDir =
|
|
11337
|
-
const jsPath =
|
|
11338
|
-
if (
|
|
11339
|
-
const tsPath =
|
|
11705
|
+
const moduleDir = dirname11(fileURLToPath3(import.meta.url));
|
|
11706
|
+
const jsPath = join27(moduleDir, "shaderTransitionWorker.js");
|
|
11707
|
+
if (existsSync17(jsPath)) return { path: jsPath, isTs: false };
|
|
11708
|
+
const tsPath = join27(moduleDir, "shaderTransitionWorker.ts");
|
|
11340
11709
|
return { path: tsPath, isTs: true };
|
|
11341
11710
|
}
|
|
11342
11711
|
function buildExecArgv(entryIsTs) {
|
|
@@ -11704,7 +12073,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
11704
12073
|
);
|
|
11705
12074
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
11706
12075
|
writeFileExclusiveSync(
|
|
11707
|
-
|
|
12076
|
+
join28(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
11708
12077
|
canvas
|
|
11709
12078
|
);
|
|
11710
12079
|
}
|
|
@@ -11895,9 +12264,9 @@ async function runCaptureHdrStage(input) {
|
|
|
11895
12264
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
11896
12265
|
}
|
|
11897
12266
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
11898
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
11899
|
-
if (debugDumpDir && !
|
|
11900
|
-
|
|
12267
|
+
const debugDumpDir = debugDumpEnabled ? join29(framesDir, "debug-composite") : null;
|
|
12268
|
+
if (debugDumpDir && !existsSync18(debugDumpDir)) {
|
|
12269
|
+
mkdirSync10(debugDumpDir, { recursive: true });
|
|
11901
12270
|
}
|
|
11902
12271
|
const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
|
|
11903
12272
|
const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
|
|
@@ -12053,16 +12422,16 @@ async function runCaptureHdrStage(input) {
|
|
|
12053
12422
|
// src/services/render/stages/encodeStage.ts
|
|
12054
12423
|
import {
|
|
12055
12424
|
copyFileSync as copyFileSync4,
|
|
12056
|
-
existsSync as
|
|
12057
|
-
mkdirSync as
|
|
12058
|
-
readdirSync as
|
|
12425
|
+
existsSync as existsSync19,
|
|
12426
|
+
mkdirSync as mkdirSync11,
|
|
12427
|
+
readdirSync as readdirSync5,
|
|
12059
12428
|
readFileSync as readFileSync8,
|
|
12060
|
-
rmSync as
|
|
12061
|
-
statSync as
|
|
12062
|
-
writeFileSync as
|
|
12429
|
+
rmSync as rmSync11,
|
|
12430
|
+
statSync as statSync8,
|
|
12431
|
+
writeFileSync as writeFileSync6
|
|
12063
12432
|
} from "node:fs";
|
|
12064
12433
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
12065
|
-
import { dirname as
|
|
12434
|
+
import { dirname as dirname12, join as join30 } from "node:path";
|
|
12066
12435
|
import {
|
|
12067
12436
|
encodeFramesChunkedConcat,
|
|
12068
12437
|
encodeFramesFromDir,
|
|
@@ -12155,12 +12524,12 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
|
12155
12524
|
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
12156
12525
|
return gifEncodeFailure(startTime, outputPath, error);
|
|
12157
12526
|
}
|
|
12158
|
-
|
|
12527
|
+
writeFileSync6(outputPath, gif);
|
|
12159
12528
|
return null;
|
|
12160
12529
|
}
|
|
12161
12530
|
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
12162
12531
|
const startTime = Date.now();
|
|
12163
|
-
const files =
|
|
12532
|
+
const files = readdirSync5(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
|
|
12164
12533
|
const frameCount = files.length;
|
|
12165
12534
|
if (frameCount === 0) {
|
|
12166
12535
|
return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
|
|
@@ -12187,7 +12556,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
12187
12556
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
12188
12557
|
if (failure) return failure;
|
|
12189
12558
|
}
|
|
12190
|
-
const fileSize =
|
|
12559
|
+
const fileSize = existsSync19(outputPath) ? statSync8(outputPath).size : 0;
|
|
12191
12560
|
return {
|
|
12192
12561
|
success: true,
|
|
12193
12562
|
outputPath,
|
|
@@ -12196,7 +12565,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
12196
12565
|
fileSize
|
|
12197
12566
|
};
|
|
12198
12567
|
} finally {
|
|
12199
|
-
|
|
12568
|
+
rmSync11(input.palettePath, { force: true });
|
|
12200
12569
|
}
|
|
12201
12570
|
}
|
|
12202
12571
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -12273,7 +12642,7 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
12273
12642
|
await new Promise((resolve13) => wake = resolve13);
|
|
12274
12643
|
}
|
|
12275
12644
|
if (signal.aborted) return;
|
|
12276
|
-
const mergedPath =
|
|
12645
|
+
const mergedPath = join30(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
12277
12646
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
12278
12647
|
landed.delete(next);
|
|
12279
12648
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -12348,19 +12717,19 @@ async function runEncodeStage(input) {
|
|
|
12348
12717
|
const stage5Start = Date.now();
|
|
12349
12718
|
if (isPngSequence) {
|
|
12350
12719
|
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
12351
|
-
if (!
|
|
12352
|
-
const captured =
|
|
12720
|
+
if (!existsSync19(outputPath)) mkdirSync11(outputPath, { recursive: true });
|
|
12721
|
+
const captured = readdirSync5(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
12353
12722
|
if (captured.length === 0) {
|
|
12354
12723
|
throw new Error(
|
|
12355
12724
|
`[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
|
|
12356
12725
|
);
|
|
12357
12726
|
}
|
|
12358
12727
|
captured.forEach((name, i) => {
|
|
12359
|
-
const dst =
|
|
12360
|
-
copyFileSync4(
|
|
12728
|
+
const dst = join30(outputPath, formatExportFrameName(i, "png"));
|
|
12729
|
+
copyFileSync4(join30(framesDir, name), dst);
|
|
12361
12730
|
});
|
|
12362
|
-
if (hasAudio && audioOutputPath &&
|
|
12363
|
-
copyFileSync4(audioOutputPath,
|
|
12731
|
+
if (hasAudio && audioOutputPath && existsSync19(audioOutputPath)) {
|
|
12732
|
+
copyFileSync4(audioOutputPath, join30(outputPath, MIXED_AUDIO_FILENAME2));
|
|
12364
12733
|
log.info(
|
|
12365
12734
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
12366
12735
|
);
|
|
@@ -12380,7 +12749,7 @@ async function runEncodeStage(input) {
|
|
|
12380
12749
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
12381
12750
|
fps: job.config.fps,
|
|
12382
12751
|
loop,
|
|
12383
|
-
palettePath:
|
|
12752
|
+
palettePath: join30(dirname12(videoOnlyPath), "gif-palette.png"),
|
|
12384
12753
|
preserveAlpha: needsAlpha,
|
|
12385
12754
|
signal: abortSignal,
|
|
12386
12755
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -12586,14 +12955,14 @@ function sampleDirectoryBytes(dir) {
|
|
|
12586
12955
|
if (!current) continue;
|
|
12587
12956
|
let entries = [];
|
|
12588
12957
|
try {
|
|
12589
|
-
entries =
|
|
12958
|
+
entries = readdirSync6(current);
|
|
12590
12959
|
} catch {
|
|
12591
12960
|
continue;
|
|
12592
12961
|
}
|
|
12593
12962
|
for (const name of entries) {
|
|
12594
|
-
const full =
|
|
12963
|
+
const full = join31(current, name);
|
|
12595
12964
|
try {
|
|
12596
|
-
const st =
|
|
12965
|
+
const st = statSync9(full);
|
|
12597
12966
|
if (st.isDirectory()) {
|
|
12598
12967
|
stack.push(full);
|
|
12599
12968
|
} else if (st.isFile()) {
|
|
@@ -12741,18 +13110,14 @@ function hasUsableVideoDimensions(metadata) {
|
|
|
12741
13110
|
return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
|
|
12742
13111
|
}
|
|
12743
13112
|
function collectVideoMetadataHints(extractedVideos) {
|
|
12744
|
-
return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
|
|
12745
|
-
id: video.videoId,
|
|
12746
|
-
width: video.metadata.width,
|
|
12747
|
-
height: video.metadata.height
|
|
12748
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
13113
|
+
return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({ id: video.videoId, ...displaySize(video.metadata) })).sort((a, b) => a.id.localeCompare(b.id));
|
|
12749
13114
|
}
|
|
12750
13115
|
function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
12751
13116
|
const ranges = [];
|
|
12752
13117
|
let rangeStart = null;
|
|
12753
13118
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
12754
|
-
const framePath =
|
|
12755
|
-
const missing = !
|
|
13119
|
+
const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
13120
|
+
const missing = !existsSync20(framePath) || statSync9(framePath).size <= 8;
|
|
12756
13121
|
if (missing && rangeStart === null) {
|
|
12757
13122
|
rangeStart = frameIndex;
|
|
12758
13123
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -12768,7 +13133,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
12768
13133
|
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
12769
13134
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
12770
13135
|
const batches = [];
|
|
12771
|
-
const outputDirFor = (batchIndex, workerId) =>
|
|
13136
|
+
const outputDirFor = (batchIndex, workerId) => join31(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
12772
13137
|
if (stride > 1) {
|
|
12773
13138
|
const spans = /* @__PURE__ */ new Map();
|
|
12774
13139
|
for (const range of ranges) {
|
|
@@ -12828,8 +13193,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
12828
13193
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
12829
13194
|
}
|
|
12830
13195
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
12831
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
12832
|
-
return
|
|
13196
|
+
if (platform2 === "win32" || platform2 === "darwin") return join31(systemTempDir, "hf-render-");
|
|
13197
|
+
return join31(dirname13(outputPath), `work-${jobId}-`);
|
|
12833
13198
|
}
|
|
12834
13199
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
12835
13200
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -12876,8 +13241,8 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
12876
13241
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
12877
13242
|
let captured = 0;
|
|
12878
13243
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
12879
|
-
const framePath =
|
|
12880
|
-
if (
|
|
13244
|
+
const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
13245
|
+
if (existsSync20(framePath)) captured++;
|
|
12881
13246
|
}
|
|
12882
13247
|
return captured;
|
|
12883
13248
|
}
|
|
@@ -12902,7 +13267,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12902
13267
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
12903
13268
|
});
|
|
12904
13269
|
pendingTransientRetry = false;
|
|
12905
|
-
const attemptWorkDir =
|
|
13270
|
+
const attemptWorkDir = join31(options.workDir, `capture-attempt-${attempt}`);
|
|
12906
13271
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
12907
13272
|
missingRanges,
|
|
12908
13273
|
currentWorkers,
|
|
@@ -13534,19 +13899,19 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
13534
13899
|
};
|
|
13535
13900
|
}
|
|
13536
13901
|
function resolveRenderDebugDir() {
|
|
13537
|
-
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(
|
|
13538
|
-
return
|
|
13902
|
+
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname13(fileURLToPath4(import.meta.url)), "../..");
|
|
13903
|
+
return join31(producerRoot, ".debug");
|
|
13539
13904
|
}
|
|
13540
13905
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
13541
13906
|
validateHlsRenderConfig(job.config);
|
|
13542
13907
|
validateDeliveryEncoderConfig(job.config);
|
|
13543
13908
|
const debugDir = resolveRenderDebugDir();
|
|
13544
|
-
const outputDir =
|
|
13545
|
-
if (!
|
|
13546
|
-
const workDir = job.config.debug ?
|
|
13909
|
+
const outputDir = dirname13(outputPath);
|
|
13910
|
+
if (!existsSync20(outputDir)) mkdirSync12(outputDir, { recursive: true });
|
|
13911
|
+
const workDir = job.config.debug ? join31(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
13547
13912
|
const pipelineStart = Date.now();
|
|
13548
13913
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
13549
|
-
const logPath = job.config.debug ?
|
|
13914
|
+
const logPath = job.config.debug ? join31(workDir, "render.log") : null;
|
|
13550
13915
|
const execution = new RenderExecutionContext({
|
|
13551
13916
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
13552
13917
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -13560,7 +13925,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
13560
13925
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
13561
13926
|
return;
|
|
13562
13927
|
}
|
|
13563
|
-
|
|
13928
|
+
rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
13564
13929
|
});
|
|
13565
13930
|
try {
|
|
13566
13931
|
await executeRenderPipeline({
|
|
@@ -13608,7 +13973,7 @@ async function executeRenderPipeline(input) {
|
|
|
13608
13973
|
imageDecodeFailures: 0
|
|
13609
13974
|
};
|
|
13610
13975
|
let hdrPerf;
|
|
13611
|
-
const perfOutputPath =
|
|
13976
|
+
const perfOutputPath = join31(workDir, "perf-summary.json");
|
|
13612
13977
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
13613
13978
|
const observability = new RenderObservabilityRecorder({
|
|
13614
13979
|
pipelineStartMs: pipelineStart,
|
|
@@ -13684,7 +14049,7 @@ async function executeRenderPipeline(input) {
|
|
|
13684
14049
|
job.startedAt = /* @__PURE__ */ new Date();
|
|
13685
14050
|
assertNotAborted();
|
|
13686
14051
|
assertConfiguredFfmpegBinariesExist();
|
|
13687
|
-
if (!
|
|
14052
|
+
if (!existsSync20(workDir)) mkdirSync12(workDir, { recursive: true });
|
|
13688
14053
|
if (job.config.debug) {
|
|
13689
14054
|
log.info("[Render] Debug artifacts enabled", { workDir, logPath });
|
|
13690
14055
|
}
|
|
@@ -13713,36 +14078,11 @@ async function executeRenderPipeline(input) {
|
|
|
13713
14078
|
requestedWorkers: job.config.workers ?? "auto"
|
|
13714
14079
|
});
|
|
13715
14080
|
const entryFile = job.config.entryFile || "index.html";
|
|
13716
|
-
|
|
13717
|
-
if (!
|
|
14081
|
+
const htmlPath = join31(projectDir, entryFile);
|
|
14082
|
+
if (!existsSync20(htmlPath)) {
|
|
13718
14083
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
13719
14084
|
}
|
|
13720
14085
|
assertNotAborted();
|
|
13721
|
-
const rawEntry = readTextFile2(htmlPath);
|
|
13722
|
-
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
13723
|
-
const wrapperPath = join28(workDir, "standalone-entry.html");
|
|
13724
|
-
const projectIndexPath = join28(projectDir, "index.html");
|
|
13725
|
-
if (!existsSync19(projectIndexPath)) {
|
|
13726
|
-
throw new Error(
|
|
13727
|
-
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
13728
|
-
);
|
|
13729
|
-
}
|
|
13730
|
-
const standaloneHtml = extractStandaloneEntryFromIndex(
|
|
13731
|
-
readTextFile2(projectIndexPath),
|
|
13732
|
-
entryFile,
|
|
13733
|
-
rawEntry
|
|
13734
|
-
);
|
|
13735
|
-
if (!standaloneHtml) {
|
|
13736
|
-
throw new Error(
|
|
13737
|
-
`Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
|
|
13738
|
-
);
|
|
13739
|
-
}
|
|
13740
|
-
writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
|
|
13741
|
-
htmlPath = wrapperPath;
|
|
13742
|
-
log.info("Extracted standalone entry from index.html host context", {
|
|
13743
|
-
entryFile
|
|
13744
|
-
});
|
|
13745
|
-
}
|
|
13746
14086
|
const stage1Start = Date.now();
|
|
13747
14087
|
updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress, {
|
|
13748
14088
|
code: "compile"
|
|
@@ -13754,7 +14094,6 @@ async function executeRenderPipeline(input) {
|
|
|
13754
14094
|
() => runCompileStage({
|
|
13755
14095
|
projectDir,
|
|
13756
14096
|
workDir,
|
|
13757
|
-
htmlPath,
|
|
13758
14097
|
entryFile,
|
|
13759
14098
|
job,
|
|
13760
14099
|
cfg,
|
|
@@ -13916,7 +14255,7 @@ async function executeRenderPipeline(input) {
|
|
|
13916
14255
|
canvas: { width, height },
|
|
13917
14256
|
page: probeSession.page,
|
|
13918
14257
|
projectDir,
|
|
13919
|
-
compiledDir:
|
|
14258
|
+
compiledDir: join31(workDir, "compiled")
|
|
13920
14259
|
});
|
|
13921
14260
|
if (videoLayerPlan.videoId === void 0) {
|
|
13922
14261
|
log.info(`[Render] Video layer passthrough off: ${videoLayerPlan.reason}`);
|
|
@@ -13934,7 +14273,22 @@ async function executeRenderPipeline(input) {
|
|
|
13934
14273
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
13935
14274
|
code: "extract_video"
|
|
13936
14275
|
});
|
|
13937
|
-
const compiledDir =
|
|
14276
|
+
const compiledDir = join31(workDir, "compiled");
|
|
14277
|
+
const segmentsCapture = (layeredOrEffectRoute) => shouldSegmentCapture({
|
|
14278
|
+
env: process.env,
|
|
14279
|
+
durationSeconds: probeResult.duration,
|
|
14280
|
+
outputFormat,
|
|
14281
|
+
layeredOrEffectRoute,
|
|
14282
|
+
streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, probeResult.duration)
|
|
14283
|
+
});
|
|
14284
|
+
const segmentedForWindow = segmentsCapture(compiled.hasShaderTransitions);
|
|
14285
|
+
const windowVideoFrames = job.config.motionBlur === void 0 && shouldWindowVideoFrames({ env: process.env, segmented: segmentedForWindow });
|
|
14286
|
+
if (!windowVideoFrames && shouldWindowVideoFrames({ env: process.env, segmented: true })) {
|
|
14287
|
+
const reason = job.config.motionBlur !== void 0 ? "motion blur" : outputFormat !== "mp4" && outputFormat !== "mov" ? `${outputFormat} output` : "capture is not segmented";
|
|
14288
|
+
log.info("[Render] HF_VIDEO_FRAME_WINDOW is set but this render extracts every frame", {
|
|
14289
|
+
reason
|
|
14290
|
+
});
|
|
14291
|
+
}
|
|
13938
14292
|
const extractResult = await observeRenderStage(
|
|
13939
14293
|
observability,
|
|
13940
14294
|
"video_extract",
|
|
@@ -13951,6 +14305,7 @@ async function executeRenderPipeline(input) {
|
|
|
13951
14305
|
assertNotAborted,
|
|
13952
14306
|
// Local staging can use links; distributed plan() alone requires real copies.
|
|
13953
14307
|
materializeSymlinks: false,
|
|
14308
|
+
deferRangeExtraction: windowVideoFrames ? true : void 0,
|
|
13954
14309
|
onProgress
|
|
13955
14310
|
})
|
|
13956
14311
|
);
|
|
@@ -14047,7 +14402,7 @@ async function executeRenderPipeline(input) {
|
|
|
14047
14402
|
try {
|
|
14048
14403
|
fileServer = await createFileServer({
|
|
14049
14404
|
projectDir,
|
|
14050
|
-
compiledDir:
|
|
14405
|
+
compiledDir: join31(workDir, "compiled"),
|
|
14051
14406
|
port: 0,
|
|
14052
14407
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
14053
14408
|
fps: job.config.fps
|
|
@@ -14065,8 +14420,8 @@ async function executeRenderPipeline(input) {
|
|
|
14065
14420
|
if (!activeFileServer) {
|
|
14066
14421
|
throw new Error("File server failed to initialize before frame capture");
|
|
14067
14422
|
}
|
|
14068
|
-
const framesDir =
|
|
14069
|
-
if (!
|
|
14423
|
+
const framesDir = join31(workDir, "captured-frames");
|
|
14424
|
+
if (!existsSync20(framesDir)) mkdirSync12(framesDir, { recursive: true });
|
|
14070
14425
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
14071
14426
|
code: "start_browsers"
|
|
14072
14427
|
});
|
|
@@ -14144,7 +14499,55 @@ async function executeRenderPipeline(input) {
|
|
|
14144
14499
|
});
|
|
14145
14500
|
const buildPrepCaptureOptions = () => stopPrepSessionOnScriptFailure(buildCaptureOptions());
|
|
14146
14501
|
const buildWorkerCaptureOptions = () => withScriptFailureStop(buildCaptureOptions(), job, log);
|
|
14147
|
-
|
|
14502
|
+
let videoFrameWindow = windowVideoFrames && (extractionResult?.extracted.some((video) => video.deferredRange) ?? false);
|
|
14503
|
+
if (windowVideoFrames) {
|
|
14504
|
+
const extracted = extractionResult?.extracted ?? [];
|
|
14505
|
+
log.info("[Render] Video frame window", {
|
|
14506
|
+
windowed: extracted.filter((video) => video.deferredRange).length,
|
|
14507
|
+
fullyExtracted: extracted.filter((video) => !video.deferredRange).length
|
|
14508
|
+
});
|
|
14509
|
+
}
|
|
14510
|
+
const buildSegmentVideoFrames = () => trackSegmentFramePeak(
|
|
14511
|
+
createSegmentVideoFrames({
|
|
14512
|
+
videos: composition.videos,
|
|
14513
|
+
extracted: extractionResult?.extracted ?? [],
|
|
14514
|
+
fps: job.config.fps,
|
|
14515
|
+
rootDir: workDir,
|
|
14516
|
+
cfg,
|
|
14517
|
+
injectorOptions: frameCacheOptions(cfg, workerCount),
|
|
14518
|
+
abortSignal: executionSignal
|
|
14519
|
+
}),
|
|
14520
|
+
workDir,
|
|
14521
|
+
(bytes) => updateCaptureObservability({ videoFrameWindowPeakBytes: bytes })
|
|
14522
|
+
);
|
|
14523
|
+
const createRenderVideoFrameInjector = () => createVideoFrameInjector2(frameLookup, frameCacheOptions(cfg, workerCount));
|
|
14524
|
+
const extractDeferredVideoFrames = async () => {
|
|
14525
|
+
videoFrameWindow = false;
|
|
14526
|
+
updateCaptureObservability({ videoFrameWindow: "off" });
|
|
14527
|
+
const extracted = extractionResult?.extracted ?? [];
|
|
14528
|
+
const videoCount = extracted.filter((video) => video.deferredRange).length;
|
|
14529
|
+
const startedAt = Date.now();
|
|
14530
|
+
log.info("[Render] Extracting the frames of the videos a window would have held back", {
|
|
14531
|
+
videoCount
|
|
14532
|
+
});
|
|
14533
|
+
await observeRenderStage(
|
|
14534
|
+
observability,
|
|
14535
|
+
"video_extract_deferred",
|
|
14536
|
+
{ videoCount },
|
|
14537
|
+
() => extractAllDeferredFrames(extracted, {
|
|
14538
|
+
fps: job.config.fps,
|
|
14539
|
+
cfg,
|
|
14540
|
+
signal: executionSignal,
|
|
14541
|
+
maxTransientRetries: resolveVideoExtractionPolicy().maxTransientRetries,
|
|
14542
|
+
onProgress: (done, total) => reportExtractProgress(job, done, total, onProgress)
|
|
14543
|
+
})
|
|
14544
|
+
);
|
|
14545
|
+
assertNotAborted();
|
|
14546
|
+
log.info("[Render] Extracted the deferred video frames", {
|
|
14547
|
+
videoCount,
|
|
14548
|
+
elapsedMs: Date.now() - startedAt
|
|
14549
|
+
});
|
|
14550
|
+
};
|
|
14148
14551
|
let captureCalibration;
|
|
14149
14552
|
const htmlInCanvasDetected = compiled.renderModeHints.reasons.some(
|
|
14150
14553
|
(r) => r.code === "htmlInCanvas"
|
|
@@ -14310,32 +14713,41 @@ async function executeRenderPipeline(input) {
|
|
|
14310
14713
|
updateJobStatus(job, "rendering", "Measuring capture speed", 25, onProgress, {
|
|
14311
14714
|
code: "start_browsers"
|
|
14312
14715
|
});
|
|
14313
|
-
const
|
|
14314
|
-
|
|
14315
|
-
"
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
|
|
14320
|
-
|
|
14321
|
-
|
|
14322
|
-
|
|
14323
|
-
|
|
14324
|
-
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
14328
|
-
|
|
14329
|
-
|
|
14330
|
-
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14716
|
+
const calibrationFrames = videoFrameWindow ? await buildSegmentVideoFrames().forFrames(
|
|
14717
|
+
selectCaptureCalibrationFrames(totalFrames),
|
|
14718
|
+
"calibration"
|
|
14719
|
+
) : void 0;
|
|
14720
|
+
let outcome;
|
|
14721
|
+
try {
|
|
14722
|
+
outcome = await observeRenderStage(
|
|
14723
|
+
observability,
|
|
14724
|
+
"capture_calibration",
|
|
14725
|
+
captureStageObservationData({
|
|
14726
|
+
forceScreenshot: captureForceScreenshot,
|
|
14727
|
+
// Override the default `capturing` — calibration writes probe
|
|
14728
|
+
// frames only, not `job.framesRendered`, so heartbeats reporting
|
|
14729
|
+
// `framesCompleted: 0` misread as broken. Field signal
|
|
14730
|
+
// ts=1784019503.
|
|
14731
|
+
stagePhase: "calibrating"
|
|
14732
|
+
}),
|
|
14733
|
+
() => runCaptureCalibration({
|
|
14734
|
+
cfg: calibrationCaptureConfig(cfg),
|
|
14735
|
+
fileServer: activeFileServer,
|
|
14736
|
+
workDir,
|
|
14737
|
+
log,
|
|
14738
|
+
job,
|
|
14739
|
+
totalFrames,
|
|
14740
|
+
forceScreenshot: captureForceScreenshot,
|
|
14741
|
+
probeSession,
|
|
14742
|
+
buildCaptureOptions: buildPrepCaptureOptions,
|
|
14743
|
+
createRenderVideoFrameInjector: calibrationFrames ? calibrationFrames.injector : createRenderVideoFrameInjector,
|
|
14744
|
+
assertNotAborted
|
|
14745
|
+
}),
|
|
14746
|
+
{ heartbeatMessage: "browser calibrating (frames not started)" }
|
|
14747
|
+
);
|
|
14748
|
+
} finally {
|
|
14749
|
+
calibrationFrames?.release();
|
|
14750
|
+
}
|
|
14339
14751
|
captureCalibration = outcome.calibration;
|
|
14340
14752
|
calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
|
|
14341
14753
|
captureForceScreenshot = outcome.forceScreenshot;
|
|
@@ -14567,7 +14979,7 @@ async function executeRenderPipeline(input) {
|
|
|
14567
14979
|
hls: ".mp4"
|
|
14568
14980
|
};
|
|
14569
14981
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
14570
|
-
const videoOnlyPath =
|
|
14982
|
+
const videoOnlyPath = join31(workDir, `video-only${videoExt}`);
|
|
14571
14983
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
14572
14984
|
if (usePageSideCompositingForTransitions) {
|
|
14573
14985
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -14668,13 +15080,7 @@ async function executeRenderPipeline(input) {
|
|
|
14668
15080
|
// excluded routes are the ones whose concat-copy or capture loop the
|
|
14669
15081
|
// segment contract does not cover (webm VP9 concat is fragile, HDR and
|
|
14670
15082
|
// shader transitions run their own compositor).
|
|
14671
|
-
useSegmentedCapture:
|
|
14672
|
-
env: process.env,
|
|
14673
|
-
durationSeconds: job.duration,
|
|
14674
|
-
outputFormat,
|
|
14675
|
-
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
14676
|
-
streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration)
|
|
14677
|
-
}),
|
|
15083
|
+
useSegmentedCapture: segmentsCapture(hasHdrContent || compiled.hasShaderTransitions),
|
|
14678
15084
|
useLayeredComposite,
|
|
14679
15085
|
usePageSideCompositing: usePageSideCompositingForTransitions,
|
|
14680
15086
|
hasHdrContent,
|
|
@@ -14714,10 +15120,14 @@ async function executeRenderPipeline(input) {
|
|
|
14714
15120
|
deWorkerInversion,
|
|
14715
15121
|
deParallelRouter
|
|
14716
15122
|
});
|
|
15123
|
+
if (videoFrameWindow && capturePlan.kind !== "sdr_segmented")
|
|
15124
|
+
await extractDeferredVideoFrames();
|
|
15125
|
+
updateCaptureObservability({ videoFrameWindow: videoFrameWindow ? "on" : "off" });
|
|
14717
15126
|
observability.checkpoint("capture_strategy", "resolved", {
|
|
14718
15127
|
plan: capturePlan.kind,
|
|
14719
15128
|
workerCount: capturePlan.workerCount,
|
|
14720
15129
|
forceScreenshot: capturePlan.forceScreenshot,
|
|
15130
|
+
videoFrameWindow: videoFrameWindow ? "on" : "off",
|
|
14721
15131
|
captureBeyondViewport: resolvedCaptureBeyondViewport ?? null,
|
|
14722
15132
|
useStreamingEncode: capturePlan.kind === "sdr_streaming",
|
|
14723
15133
|
useLayeredComposite: capturePlan.kind === "hdr_layered",
|
|
@@ -14855,7 +15265,7 @@ async function executeRenderPipeline(input) {
|
|
|
14855
15265
|
pageMotionBlur: job.config.pageMotionBlur,
|
|
14856
15266
|
captureQuality: captureOptions.quality
|
|
14857
15267
|
});
|
|
14858
|
-
const segmentDir = segmentDirFor(
|
|
15268
|
+
const segmentDir = segmentDirFor(join31(projectDir, "renders"), segmentPlanHash);
|
|
14859
15269
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
14860
15270
|
if (job.config.resumeSegments === true) {
|
|
14861
15271
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -14870,10 +15280,10 @@ async function executeRenderPipeline(input) {
|
|
|
14870
15280
|
});
|
|
14871
15281
|
}
|
|
14872
15282
|
if (completedSegments.size === 0) {
|
|
14873
|
-
|
|
15283
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14874
15284
|
}
|
|
14875
15285
|
} else {
|
|
14876
|
-
|
|
15286
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14877
15287
|
}
|
|
14878
15288
|
const emptySegmentManifest = () => ({
|
|
14879
15289
|
version: 1,
|
|
@@ -14923,6 +15333,7 @@ async function executeRenderPipeline(input) {
|
|
|
14923
15333
|
},
|
|
14924
15334
|
buildCaptureOptions: buildWorkerCaptureOptions,
|
|
14925
15335
|
createRenderVideoFrameInjector,
|
|
15336
|
+
segmentVideoFrames: videoFrameWindow ? buildSegmentVideoFrames() : void 0,
|
|
14926
15337
|
abortSignal: executionSignal,
|
|
14927
15338
|
assertNotAborted,
|
|
14928
15339
|
onProgress,
|
|
@@ -14974,7 +15385,7 @@ async function executeRenderPipeline(input) {
|
|
|
14974
15385
|
probeSession = null;
|
|
14975
15386
|
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14976
15387
|
}
|
|
14977
|
-
|
|
15388
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14978
15389
|
completedSegments = /* @__PURE__ */ new Set();
|
|
14979
15390
|
segmentManifest = emptySegmentManifest();
|
|
14980
15391
|
dedupPerfs.length = 0;
|
|
@@ -15013,11 +15424,12 @@ async function executeRenderPipeline(input) {
|
|
|
15013
15424
|
);
|
|
15014
15425
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
15015
15426
|
if (job.config.keepSegments !== true) {
|
|
15016
|
-
|
|
15427
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
15017
15428
|
}
|
|
15018
15429
|
} else {
|
|
15019
15430
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
15020
15431
|
syncCapturePlan();
|
|
15432
|
+
if (videoFrameWindow) await extractDeferredVideoFrames();
|
|
15021
15433
|
probeSession = null;
|
|
15022
15434
|
observability.checkpoint(
|
|
15023
15435
|
"capture_segmented",
|
|
@@ -15288,8 +15700,8 @@ async function executeRenderPipeline(input) {
|
|
|
15288
15700
|
"capture_disk",
|
|
15289
15701
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
15290
15702
|
);
|
|
15291
|
-
|
|
15292
|
-
|
|
15703
|
+
rmSync12(framesDir, { recursive: true, force: true });
|
|
15704
|
+
mkdirSync12(framesDir, { recursive: true });
|
|
15293
15705
|
resetCaptureAttemptProgress(job);
|
|
15294
15706
|
dedupPerfs.length = 0;
|
|
15295
15707
|
cfg.useDrawElement = false;
|
|
@@ -15394,7 +15806,7 @@ async function executeRenderPipeline(input) {
|
|
|
15394
15806
|
})
|
|
15395
15807
|
);
|
|
15396
15808
|
const totalElapsed = Date.now() - pipelineStart;
|
|
15397
|
-
const tmpPeakBytes =
|
|
15809
|
+
const tmpPeakBytes = existsSync20(workDir) ? sampleDirectoryBytes(workDir) : 0;
|
|
15398
15810
|
recordTransientRetryObservability();
|
|
15399
15811
|
observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
|
|
15400
15812
|
const observabilitySummary = observability.summary({
|
|
@@ -15466,7 +15878,7 @@ async function executeRenderPipeline(input) {
|
|
|
15466
15878
|
job.perfSummary = perfSummary;
|
|
15467
15879
|
if (job.config.debug) {
|
|
15468
15880
|
try {
|
|
15469
|
-
|
|
15881
|
+
writeFileSync7(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
|
|
15470
15882
|
} catch (err) {
|
|
15471
15883
|
log.debug("Failed to write perf summary", {
|
|
15472
15884
|
perfOutputPath,
|
|
@@ -15475,8 +15887,8 @@ async function executeRenderPipeline(input) {
|
|
|
15475
15887
|
}
|
|
15476
15888
|
}
|
|
15477
15889
|
if (job.config.debug) {
|
|
15478
|
-
if (!isPngSequence && !isHls &&
|
|
15479
|
-
const debugOutput =
|
|
15890
|
+
if (!isPngSequence && !isHls && existsSync20(stagedOutputPath)) {
|
|
15891
|
+
const debugOutput = join31(workDir, `output${videoExt}`);
|
|
15480
15892
|
copyFileSync5(stagedOutputPath, debugOutput);
|
|
15481
15893
|
}
|
|
15482
15894
|
}
|
|
@@ -15590,15 +16002,15 @@ import {
|
|
|
15590
16002
|
REFERENCE_BACKGROUND_FIELD as REFERENCE_BACKGROUND_FIELD4,
|
|
15591
16003
|
referenceBackgroundResult as referenceBackgroundResult6
|
|
15592
16004
|
} from "@hyperframes/engine";
|
|
15593
|
-
import { mkdirSync as
|
|
15594
|
-
import { join as
|
|
16005
|
+
import { mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync7, readFileSync as readFileSync14, rmSync as rmSync18 } from "node:fs";
|
|
16006
|
+
import { join as join45 } from "node:path";
|
|
15595
16007
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
15596
16008
|
|
|
15597
16009
|
// src/services/distributed/mp4Stitch.ts
|
|
15598
|
-
import { createReadStream as createReadStream3, createWriteStream, mkdirSync as
|
|
16010
|
+
import { createReadStream as createReadStream3, createWriteStream, mkdirSync as mkdirSync13, mkdtempSync as mkdtempSync4, rmSync as rmSync13 } from "node:fs";
|
|
15599
16011
|
import { open } from "node:fs/promises";
|
|
15600
16012
|
import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
|
|
15601
|
-
import { dirname as
|
|
16013
|
+
import { dirname as dirname14, join as join32 } from "node:path";
|
|
15602
16014
|
import { finished, pipeline } from "node:stream/promises";
|
|
15603
16015
|
function boxAt(src, off, at, limit) {
|
|
15604
16016
|
let size = src.readUInt32BE(off);
|
|
@@ -15962,18 +16374,18 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
15962
16374
|
const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
|
|
15963
16375
|
const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
|
|
15964
16376
|
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
15965
|
-
|
|
16377
|
+
mkdirSync13(dirname14(outputPath), { recursive: true });
|
|
15966
16378
|
const workDir = mkdtempSync4(`${outputPath}.stitch-`);
|
|
15967
|
-
const out = createWriteStream(
|
|
16379
|
+
const out = createWriteStream(join32(workDir, "out.mp4"));
|
|
15968
16380
|
try {
|
|
15969
16381
|
out.write(header);
|
|
15970
16382
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
15971
16383
|
out.end();
|
|
15972
16384
|
await finished(out);
|
|
15973
|
-
renameWithRetry7(
|
|
16385
|
+
renameWithRetry7(join32(workDir, "out.mp4"), outputPath);
|
|
15974
16386
|
} finally {
|
|
15975
16387
|
out.destroy();
|
|
15976
|
-
|
|
16388
|
+
rmSync13(workDir, { recursive: true, force: true });
|
|
15977
16389
|
}
|
|
15978
16390
|
}
|
|
15979
16391
|
|
|
@@ -15987,13 +16399,13 @@ import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframe
|
|
|
15987
16399
|
import {
|
|
15988
16400
|
copyFileSync as copyFileSync6,
|
|
15989
16401
|
cpSync as cpSync2,
|
|
15990
|
-
existsSync as
|
|
16402
|
+
existsSync as existsSync25,
|
|
15991
16403
|
linkSync,
|
|
15992
|
-
mkdirSync as
|
|
15993
|
-
rmSync as
|
|
15994
|
-
writeFileSync as
|
|
16404
|
+
mkdirSync as mkdirSync16,
|
|
16405
|
+
rmSync as rmSync14,
|
|
16406
|
+
writeFileSync as writeFileSync10
|
|
15995
16407
|
} from "node:fs";
|
|
15996
|
-
import { extname as extname6, isAbsolute as isAbsolute3, join as
|
|
16408
|
+
import { extname as extname6, isAbsolute as isAbsolute3, join as join40, relative as relative5, sep as sep2 } from "node:path";
|
|
15997
16409
|
import {
|
|
15998
16410
|
fpsToNumber as fpsToNumber9,
|
|
15999
16411
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -16009,28 +16421,26 @@ import {
|
|
|
16009
16421
|
} from "@hyperframes/engine";
|
|
16010
16422
|
|
|
16011
16423
|
// src/services/distributed/planFrameCost.ts
|
|
16012
|
-
import { join as
|
|
16424
|
+
import { join as join35 } from "node:path";
|
|
16013
16425
|
import {
|
|
16014
16426
|
closeCaptureSession as closeCaptureSession9,
|
|
16015
16427
|
createCaptureSession as createCaptureSession9,
|
|
16016
|
-
createFrameLookupTable as
|
|
16428
|
+
createFrameLookupTable as createFrameLookupTable4,
|
|
16017
16429
|
initializeSession as initializeSession9
|
|
16018
16430
|
} from "@hyperframes/engine";
|
|
16019
16431
|
|
|
16020
16432
|
// src/services/distributed/chunkFrames.ts
|
|
16021
|
-
import { existsSync as
|
|
16022
|
-
import { extname as extname4, join as
|
|
16433
|
+
import { existsSync as existsSync22, readdirSync as readdirSync7 } from "node:fs";
|
|
16434
|
+
import { extname as extname4, join as join34 } from "node:path";
|
|
16023
16435
|
import {
|
|
16024
16436
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
16025
|
-
|
|
16026
|
-
createVideoFrameInjector as createVideoFrameInjector2,
|
|
16027
|
-
extractVideoFramesRange
|
|
16437
|
+
createVideoFrameInjector as createVideoFrameInjector3
|
|
16028
16438
|
} from "@hyperframes/engine";
|
|
16029
16439
|
|
|
16030
16440
|
// src/services/distributed/shared.ts
|
|
16031
16441
|
import { execFile as execFileCallback } from "node:child_process";
|
|
16032
|
-
import { dirname as
|
|
16033
|
-
import { existsSync as
|
|
16442
|
+
import { dirname as dirname15, join as join33 } from "node:path";
|
|
16443
|
+
import { existsSync as existsSync21, readFileSync as readFileSync9 } from "node:fs";
|
|
16034
16444
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
16035
16445
|
import { promisify as promisify2 } from "node:util";
|
|
16036
16446
|
import {
|
|
@@ -16045,8 +16455,8 @@ function isPlanAudioArtifactPath(path) {
|
|
|
16045
16455
|
}
|
|
16046
16456
|
function resolvePlanAudioPath(planDir) {
|
|
16047
16457
|
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
16048
|
-
const candidate =
|
|
16049
|
-
if (
|
|
16458
|
+
const candidate = join33(planDir, name);
|
|
16459
|
+
if (existsSync21(candidate)) return candidate;
|
|
16050
16460
|
}
|
|
16051
16461
|
return null;
|
|
16052
16462
|
}
|
|
@@ -16303,11 +16713,11 @@ function buildSyntheticRenderJob(input) {
|
|
|
16303
16713
|
var cachedProducerVersion = null;
|
|
16304
16714
|
function readProducerVersion() {
|
|
16305
16715
|
if (cachedProducerVersion !== null) return cachedProducerVersion;
|
|
16306
|
-
const startDir =
|
|
16716
|
+
const startDir = dirname15(fileURLToPath5(import.meta.url));
|
|
16307
16717
|
let current = startDir;
|
|
16308
16718
|
for (let i = 0; i < 10; i++) {
|
|
16309
|
-
const candidate =
|
|
16310
|
-
if (
|
|
16719
|
+
const candidate = join33(current, "package.json");
|
|
16720
|
+
if (existsSync21(candidate)) {
|
|
16311
16721
|
try {
|
|
16312
16722
|
const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
|
|
16313
16723
|
if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
|
|
@@ -16317,7 +16727,7 @@ function readProducerVersion() {
|
|
|
16317
16727
|
} catch {
|
|
16318
16728
|
}
|
|
16319
16729
|
}
|
|
16320
|
-
const parent =
|
|
16730
|
+
const parent = dirname15(current);
|
|
16321
16731
|
if (parent === current) break;
|
|
16322
16732
|
current = parent;
|
|
16323
16733
|
}
|
|
@@ -16342,11 +16752,11 @@ var RenderChunkValidationError = class extends Error {
|
|
|
16342
16752
|
};
|
|
16343
16753
|
function createChunkVideoFrameInjectorFactory(frameLookup, options) {
|
|
16344
16754
|
if (!(frameLookup instanceof Promise))
|
|
16345
|
-
return () =>
|
|
16755
|
+
return () => createVideoFrameInjector3(frameLookup, options);
|
|
16346
16756
|
return () => {
|
|
16347
16757
|
let hook;
|
|
16348
16758
|
return async (page, time, heldVideoTime) => {
|
|
16349
|
-
hook ??=
|
|
16759
|
+
hook ??= createVideoFrameInjector3(await frameLookup, options);
|
|
16350
16760
|
await hook?.(page, time, heldVideoTime);
|
|
16351
16761
|
};
|
|
16352
16762
|
};
|
|
@@ -16360,14 +16770,14 @@ function frameNumberFromFileName(name) {
|
|
|
16360
16770
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
16361
16771
|
const result = [];
|
|
16362
16772
|
for (const v of videos) {
|
|
16363
|
-
const outputDir =
|
|
16364
|
-
if (!
|
|
16773
|
+
const outputDir = join34(planDir, "video-frames", v.videoId);
|
|
16774
|
+
if (!existsSync22(outputDir)) {
|
|
16365
16775
|
throw new Error(
|
|
16366
16776
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
16367
16777
|
);
|
|
16368
16778
|
}
|
|
16369
16779
|
const ext = (extname4(v.framePattern) || ".jpg").toLowerCase();
|
|
16370
|
-
const frames =
|
|
16780
|
+
const frames = readdirSync7(outputDir).filter((name) => name.toLowerCase().endsWith(ext)).sort((left, right) => {
|
|
16371
16781
|
const leftNumber = frameNumberFromFileName(left);
|
|
16372
16782
|
const rightNumber = frameNumberFromFileName(right);
|
|
16373
16783
|
if (leftNumber === null || rightNumber === null) return left.localeCompare(right);
|
|
@@ -16379,7 +16789,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
16379
16789
|
if (!frameName) continue;
|
|
16380
16790
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
16381
16791
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
16382
|
-
framePaths.set(frameIndex,
|
|
16792
|
+
framePaths.set(frameIndex, join34(outputDir, frameName));
|
|
16383
16793
|
}
|
|
16384
16794
|
result.push({
|
|
16385
16795
|
videoId: v.videoId,
|
|
@@ -16396,15 +16806,6 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
16396
16806
|
}
|
|
16397
16807
|
return result;
|
|
16398
16808
|
}
|
|
16399
|
-
function contiguousRuns(sortedIndexes) {
|
|
16400
|
-
const runs2 = [];
|
|
16401
|
-
for (const index of sortedIndexes) {
|
|
16402
|
-
const last = runs2[runs2.length - 1];
|
|
16403
|
-
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
16404
|
-
else runs2.push({ firstFrame: index, frames: 1 });
|
|
16405
|
-
}
|
|
16406
|
-
return runs2;
|
|
16407
|
-
}
|
|
16408
16809
|
function buildChunkCaptureOptions(input) {
|
|
16409
16810
|
const { dimensions, encoder } = input;
|
|
16410
16811
|
const videoCaptureBeyondViewport = resolveVideoCaptureBeyondViewport(input.videoCount);
|
|
@@ -16427,28 +16828,19 @@ function buildChunkCaptureOptions(input) {
|
|
|
16427
16828
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
16428
16829
|
};
|
|
16429
16830
|
}
|
|
16430
|
-
function
|
|
16431
|
-
|
|
16432
|
-
|
|
16433
|
-
|
|
16434
|
-
|
|
16435
|
-
|
|
16436
|
-
|
|
16437
|
-
|
|
16438
|
-
|
|
16439
|
-
|
|
16440
|
-
|
|
16441
|
-
|
|
16831
|
+
function planExtractedFrames(planVideos) {
|
|
16832
|
+
return planVideos.extracted.map((video) => ({
|
|
16833
|
+
...video,
|
|
16834
|
+
outputDir: "",
|
|
16835
|
+
framePaths: /* @__PURE__ */ new Map()
|
|
16836
|
+
}));
|
|
16837
|
+
}
|
|
16838
|
+
function shownPlanFrameIndexes(planVideos, frames, fps) {
|
|
16839
|
+
return shownDeferredFrameIndexes(
|
|
16840
|
+
{ videos: planVideos.videos, extracted: planExtractedFrames(planVideos) },
|
|
16841
|
+
frames,
|
|
16842
|
+
fps
|
|
16442
16843
|
);
|
|
16443
|
-
for (const frame of frames) {
|
|
16444
|
-
const globalTime = frame * fps.den / fps.num;
|
|
16445
|
-
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
16446
|
-
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
16447
|
-
indexes.add(payload.frameIndex);
|
|
16448
|
-
shown.set(videoId, indexes);
|
|
16449
|
-
}
|
|
16450
|
-
}
|
|
16451
|
-
return shown;
|
|
16452
16844
|
}
|
|
16453
16845
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
16454
16846
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
@@ -16465,49 +16857,32 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
16465
16857
|
url: remoteSources[path].split("?")[0]
|
|
16466
16858
|
});
|
|
16467
16859
|
}
|
|
16468
|
-
|
|
16469
|
-
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16860
|
+
const planned = new Map(input.planVideos.extracted.map((video) => [video.videoId, video]));
|
|
16861
|
+
return extractDeferredFrames({
|
|
16862
|
+
videos: input.planVideos.videos,
|
|
16863
|
+
extracted: planExtractedFrames(input.planVideos),
|
|
16864
|
+
frames: input.frames,
|
|
16865
|
+
fps: input.fps,
|
|
16866
|
+
outputRoot: input.outputRoot,
|
|
16867
|
+
cfg: input.cfg,
|
|
16868
|
+
shownBoxes: input.shownBoxes,
|
|
16869
|
+
sourceOf: (video) => {
|
|
16870
|
+
const range = planned.get(video.videoId).deferredRange;
|
|
16478
16871
|
const remoteUrl = remoteSources[range.sourcePath];
|
|
16479
|
-
const sourcePath = remoteUrl ??
|
|
16480
|
-
if (remoteUrl === void 0 && !
|
|
16872
|
+
const sourcePath = remoteUrl ?? join34(input.planDir, ...range.sourcePath.split("/"));
|
|
16873
|
+
if (remoteUrl === void 0 && !existsSync22(sourcePath)) {
|
|
16481
16874
|
throw new RenderChunkValidationError(
|
|
16482
16875
|
MISSING_PLAN_ARTIFACT,
|
|
16483
16876
|
`[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
|
|
16484
16877
|
);
|
|
16485
16878
|
}
|
|
16486
|
-
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
outputDir: input.outputRoot,
|
|
16494
|
-
format: range.format,
|
|
16495
|
-
frameRanges: contiguousRuns(indexes),
|
|
16496
|
-
shownBox: input.shownBoxes?.get(video.videoId)
|
|
16497
|
-
},
|
|
16498
|
-
void 0,
|
|
16499
|
-
input.cfg
|
|
16500
|
-
);
|
|
16501
|
-
const missing = indexes.find((index) => !extracted.framePaths.has(index));
|
|
16502
|
-
if (missing !== void 0) {
|
|
16503
|
-
throw new RenderChunkValidationError(
|
|
16504
|
-
INVALID_VIDEO_METADATA,
|
|
16505
|
-
`[renderChunk] video ${JSON.stringify(video.videoId)} has no frame ${missing}; the plan expected ${video.totalFrames} frames from ${range.sourcePath}.`
|
|
16506
|
-
);
|
|
16507
|
-
}
|
|
16508
|
-
return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
|
|
16509
|
-
})
|
|
16510
|
-
);
|
|
16879
|
+
return sourcePath;
|
|
16880
|
+
},
|
|
16881
|
+
onMissingFrame: (video, index) => new RenderChunkValidationError(
|
|
16882
|
+
INVALID_VIDEO_METADATA,
|
|
16883
|
+
`[renderChunk] video ${JSON.stringify(video.videoId)} has no frame ${index}; the plan expected ${video.totalFrames} frames from ${planned.get(video.videoId).deferredRange.sourcePath}.`
|
|
16884
|
+
)
|
|
16885
|
+
});
|
|
16511
16886
|
}
|
|
16512
16887
|
|
|
16513
16888
|
// src/services/distributed/planFrameCost.ts
|
|
@@ -16525,12 +16900,12 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
16525
16900
|
planVideos,
|
|
16526
16901
|
frames: sampled,
|
|
16527
16902
|
fps: fpsRatio,
|
|
16528
|
-
outputRoot:
|
|
16903
|
+
outputRoot: join35(workDir, "frame-cost-video-frames"),
|
|
16529
16904
|
cfg: input.cfg
|
|
16530
16905
|
})
|
|
16531
16906
|
];
|
|
16532
|
-
const lookup = extracted.length > 0 ?
|
|
16533
|
-
const compiledDir =
|
|
16907
|
+
const lookup = extracted.length > 0 ? createFrameLookupTable4(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
16908
|
+
const compiledDir = join35(planDir, "compiled");
|
|
16534
16909
|
const fileServer = await createFileServer({
|
|
16535
16910
|
projectDir: compiledDir,
|
|
16536
16911
|
compiledDir,
|
|
@@ -16542,7 +16917,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
16542
16917
|
try {
|
|
16543
16918
|
session = await createCaptureSession9(
|
|
16544
16919
|
fileServer.url,
|
|
16545
|
-
|
|
16920
|
+
join35(workDir, "frame-cost"),
|
|
16546
16921
|
buildChunkCaptureOptions({
|
|
16547
16922
|
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
16548
16923
|
encoder: input,
|
|
@@ -16639,7 +17014,7 @@ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
|
|
|
16639
17014
|
}
|
|
16640
17015
|
|
|
16641
17016
|
// src/services/render/planReferenceBackground.ts
|
|
16642
|
-
import { join as
|
|
17017
|
+
import { join as join36 } from "node:path";
|
|
16643
17018
|
import {
|
|
16644
17019
|
closeCaptureSession as closeCaptureSession10,
|
|
16645
17020
|
createCaptureSession as createCaptureSession10,
|
|
@@ -16652,14 +17027,14 @@ async function readPlanReferenceBackground(input) {
|
|
|
16652
17027
|
try {
|
|
16653
17028
|
server = await createFileServer({
|
|
16654
17029
|
projectDir: input.projectDir,
|
|
16655
|
-
compiledDir:
|
|
17030
|
+
compiledDir: join36(input.workDir, "compiled"),
|
|
16656
17031
|
port: 0,
|
|
16657
17032
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
16658
17033
|
fps: input.options.fps
|
|
16659
17034
|
});
|
|
16660
17035
|
session = await createCaptureSession10(
|
|
16661
17036
|
server.url,
|
|
16662
|
-
|
|
17037
|
+
join36(input.workDir, "reference"),
|
|
16663
17038
|
{
|
|
16664
17039
|
...input.options,
|
|
16665
17040
|
requiresWebGpu: false
|
|
@@ -16695,8 +17070,8 @@ async function readPlanReferenceBackground(input) {
|
|
|
16695
17070
|
}
|
|
16696
17071
|
|
|
16697
17072
|
// src/services/render/stages/freezePlan.ts
|
|
16698
|
-
import { existsSync as
|
|
16699
|
-
import { join as
|
|
17073
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
17074
|
+
import { join as join37, relative as relative3, resolve as resolve9 } from "node:path";
|
|
16700
17075
|
import {
|
|
16701
17076
|
REFERENCE_BACKGROUND_FIELD,
|
|
16702
17077
|
referenceBackgroundResult as referenceBackgroundResult3
|
|
@@ -16889,7 +17264,7 @@ function planFileSha256(absolutePath, digests) {
|
|
|
16889
17264
|
return sha256;
|
|
16890
17265
|
}
|
|
16891
17266
|
async function prehashPlanFiles(dir, digests) {
|
|
16892
|
-
if (!
|
|
17267
|
+
if (!existsSync23(dir)) return;
|
|
16893
17268
|
for (const { absolutePath } of listPlanFiles(dir)) {
|
|
16894
17269
|
if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
|
|
16895
17270
|
}
|
|
@@ -16912,9 +17287,9 @@ function listPlanFiles(planDir) {
|
|
|
16912
17287
|
const results = [];
|
|
16913
17288
|
const rootResolved = resolve9(planDir);
|
|
16914
17289
|
function walk(dir) {
|
|
16915
|
-
const entries =
|
|
17290
|
+
const entries = readdirSync8(dir, { withFileTypes: true });
|
|
16916
17291
|
for (const entry of entries) {
|
|
16917
|
-
const full =
|
|
17292
|
+
const full = join37(dir, entry.name);
|
|
16918
17293
|
if (entry.isDirectory()) {
|
|
16919
17294
|
walk(full);
|
|
16920
17295
|
} else if (entry.isFile()) {
|
|
@@ -16955,19 +17330,19 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
|
16955
17330
|
function writePlanRemoteFiles(planDir, files) {
|
|
16956
17331
|
if (files.length === 0) return;
|
|
16957
17332
|
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
16958
|
-
|
|
17333
|
+
writeFileSync8(join37(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
16959
17334
|
}
|
|
16960
17335
|
function readPlanRemoteFiles(planDir) {
|
|
16961
|
-
const path =
|
|
16962
|
-
return
|
|
17336
|
+
const path = join37(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
17337
|
+
return existsSync23(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
|
|
16963
17338
|
}
|
|
16964
17339
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
16965
|
-
const planJsonPath =
|
|
16966
|
-
const encoderJsonPath =
|
|
16967
|
-
if (!
|
|
17340
|
+
const planJsonPath = join37(planDir, "plan.json");
|
|
17341
|
+
const encoderJsonPath = join37(planDir, "meta", "encoder.json");
|
|
17342
|
+
if (!existsSync23(planJsonPath)) {
|
|
16968
17343
|
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
16969
17344
|
}
|
|
16970
|
-
if (!
|
|
17345
|
+
if (!existsSync23(encoderJsonPath)) {
|
|
16971
17346
|
throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
|
|
16972
17347
|
}
|
|
16973
17348
|
const planJson = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
|
|
@@ -16997,21 +17372,21 @@ async function freezePlan(input) {
|
|
|
16997
17372
|
hasAudio,
|
|
16998
17373
|
digests
|
|
16999
17374
|
} = input;
|
|
17000
|
-
if (!
|
|
17375
|
+
if (!existsSync23(planDir)) {
|
|
17001
17376
|
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
17002
17377
|
}
|
|
17003
|
-
const metaDir =
|
|
17004
|
-
if (!
|
|
17005
|
-
|
|
17006
|
-
|
|
17378
|
+
const metaDir = join37(planDir, "meta");
|
|
17379
|
+
if (!existsSync23(metaDir)) mkdirSync14(metaDir, { recursive: true });
|
|
17380
|
+
writeFileSync8(
|
|
17381
|
+
join37(metaDir, "composition.json"),
|
|
17007
17382
|
`${JSON.stringify(composition, null, 2)}
|
|
17008
17383
|
`,
|
|
17009
17384
|
"utf-8"
|
|
17010
17385
|
);
|
|
17011
17386
|
const encoderForCanonical = stripUndefined(encoder);
|
|
17012
17387
|
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
17013
|
-
|
|
17014
|
-
|
|
17388
|
+
writeFileSync8(join37(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
17389
|
+
writeFileSync8(join37(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
17015
17390
|
`, "utf-8");
|
|
17016
17391
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
17017
17392
|
const planHash = computePlanHash({
|
|
@@ -17036,8 +17411,8 @@ async function freezePlan(input) {
|
|
|
17036
17411
|
duration: durationSeconds,
|
|
17037
17412
|
hasAudio
|
|
17038
17413
|
};
|
|
17039
|
-
const planJsonPath =
|
|
17040
|
-
|
|
17414
|
+
const planJsonPath = join37(planDir, "plan.json");
|
|
17415
|
+
writeFileSync8(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
17041
17416
|
`, "utf-8");
|
|
17042
17417
|
return { planJsonPath, planHash };
|
|
17043
17418
|
}
|
|
@@ -17078,9 +17453,9 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
17078
17453
|
}
|
|
17079
17454
|
|
|
17080
17455
|
// src/services/distributed/chunkAudio.ts
|
|
17081
|
-
import { closeSync as closeSync6, existsSync as
|
|
17082
|
-
import { writeFileSync as
|
|
17083
|
-
import { join as
|
|
17456
|
+
import { closeSync as closeSync6, existsSync as existsSync24, ftruncateSync, mkdirSync as mkdirSync15, openSync as openSync6, readFileSync as readFileSync11 } from "node:fs";
|
|
17457
|
+
import { writeFileSync as writeFileSync9, writeSync } from "node:fs";
|
|
17458
|
+
import { join as join38 } from "node:path";
|
|
17084
17459
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure4, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
|
|
17085
17460
|
function audioCutForChunk(grid, chunks, index, fps) {
|
|
17086
17461
|
const firstPacketAt = (frame) => Math.min(
|
|
@@ -17168,7 +17543,7 @@ async function writeChunkAudioSlices(input) {
|
|
|
17168
17543
|
});
|
|
17169
17544
|
const bytes = readFileSync11(input.audioPath);
|
|
17170
17545
|
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
17171
|
-
|
|
17546
|
+
mkdirSync15(join38(input.planDir, SLICE_DIR), { recursive: true });
|
|
17172
17547
|
input.chunks.forEach((_, index) => {
|
|
17173
17548
|
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
17174
17549
|
if (!cut) return;
|
|
@@ -17176,17 +17551,17 @@ async function writeChunkAudioSlices(input) {
|
|
|
17176
17551
|
const start = offsets[cut.firstPacket];
|
|
17177
17552
|
layout.partStarts[index] = start;
|
|
17178
17553
|
const end = offsets[last] + track.sizes[last];
|
|
17179
|
-
|
|
17554
|
+
writeFileSync9(join38(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
17180
17555
|
});
|
|
17181
17556
|
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
17182
|
-
|
|
17183
|
-
|
|
17557
|
+
writeFileSync9(join38(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
17558
|
+
writeFileSync9(join38(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
17184
17559
|
}
|
|
17185
17560
|
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
17186
|
-
const layoutPath =
|
|
17187
|
-
if (!
|
|
17561
|
+
const layoutPath = join38(planDir, SLICE_LAYOUT);
|
|
17562
|
+
if (!existsSync24(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
17188
17563
|
const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
|
|
17189
|
-
const frame = readFileSync11(
|
|
17564
|
+
const frame = readFileSync11(join38(planDir, SLICE_FRAME));
|
|
17190
17565
|
const fd = openSync6(scratchPath, "w");
|
|
17191
17566
|
try {
|
|
17192
17567
|
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
@@ -17199,7 +17574,7 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
|
17199
17574
|
);
|
|
17200
17575
|
const start = layout.partStarts[chunkIndex];
|
|
17201
17576
|
if (start !== void 0) {
|
|
17202
|
-
const part = readFileSync11(
|
|
17577
|
+
const part = readFileSync11(join38(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
17203
17578
|
writeSync(fd, part, 0, part.length, start);
|
|
17204
17579
|
}
|
|
17205
17580
|
ftruncateSync(fd, layout.sizeBytes);
|
|
@@ -17254,8 +17629,8 @@ async function muxChunkWithAudio(input) {
|
|
|
17254
17629
|
|
|
17255
17630
|
// src/services/distributed/planSize.ts
|
|
17256
17631
|
import { createHash as createHash9 } from "node:crypto";
|
|
17257
|
-
import { lstatSync as lstatSync2, readdirSync as
|
|
17258
|
-
import { extname as extname5, join as
|
|
17632
|
+
import { lstatSync as lstatSync2, readdirSync as readdirSync9 } from "node:fs";
|
|
17633
|
+
import { extname as extname5, join as join39, relative as relative4 } from "node:path";
|
|
17259
17634
|
var MAX_TOP_COMPONENTS = 10;
|
|
17260
17635
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
17261
17636
|
".aac",
|
|
@@ -17362,12 +17737,12 @@ function recordFile(result, components, rootDir, rootKind, filePath) {
|
|
|
17362
17737
|
function walkRegularFiles(dir, visit) {
|
|
17363
17738
|
let entries;
|
|
17364
17739
|
try {
|
|
17365
|
-
entries =
|
|
17740
|
+
entries = readdirSync9(dir, { withFileTypes: true });
|
|
17366
17741
|
} catch {
|
|
17367
17742
|
return;
|
|
17368
17743
|
}
|
|
17369
17744
|
for (const entry of entries) {
|
|
17370
|
-
const full =
|
|
17745
|
+
const full = join39(dir, entry.name);
|
|
17371
17746
|
if (entry.isDirectory()) {
|
|
17372
17747
|
walkRegularFiles(full, visit);
|
|
17373
17748
|
} else if (entry.isFile()) {
|
|
@@ -17579,13 +17954,13 @@ function assertPositiveInteger2(name, value) {
|
|
|
17579
17954
|
}
|
|
17580
17955
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
17581
17956
|
"plan.json",
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17957
|
+
join40("meta", "composition.json"),
|
|
17958
|
+
join40("meta", "encoder.json"),
|
|
17959
|
+
join40("meta", "chunks.json")
|
|
17585
17960
|
];
|
|
17586
17961
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
17587
17962
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
17588
|
-
|
|
17963
|
+
rmSync14(join40(planDir, relativePath), { force: true });
|
|
17589
17964
|
}
|
|
17590
17965
|
}
|
|
17591
17966
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -17740,19 +18115,19 @@ function createVideoSourceShipper(input) {
|
|
|
17740
18115
|
const known = shipped.get(srcPath);
|
|
17741
18116
|
if (known) return known;
|
|
17742
18117
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
17743
|
-
const current = stagedRel === null ? srcPath :
|
|
18118
|
+
const current = stagedRel === null ? srcPath : join40(input.planDir, "video-frames", stagedRel);
|
|
17744
18119
|
let result = null;
|
|
17745
18120
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
17746
18121
|
const rel = pathInside(root, srcPath);
|
|
17747
|
-
if (rel !== null && (
|
|
18122
|
+
if (rel !== null && (existsSync25(join40(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
17748
18123
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
17749
18124
|
break;
|
|
17750
18125
|
}
|
|
17751
18126
|
}
|
|
17752
18127
|
if (result === null) {
|
|
17753
18128
|
result = `video-sources/${shipped.size}${extname6(current)}`;
|
|
17754
|
-
const destination =
|
|
17755
|
-
|
|
18129
|
+
const destination = join40(input.planDir, ...result.split("/"));
|
|
18130
|
+
mkdirSync16(join40(input.planDir, "video-sources"), { recursive: true });
|
|
17756
18131
|
try {
|
|
17757
18132
|
linkSync(current, destination);
|
|
17758
18133
|
} catch {
|
|
@@ -17772,7 +18147,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
17772
18147
|
useGpu: false,
|
|
17773
18148
|
browserGpuMode: "software"
|
|
17774
18149
|
});
|
|
17775
|
-
if (!
|
|
18150
|
+
if (!existsSync25(planDir)) mkdirSync16(planDir, { recursive: true });
|
|
17776
18151
|
const log = config.logger ?? defaultLogger;
|
|
17777
18152
|
const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
|
|
17778
18153
|
const abortSignal = config.abortSignal;
|
|
@@ -17801,15 +18176,15 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
17801
18176
|
variables: config.variables
|
|
17802
18177
|
});
|
|
17803
18178
|
const entryFile = config.entryFile ?? "index.html";
|
|
17804
|
-
const htmlPath =
|
|
17805
|
-
if (!
|
|
18179
|
+
const htmlPath = join40(projectDir, entryFile);
|
|
18180
|
+
if (!existsSync25(htmlPath)) {
|
|
17806
18181
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
17807
18182
|
}
|
|
17808
|
-
const workDir =
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
const compiledDir =
|
|
17812
|
-
|
|
18183
|
+
const workDir = join40(planDir, ".plan-work");
|
|
18184
|
+
rmSync14(workDir, { recursive: true, force: true });
|
|
18185
|
+
mkdirSync16(workDir, { recursive: true });
|
|
18186
|
+
const compiledDir = join40(workDir, "compiled");
|
|
18187
|
+
mkdirSync16(compiledDir, { recursive: true });
|
|
17813
18188
|
let stageStarted = Date.now();
|
|
17814
18189
|
cpSync2(projectDir, compiledDir, {
|
|
17815
18190
|
recursive: true,
|
|
@@ -17822,10 +18197,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
17822
18197
|
}
|
|
17823
18198
|
});
|
|
17824
18199
|
logPlanStage(log, "copy", stageStarted);
|
|
17825
|
-
const finalCompiledDir =
|
|
18200
|
+
const finalCompiledDir = join40(planDir, "compiled");
|
|
17826
18201
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
17827
18202
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
17828
|
-
const remoteLocalPath = (rel) =>
|
|
18203
|
+
const remoteLocalPath = (rel) => join40(projectDir, ...rel.split("/"));
|
|
17829
18204
|
for (const [, file] of remoteFiles) {
|
|
17830
18205
|
declareContentAddressedUrl(file.url, file.sha256);
|
|
17831
18206
|
if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
|
|
@@ -17846,7 +18221,6 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
17846
18221
|
urlByAbsentLocalPath,
|
|
17847
18222
|
projectDir,
|
|
17848
18223
|
workDir,
|
|
17849
|
-
htmlPath,
|
|
17850
18224
|
entryFile,
|
|
17851
18225
|
job,
|
|
17852
18226
|
cfg,
|
|
@@ -18029,17 +18403,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
18029
18403
|
let stageStarted = Date.now();
|
|
18030
18404
|
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
18031
18405
|
logPlanStage(log, "extract", stageStarted);
|
|
18032
|
-
const stagedVideoFrames =
|
|
18033
|
-
const videoFramesDst =
|
|
18034
|
-
if (
|
|
18035
|
-
const videoSourcesDir =
|
|
18036
|
-
|
|
18037
|
-
if (
|
|
18406
|
+
const stagedVideoFrames = join40(compiledDir, "__hyperframes_video_frames");
|
|
18407
|
+
const videoFramesDst = join40(planDir, "video-frames");
|
|
18408
|
+
if (existsSync25(videoFramesDst)) rmSync14(videoFramesDst, { recursive: true, force: true });
|
|
18409
|
+
const videoSourcesDir = join40(planDir, "video-sources");
|
|
18410
|
+
rmSync14(videoSourcesDir, { recursive: true, force: true });
|
|
18411
|
+
if (existsSync25(stagedVideoFrames)) {
|
|
18038
18412
|
renameWithRetry8(stagedVideoFrames, videoFramesDst);
|
|
18039
18413
|
} else {
|
|
18040
|
-
|
|
18414
|
+
mkdirSync16(videoFramesDst, { recursive: true });
|
|
18041
18415
|
}
|
|
18042
|
-
if (
|
|
18416
|
+
if (existsSync25(finalCompiledDir)) rmSync14(finalCompiledDir, { recursive: true, force: true });
|
|
18043
18417
|
renameWithRetry8(compiledDir, finalCompiledDir);
|
|
18044
18418
|
const shipVideoSource = createVideoSourceShipper({
|
|
18045
18419
|
planDir,
|
|
@@ -18069,7 +18443,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
18069
18443
|
};
|
|
18070
18444
|
})
|
|
18071
18445
|
});
|
|
18072
|
-
|
|
18446
|
+
mkdirSync16(join40(planDir, "meta"), { recursive: true });
|
|
18073
18447
|
writePlanRemoteFiles(
|
|
18074
18448
|
planDir,
|
|
18075
18449
|
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
@@ -18078,8 +18452,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
18078
18452
|
sizeBytes
|
|
18079
18453
|
}))
|
|
18080
18454
|
);
|
|
18081
|
-
|
|
18082
|
-
|
|
18455
|
+
writeFileSync10(
|
|
18456
|
+
join40(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
18083
18457
|
JSON.stringify(planVideosJson, null, 2),
|
|
18084
18458
|
"utf-8"
|
|
18085
18459
|
);
|
|
@@ -18158,11 +18532,11 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
18158
18532
|
if (audioResult?.audioError) {
|
|
18159
18533
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
18160
18534
|
}
|
|
18161
|
-
const planAudioPath =
|
|
18162
|
-
if (audioResult?.hasAudio &&
|
|
18535
|
+
const planAudioPath = join40(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
18536
|
+
if (audioResult?.hasAudio && existsSync25(audioResult.audioOutputPath)) {
|
|
18163
18537
|
renameWithRetry8(audioResult.audioOutputPath, planAudioPath);
|
|
18164
18538
|
}
|
|
18165
|
-
if (chunksCarryAudio &&
|
|
18539
|
+
if (chunksCarryAudio && existsSync25(planAudioPath)) {
|
|
18166
18540
|
if (options.sliceChunkAudio) {
|
|
18167
18541
|
stageStarted = Date.now();
|
|
18168
18542
|
await writeChunkAudioSlices({
|
|
@@ -18208,7 +18582,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
18208
18582
|
format: config.format
|
|
18209
18583
|
};
|
|
18210
18584
|
try {
|
|
18211
|
-
|
|
18585
|
+
rmSync14(workDir, { recursive: true, force: true });
|
|
18212
18586
|
} catch (err) {
|
|
18213
18587
|
log.warn("[plan] failed to remove temp work dir", {
|
|
18214
18588
|
workDir,
|
|
@@ -18280,9 +18654,9 @@ async function plan(projectDir, config, planDir, options = {}) {
|
|
|
18280
18654
|
|
|
18281
18655
|
// src/services/distributed/renderChunk.ts
|
|
18282
18656
|
import { randomBytes } from "node:crypto";
|
|
18283
|
-
import { existsSync as
|
|
18657
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync13, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
|
|
18284
18658
|
import { cpus as cpus3 } from "node:os";
|
|
18285
|
-
import { join as
|
|
18659
|
+
import { join as join44 } from "node:path";
|
|
18286
18660
|
import {
|
|
18287
18661
|
assertSwiftShader,
|
|
18288
18662
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -18290,7 +18664,7 @@ import {
|
|
|
18290
18664
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
18291
18665
|
closeCaptureSession as closeCaptureSession11,
|
|
18292
18666
|
createCaptureSession as createCaptureSession11,
|
|
18293
|
-
createFrameLookupTable as
|
|
18667
|
+
createFrameLookupTable as createFrameLookupTable6,
|
|
18294
18668
|
deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
|
|
18295
18669
|
getEncoderPreset as getEncoderPreset5,
|
|
18296
18670
|
initializeSession as initializeSession10,
|
|
@@ -18307,34 +18681,34 @@ import {
|
|
|
18307
18681
|
} from "@hyperframes/engine";
|
|
18308
18682
|
import {
|
|
18309
18683
|
copyFileSync as copyFileSync8,
|
|
18310
|
-
existsSync as
|
|
18684
|
+
existsSync as existsSync27,
|
|
18311
18685
|
lstatSync as lstatSync3,
|
|
18312
|
-
mkdirSync as
|
|
18686
|
+
mkdirSync as mkdirSync18,
|
|
18313
18687
|
mkdtempSync as mkdtempSync6,
|
|
18314
18688
|
readFileSync as readFileSync12,
|
|
18315
|
-
readdirSync as
|
|
18316
|
-
rmSync as
|
|
18317
|
-
statSync as
|
|
18318
|
-
writeFileSync as
|
|
18689
|
+
readdirSync as readdirSync10,
|
|
18690
|
+
rmSync as rmSync16,
|
|
18691
|
+
statSync as statSync11,
|
|
18692
|
+
writeFileSync as writeFileSync12
|
|
18319
18693
|
} from "node:fs";
|
|
18320
18694
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
18321
|
-
import { dirname as
|
|
18695
|
+
import { dirname as dirname17, isAbsolute as isAbsolute4, join as join43, relative as relative6, resolve as resolve10, sep as sep3 } from "node:path";
|
|
18322
18696
|
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
18323
|
-
import { createFrameLookupTable as
|
|
18697
|
+
import { createFrameLookupTable as createFrameLookupTable5, renameWithRetry as renameWithRetry10 } from "@hyperframes/engine";
|
|
18324
18698
|
|
|
18325
18699
|
// src/services/distributed/planV2Publisher.ts
|
|
18326
18700
|
import {
|
|
18327
18701
|
copyFileSync as copyFileSync7,
|
|
18328
|
-
existsSync as
|
|
18702
|
+
existsSync as existsSync26,
|
|
18329
18703
|
linkSync as linkSync2,
|
|
18330
|
-
mkdirSync as
|
|
18704
|
+
mkdirSync as mkdirSync17,
|
|
18331
18705
|
mkdtempSync as mkdtempSync5,
|
|
18332
|
-
rmSync as
|
|
18333
|
-
statSync as
|
|
18334
|
-
writeFileSync as
|
|
18706
|
+
rmSync as rmSync15,
|
|
18707
|
+
statSync as statSync10,
|
|
18708
|
+
writeFileSync as writeFileSync11
|
|
18335
18709
|
} from "node:fs";
|
|
18336
18710
|
import { renameWithRetry as renameWithRetry9 } from "@hyperframes/engine";
|
|
18337
|
-
import { dirname as
|
|
18711
|
+
import { dirname as dirname16, join as join42 } from "node:path";
|
|
18338
18712
|
|
|
18339
18713
|
// src/services/distributed/planV2Errors.ts
|
|
18340
18714
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -18349,7 +18723,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
18349
18723
|
};
|
|
18350
18724
|
|
|
18351
18725
|
// src/services/distributed/planV2Layout.ts
|
|
18352
|
-
import { join as
|
|
18726
|
+
import { join as join41 } from "node:path";
|
|
18353
18727
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
18354
18728
|
function assertPlanV2Sha256(value, field) {
|
|
18355
18729
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -18359,7 +18733,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
18359
18733
|
}
|
|
18360
18734
|
function planV2BlobPath(root, sha256) {
|
|
18361
18735
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
18362
|
-
return
|
|
18736
|
+
return join41(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
18363
18737
|
}
|
|
18364
18738
|
|
|
18365
18739
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -18394,27 +18768,27 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18394
18768
|
#linkFile;
|
|
18395
18769
|
#committed = false;
|
|
18396
18770
|
constructor(destinationDir, options = {}) {
|
|
18397
|
-
if (
|
|
18771
|
+
if (existsSync26(destinationDir)) {
|
|
18398
18772
|
throw new PlanV2IntegrityError(`output directory already exists: ${destinationDir}`);
|
|
18399
18773
|
}
|
|
18400
18774
|
this.destinationDir = destinationDir;
|
|
18401
18775
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
18402
|
-
|
|
18403
|
-
this.temporaryDir = mkdtempSync5(
|
|
18776
|
+
mkdirSync17(dirname16(destinationDir), { recursive: true });
|
|
18777
|
+
this.temporaryDir = mkdtempSync5(join42(dirname16(destinationDir), ".plan-v2-publish-"));
|
|
18404
18778
|
}
|
|
18405
18779
|
async putBlob(blob) {
|
|
18406
18780
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
18407
|
-
const sourceSize =
|
|
18781
|
+
const sourceSize = statSync10(blob.sourcePath).size;
|
|
18408
18782
|
if (sourceSize !== blob.sizeBytes) {
|
|
18409
18783
|
throw new PlanV2IntegrityError(
|
|
18410
18784
|
`published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
|
|
18411
18785
|
);
|
|
18412
18786
|
}
|
|
18413
18787
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
18414
|
-
if (
|
|
18415
|
-
|
|
18416
|
-
const stagingDir = mkdtempSync5(
|
|
18417
|
-
const temporaryPath =
|
|
18788
|
+
if (existsSync26(destinationPath)) return;
|
|
18789
|
+
mkdirSync17(dirname16(destinationPath), { recursive: true });
|
|
18790
|
+
const stagingDir = mkdtempSync5(join42(dirname16(destinationPath), ".plan-v2-blob-"));
|
|
18791
|
+
const temporaryPath = join42(stagingDir, "blob");
|
|
18418
18792
|
try {
|
|
18419
18793
|
try {
|
|
18420
18794
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
@@ -18424,24 +18798,24 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
18424
18798
|
}
|
|
18425
18799
|
renameWithRetry9(temporaryPath, destinationPath);
|
|
18426
18800
|
} finally {
|
|
18427
|
-
|
|
18801
|
+
rmSync15(stagingDir, { recursive: true, force: true });
|
|
18428
18802
|
}
|
|
18429
18803
|
}
|
|
18430
18804
|
async commitManifest(manifestBytes) {
|
|
18431
18805
|
for (const digest of new Set(manifestDigests(manifestBytes))) {
|
|
18432
|
-
if (!
|
|
18806
|
+
if (!existsSync26(planV2BlobPath(this.temporaryDir, digest))) {
|
|
18433
18807
|
throw new PlanV2IntegrityError(
|
|
18434
18808
|
`cannot commit manifest before referenced blob is durable: ${digest}`
|
|
18435
18809
|
);
|
|
18436
18810
|
}
|
|
18437
18811
|
}
|
|
18438
|
-
|
|
18812
|
+
writeFileSync11(join42(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
18439
18813
|
renameWithRetry9(this.temporaryDir, this.destinationDir);
|
|
18440
18814
|
this.#committed = true;
|
|
18441
18815
|
}
|
|
18442
18816
|
async abort() {
|
|
18443
18817
|
if (!this.#committed) {
|
|
18444
|
-
|
|
18818
|
+
rmSync15(this.temporaryDir, { recursive: true, force: true });
|
|
18445
18819
|
}
|
|
18446
18820
|
}
|
|
18447
18821
|
};
|
|
@@ -18479,8 +18853,8 @@ function listFiles(root) {
|
|
|
18479
18853
|
const files = [];
|
|
18480
18854
|
const rootResolved = resolve10(root);
|
|
18481
18855
|
function walk(dir) {
|
|
18482
|
-
for (const entry of
|
|
18483
|
-
const absolutePath =
|
|
18856
|
+
for (const entry of readdirSync10(dir, { withFileTypes: true })) {
|
|
18857
|
+
const absolutePath = join43(dir, entry.name);
|
|
18484
18858
|
if (entry.isDirectory()) {
|
|
18485
18859
|
walk(absolutePath);
|
|
18486
18860
|
} else if (entry.isFile()) {
|
|
@@ -18507,13 +18881,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
18507
18881
|
}
|
|
18508
18882
|
}
|
|
18509
18883
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
18510
|
-
const videoRoot =
|
|
18511
|
-
if (!
|
|
18512
|
-
for (const videoEntry of
|
|
18884
|
+
const videoRoot = join43(executionPlanDir, "video-frames");
|
|
18885
|
+
if (!existsSync27(videoRoot)) return;
|
|
18886
|
+
for (const videoEntry of readdirSync10(videoRoot, { withFileTypes: true })) {
|
|
18513
18887
|
if (!videoEntry.isDirectory()) continue;
|
|
18514
|
-
const videoDir =
|
|
18515
|
-
if (
|
|
18516
|
-
const sentinelPath =
|
|
18888
|
+
const videoDir = join43(videoRoot, videoEntry.name);
|
|
18889
|
+
if (readdirSync10(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
18890
|
+
const sentinelPath = join43(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
18517
18891
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
18518
18892
|
}
|
|
18519
18893
|
}
|
|
@@ -18560,15 +18934,15 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
18560
18934
|
if (video.deferredRange) {
|
|
18561
18935
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
18562
18936
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
18563
|
-
framePaths2.set(index,
|
|
18937
|
+
framePaths2.set(index, join43(outputDir, String(index)));
|
|
18564
18938
|
}
|
|
18565
18939
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
18566
18940
|
}
|
|
18567
|
-
const frameNames =
|
|
18941
|
+
const frameNames = readdirSync10(outputDir).sort();
|
|
18568
18942
|
const framePaths = /* @__PURE__ */ new Map();
|
|
18569
18943
|
for (const frameName of frameNames) {
|
|
18570
18944
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
18571
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
18945
|
+
assertValidExtractionCacheCompleteSentinel(join43(outputDir, frameName));
|
|
18572
18946
|
continue;
|
|
18573
18947
|
}
|
|
18574
18948
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -18587,7 +18961,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
18587
18961
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
18588
18962
|
);
|
|
18589
18963
|
}
|
|
18590
|
-
framePaths.set(frameIndex,
|
|
18964
|
+
framePaths.set(frameIndex, join43(outputDir, frameName));
|
|
18591
18965
|
}
|
|
18592
18966
|
return {
|
|
18593
18967
|
...video,
|
|
@@ -18608,11 +18982,11 @@ function parsePlanVideosJson2(value) {
|
|
|
18608
18982
|
}
|
|
18609
18983
|
}
|
|
18610
18984
|
function materializeExtractedVideoDirectories(planDir) {
|
|
18611
|
-
const videosPath =
|
|
18612
|
-
if (!
|
|
18985
|
+
const videosPath = join43(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18986
|
+
if (!existsSync27(videosPath)) return;
|
|
18613
18987
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
18614
18988
|
for (const video of videos.extracted) {
|
|
18615
|
-
|
|
18989
|
+
mkdirSync18(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
|
|
18616
18990
|
}
|
|
18617
18991
|
}
|
|
18618
18992
|
function parseChunkSlices(value) {
|
|
@@ -18642,13 +19016,13 @@ function parseChunkSlices(value) {
|
|
|
18642
19016
|
});
|
|
18643
19017
|
}
|
|
18644
19018
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
18645
|
-
const videoRoot =
|
|
18646
|
-
const hasExtractedFrames =
|
|
18647
|
-
const videosPath =
|
|
18648
|
-
if (!
|
|
19019
|
+
const videoRoot = join43(executionPlanDir, "video-frames");
|
|
19020
|
+
const hasExtractedFrames = existsSync27(videoRoot) && listFiles(videoRoot).length > 0;
|
|
19021
|
+
const videosPath = join43(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
19022
|
+
if (!existsSync27(videosPath)) {
|
|
18649
19023
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
18650
19024
|
}
|
|
18651
|
-
const chunksPath =
|
|
19025
|
+
const chunksPath = join43(executionPlanDir, "meta", "chunks.json");
|
|
18652
19026
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18653
19027
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
18654
19028
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -18656,7 +19030,7 @@ function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
|
18656
19030
|
const extracted = listVideoFramePaths(executionPlanDir, parsedVideos);
|
|
18657
19031
|
const fpsNum = readPositiveInteger2(dimensions.fpsNum, "dimensions.fpsNum");
|
|
18658
19032
|
const fpsDen = readPositiveInteger2(dimensions.fpsDen, "dimensions.fpsDen");
|
|
18659
|
-
const table =
|
|
19033
|
+
const table = createFrameLookupTable5(
|
|
18660
19034
|
parsedVideos.videos,
|
|
18661
19035
|
extracted,
|
|
18662
19036
|
resolveRenderFpsConfig({ num: fpsNum, den: fpsDen }).value
|
|
@@ -18702,22 +19076,22 @@ function computeManifestHash(manifest) {
|
|
|
18702
19076
|
return sha256Hex(`${PLAN_V2_HASH_PREFIX}${canonicalJsonStringify(manifestPayload(manifest))}`);
|
|
18703
19077
|
}
|
|
18704
19078
|
function writeBlob(sourcePath, destinationPath) {
|
|
18705
|
-
if (
|
|
18706
|
-
|
|
18707
|
-
const temporaryDir = mkdtempSync6(
|
|
18708
|
-
const temporaryPath =
|
|
19079
|
+
if (existsSync27(destinationPath)) return;
|
|
19080
|
+
mkdirSync18(dirname17(destinationPath), { recursive: true });
|
|
19081
|
+
const temporaryDir = mkdtempSync6(join43(dirname17(destinationPath), ".plan-v2-blob-"));
|
|
19082
|
+
const temporaryPath = join43(temporaryDir, "blob");
|
|
18709
19083
|
try {
|
|
18710
19084
|
copyFileSync8(sourcePath, temporaryPath);
|
|
18711
19085
|
renameWithRetry10(temporaryPath, destinationPath);
|
|
18712
19086
|
} finally {
|
|
18713
|
-
|
|
19087
|
+
rmSync16(temporaryDir, { recursive: true, force: true });
|
|
18714
19088
|
}
|
|
18715
19089
|
}
|
|
18716
19090
|
function publishBlobFor(absolutePath, digests) {
|
|
18717
19091
|
return {
|
|
18718
19092
|
sourcePath: absolutePath,
|
|
18719
19093
|
sha256: planFileSha256(absolutePath, digests),
|
|
18720
|
-
sizeBytes:
|
|
19094
|
+
sizeBytes: statSync11(absolutePath).size
|
|
18721
19095
|
};
|
|
18722
19096
|
}
|
|
18723
19097
|
async function putBlobs(publisher, blobs) {
|
|
@@ -18741,8 +19115,8 @@ async function putTasks(tasks) {
|
|
|
18741
19115
|
if (failure) throw failure.reason;
|
|
18742
19116
|
}
|
|
18743
19117
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
18744
|
-
const executionPlanPath =
|
|
18745
|
-
if (!
|
|
19118
|
+
const executionPlanPath = join43(executionPlanDir, "plan.json");
|
|
19119
|
+
if (!existsSync27(executionPlanPath)) {
|
|
18746
19120
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
18747
19121
|
}
|
|
18748
19122
|
const executionPlanValue = readJsonFile(executionPlanPath, "execution plan.json");
|
|
@@ -18769,7 +19143,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
18769
19143
|
}
|
|
18770
19144
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
18771
19145
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
18772
|
-
const encoder = readJsonFile(
|
|
19146
|
+
const encoder = readJsonFile(join43(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
18773
19147
|
const files = listFiles(executionPlanDir);
|
|
18774
19148
|
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
18775
19149
|
for (const file of files) {
|
|
@@ -18822,28 +19196,28 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
18822
19196
|
};
|
|
18823
19197
|
}
|
|
18824
19198
|
function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
18825
|
-
if (
|
|
19199
|
+
if (existsSync27(planV2Dir)) {
|
|
18826
19200
|
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
18827
19201
|
}
|
|
18828
19202
|
const publication = buildPlanV2Publication(executionPlanDir);
|
|
18829
19203
|
if (publication.storedBlobs.length > 0) {
|
|
18830
19204
|
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
18831
19205
|
}
|
|
18832
|
-
|
|
18833
|
-
const tempDir = mkdtempSync6(
|
|
19206
|
+
mkdirSync18(dirname17(planV2Dir), { recursive: true });
|
|
19207
|
+
const tempDir = mkdtempSync6(join43(dirname17(planV2Dir), ".plan-v2-build-"));
|
|
18834
19208
|
try {
|
|
18835
19209
|
for (const blob of publication.blobs) {
|
|
18836
19210
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
18837
19211
|
}
|
|
18838
|
-
|
|
18839
|
-
|
|
19212
|
+
writeFileSync12(
|
|
19213
|
+
join43(tempDir, "plan.json"),
|
|
18840
19214
|
canonicalJsonStringify(publication.manifest),
|
|
18841
19215
|
"utf-8"
|
|
18842
19216
|
);
|
|
18843
19217
|
renameWithRetry10(tempDir, planV2Dir);
|
|
18844
19218
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
18845
19219
|
} catch (error) {
|
|
18846
|
-
|
|
19220
|
+
rmSync16(tempDir, { recursive: true, force: true });
|
|
18847
19221
|
throw error;
|
|
18848
19222
|
}
|
|
18849
19223
|
}
|
|
@@ -18877,7 +19251,7 @@ async function publishExecutionPlan(executionPlanDir, publisher, { digests, log
|
|
|
18877
19251
|
}
|
|
18878
19252
|
}
|
|
18879
19253
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
18880
|
-
const stagingRoot = mkdtempSync6(
|
|
19254
|
+
const stagingRoot = mkdtempSync6(join43(options.stagingParentDir ?? tmpdir3(), ".execution-plan-"));
|
|
18881
19255
|
const digests = /* @__PURE__ */ new Map();
|
|
18882
19256
|
try {
|
|
18883
19257
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -18888,7 +19262,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
18888
19262
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
18889
19263
|
whileAudioMixes: () => putBlobs(
|
|
18890
19264
|
publisher,
|
|
18891
|
-
listFiles(
|
|
19265
|
+
listFiles(join43(stagingRoot, "compiled")).map(
|
|
18892
19266
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
18893
19267
|
)
|
|
18894
19268
|
),
|
|
@@ -18904,16 +19278,16 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
18904
19278
|
}
|
|
18905
19279
|
throw error;
|
|
18906
19280
|
} finally {
|
|
18907
|
-
|
|
19281
|
+
rmSync16(stagingRoot, { recursive: true, force: true });
|
|
18908
19282
|
}
|
|
18909
19283
|
}
|
|
18910
19284
|
async function planV2(projectDir, config, planV2Dir) {
|
|
18911
|
-
if (
|
|
19285
|
+
if (existsSync27(planV2Dir)) {
|
|
18912
19286
|
throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
|
|
18913
19287
|
}
|
|
18914
19288
|
const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
|
|
18915
19289
|
const manifest = await planV2WithPublisher(projectDir, config, publisher, {
|
|
18916
|
-
stagingParentDir:
|
|
19290
|
+
stagingParentDir: dirname17(planV2Dir)
|
|
18917
19291
|
});
|
|
18918
19292
|
return resultFromManifest(planV2Dir, manifest);
|
|
18919
19293
|
}
|
|
@@ -18925,7 +19299,7 @@ function resultFromManifest(planV2Dir, manifest) {
|
|
|
18925
19299
|
isRecord3(executionPlan) ? executionPlan[REFERENCE_BACKGROUND_FIELD3] : void 0
|
|
18926
19300
|
),
|
|
18927
19301
|
planDir: planV2Dir,
|
|
18928
|
-
manifestPath:
|
|
19302
|
+
manifestPath: join43(planV2Dir, "plan.json"),
|
|
18929
19303
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
18930
19304
|
planHash: manifest.planHash,
|
|
18931
19305
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -19061,8 +19435,8 @@ function parsePlanV2Manifest(value) {
|
|
|
19061
19435
|
return parsed;
|
|
19062
19436
|
}
|
|
19063
19437
|
function readPlanV2Manifest(planV2Dir) {
|
|
19064
|
-
const manifestPath =
|
|
19065
|
-
if (!
|
|
19438
|
+
const manifestPath = join43(planV2Dir, "plan.json");
|
|
19439
|
+
if (!existsSync27(manifestPath)) {
|
|
19066
19440
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
19067
19441
|
}
|
|
19068
19442
|
const value = readJsonFile(manifestPath, "v2 manifest");
|
|
@@ -19081,10 +19455,10 @@ function listPlanV2ArtifactsForTarget(manifest, target) {
|
|
|
19081
19455
|
}
|
|
19082
19456
|
function verifyBlob(planV2Dir, artifact) {
|
|
19083
19457
|
const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
|
|
19084
|
-
if (!
|
|
19458
|
+
if (!existsSync27(sourcePath)) {
|
|
19085
19459
|
throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
|
|
19086
19460
|
}
|
|
19087
|
-
const stats =
|
|
19461
|
+
const stats = statSync11(sourcePath);
|
|
19088
19462
|
if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
|
|
19089
19463
|
throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
|
|
19090
19464
|
}
|
|
@@ -19094,7 +19468,7 @@ function verifyBlob(planV2Dir, artifact) {
|
|
|
19094
19468
|
return sourcePath;
|
|
19095
19469
|
}
|
|
19096
19470
|
function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}) {
|
|
19097
|
-
if (
|
|
19471
|
+
if (existsSync27(destinationDir)) {
|
|
19098
19472
|
throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
|
|
19099
19473
|
}
|
|
19100
19474
|
const manifest = readPlanV2Manifest(planV2Dir);
|
|
@@ -19106,17 +19480,17 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19106
19480
|
artifact,
|
|
19107
19481
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
19108
19482
|
}));
|
|
19109
|
-
|
|
19110
|
-
const tempDir = mkdtempSync6(
|
|
19483
|
+
mkdirSync18(dirname17(destinationDir), { recursive: true });
|
|
19484
|
+
const tempDir = mkdtempSync6(join43(dirname17(destinationDir), ".plan-v2-materialize-"));
|
|
19111
19485
|
try {
|
|
19112
19486
|
for (const { artifact, sourcePath } of verified) {
|
|
19113
|
-
const destinationPath =
|
|
19114
|
-
|
|
19487
|
+
const destinationPath = join43(tempDir, ...artifact.path.split("/"));
|
|
19488
|
+
mkdirSync18(dirname17(destinationPath), { recursive: true });
|
|
19115
19489
|
copyFileSync8(sourcePath, destinationPath);
|
|
19116
19490
|
}
|
|
19117
19491
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
19118
|
-
|
|
19119
|
-
|
|
19492
|
+
writeFileSync12(
|
|
19493
|
+
join43(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
19120
19494
|
canonicalJsonStringify({
|
|
19121
19495
|
manifest,
|
|
19122
19496
|
target,
|
|
@@ -19126,7 +19500,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19126
19500
|
);
|
|
19127
19501
|
renameWithRetry10(tempDir, destinationDir);
|
|
19128
19502
|
} catch (error) {
|
|
19129
|
-
|
|
19503
|
+
rmSync16(tempDir, { recursive: true, force: true });
|
|
19130
19504
|
throw error;
|
|
19131
19505
|
}
|
|
19132
19506
|
return {
|
|
@@ -19139,7 +19513,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
19139
19513
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
19140
19514
|
// before the container change carries the legacy name, and materializing it
|
|
19141
19515
|
// under the new one would point at a file that was never written.
|
|
19142
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
19516
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join43(destinationDir, audioArtifact.path) : null : null
|
|
19143
19517
|
};
|
|
19144
19518
|
}
|
|
19145
19519
|
function assertRemoteSources(remoteSources, artifacts) {
|
|
@@ -19158,8 +19532,8 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
19158
19532
|
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
19159
19533
|
}
|
|
19160
19534
|
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
19161
|
-
const markerPath =
|
|
19162
|
-
if (!
|
|
19535
|
+
const markerPath = join43(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
19536
|
+
if (!existsSync27(markerPath)) return null;
|
|
19163
19537
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
19164
19538
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
19165
19539
|
throw new PlanV2IntegrityError("malformed materialization marker");
|
|
@@ -19176,8 +19550,8 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
19176
19550
|
assertRemoteSources(remoteSources, required);
|
|
19177
19551
|
for (const artifact of required) {
|
|
19178
19552
|
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
19179
|
-
const path =
|
|
19180
|
-
if (!
|
|
19553
|
+
const path = join43(planDir, ...artifact.path.split("/"));
|
|
19554
|
+
if (!existsSync27(path) || statSync11(path).size !== artifact.sizeBytes) {
|
|
19181
19555
|
throw new PlanV2IntegrityError(
|
|
19182
19556
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
19183
19557
|
);
|
|
@@ -19295,9 +19669,9 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
|
|
|
19295
19669
|
}
|
|
19296
19670
|
function hashChunkOutput(outputPath, kind) {
|
|
19297
19671
|
if (kind === "file") return sha256Hex(readFileSync13(outputPath));
|
|
19298
|
-
const entries =
|
|
19672
|
+
const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
19299
19673
|
const lines = entries.map(
|
|
19300
|
-
(name) => `${name}\0${sha256Hex(readFileSync13(
|
|
19674
|
+
(name) => `${name}\0${sha256Hex(readFileSync13(join44(outputPath, name)))}`
|
|
19301
19675
|
);
|
|
19302
19676
|
return sha256Hex(lines.join("\0"));
|
|
19303
19677
|
}
|
|
@@ -19314,10 +19688,10 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
19314
19688
|
async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
19315
19689
|
const start = Date.now();
|
|
19316
19690
|
const log = defaultLogger;
|
|
19317
|
-
const planJsonPath =
|
|
19318
|
-
const encoderJsonPath =
|
|
19319
|
-
const chunksJsonPath =
|
|
19320
|
-
if (!
|
|
19691
|
+
const planJsonPath = join44(planDir, "plan.json");
|
|
19692
|
+
const encoderJsonPath = join44(planDir, "meta", "encoder.json");
|
|
19693
|
+
const chunksJsonPath = join44(planDir, "meta", "chunks.json");
|
|
19694
|
+
if (!existsSync28(planJsonPath)) {
|
|
19321
19695
|
throw new RenderChunkValidationError(
|
|
19322
19696
|
MISSING_PLAN_ARTIFACT,
|
|
19323
19697
|
`[renderChunk] planDir is missing required artifact: ${planJsonPath}`
|
|
@@ -19333,7 +19707,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19333
19707
|
const v2Manifest = v2Materialization?.manifest ?? null;
|
|
19334
19708
|
const remoteSources = v2Materialization?.remoteSources ?? {};
|
|
19335
19709
|
for (const required of [encoderJsonPath, chunksJsonPath]) {
|
|
19336
|
-
if (!
|
|
19710
|
+
if (!existsSync28(required)) {
|
|
19337
19711
|
throw new RenderChunkValidationError(
|
|
19338
19712
|
MISSING_PLAN_ARTIFACT,
|
|
19339
19713
|
`[renderChunk] planDir is missing required artifact: ${required}`
|
|
@@ -19342,9 +19716,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19342
19716
|
}
|
|
19343
19717
|
const encoder = JSON.parse(readFileSync13(encoderJsonPath, "utf-8"));
|
|
19344
19718
|
const chunks = JSON.parse(readFileSync13(chunksJsonPath, "utf-8"));
|
|
19345
|
-
const videosJsonPath =
|
|
19719
|
+
const videosJsonPath = join44(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
19346
19720
|
let planVideos = null;
|
|
19347
|
-
if (
|
|
19721
|
+
if (existsSync28(videosJsonPath)) {
|
|
19348
19722
|
try {
|
|
19349
19723
|
planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync13(videosJsonPath, "utf-8")));
|
|
19350
19724
|
} catch (err) {
|
|
@@ -19374,8 +19748,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19374
19748
|
}
|
|
19375
19749
|
const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
|
|
19376
19750
|
const framesInChunk = range.endFrame - range.startFrame;
|
|
19377
|
-
const compiledDir =
|
|
19378
|
-
if (!
|
|
19751
|
+
const compiledDir = join44(planDir, "compiled");
|
|
19752
|
+
if (!existsSync28(compiledDir)) {
|
|
19379
19753
|
throw new RenderChunkValidationError(
|
|
19380
19754
|
MISSING_PLAN_ARTIFACT,
|
|
19381
19755
|
`[renderChunk] planDir missing compiled/ directory: ${compiledDir}`
|
|
@@ -19431,14 +19805,14 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19431
19805
|
forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
|
|
19432
19806
|
};
|
|
19433
19807
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
19434
|
-
|
|
19435
|
-
const framesDir =
|
|
19436
|
-
|
|
19808
|
+
mkdirSync19(workDir, { recursive: true });
|
|
19809
|
+
const framesDir = join44(workDir, "captured-frames");
|
|
19810
|
+
mkdirSync19(framesDir, { recursive: true });
|
|
19437
19811
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
19438
19812
|
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
19439
19813
|
const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
19440
19814
|
const deferredVideos = planVideos?.extracted.filter((video) => video.deferredRange) ?? [];
|
|
19441
|
-
const shownFrames = planVideos ?
|
|
19815
|
+
const shownFrames = planVideos ? shownPlanFrameIndexes(planVideos, chunkFrameNumbers, chunkFps) : /* @__PURE__ */ new Map();
|
|
19442
19816
|
const remoteVideoIds = new Set(
|
|
19443
19817
|
planVideos?.extracted.filter(
|
|
19444
19818
|
(video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
|
|
@@ -19457,7 +19831,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19457
19831
|
dimensions: plan2.dimensions,
|
|
19458
19832
|
encoder,
|
|
19459
19833
|
videoCount: planVideos?.videos.length ?? 0,
|
|
19460
|
-
html: readFileSync13(
|
|
19834
|
+
html: readFileSync13(join44(compiledDir, "index.html"), "utf-8"),
|
|
19461
19835
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
19462
19836
|
skipReadinessVideoIds: deferredVideos.filter(
|
|
19463
19837
|
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
@@ -19592,7 +19966,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19592
19966
|
deferredVideoFrames.catch(() => {
|
|
19593
19967
|
});
|
|
19594
19968
|
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
|
|
19595
|
-
(deferred) =>
|
|
19969
|
+
(deferred) => createFrameLookupTable6(
|
|
19596
19970
|
planVideos.videos,
|
|
19597
19971
|
[
|
|
19598
19972
|
...rebuildExtractedFramesFromPlanDir(
|
|
@@ -19625,10 +19999,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19625
19999
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
19626
20000
|
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
19627
20001
|
if (isPngSequence) {
|
|
19628
|
-
if (!
|
|
20002
|
+
if (!existsSync28(outputChunkPath)) mkdirSync19(outputChunkPath, { recursive: true });
|
|
19629
20003
|
} else {
|
|
19630
|
-
const outDir =
|
|
19631
|
-
if (!
|
|
20004
|
+
const outDir = join44(outputChunkPath, "..");
|
|
20005
|
+
if (!existsSync28(outDir)) mkdirSync19(outDir, { recursive: true });
|
|
19632
20006
|
}
|
|
19633
20007
|
const encodeInput = {
|
|
19634
20008
|
job,
|
|
@@ -19744,9 +20118,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19744
20118
|
return observedChunkCaptureMode(capturePerfs);
|
|
19745
20119
|
},
|
|
19746
20120
|
resetForScreenshotRetry: () => {
|
|
19747
|
-
|
|
20121
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
19748
20122
|
boxesMeasured();
|
|
19749
|
-
|
|
20123
|
+
mkdirSync19(framesDir, { recursive: true });
|
|
19750
20124
|
capturePerfs.length = 0;
|
|
19751
20125
|
captureAttempts.length = 0;
|
|
19752
20126
|
job.framesRendered = 0;
|
|
@@ -19790,8 +20164,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19790
20164
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
19791
20165
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
19792
20166
|
});
|
|
19793
|
-
|
|
19794
|
-
|
|
20167
|
+
rmSync17(videoOnlyPath, { force: true });
|
|
20168
|
+
rmSync17(audioScratchPath, { force: true });
|
|
19795
20169
|
}
|
|
19796
20170
|
encodeStageMs = Date.now() - encodeStarted;
|
|
19797
20171
|
} finally {
|
|
@@ -19838,11 +20212,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19838
20212
|
producerVersion: plan2.producerVersion,
|
|
19839
20213
|
ffmpegVersion
|
|
19840
20214
|
};
|
|
19841
|
-
|
|
20215
|
+
writeFileSync13(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
19842
20216
|
`, "utf-8");
|
|
19843
20217
|
try {
|
|
19844
|
-
|
|
19845
|
-
|
|
20218
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
20219
|
+
rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
19846
20220
|
} catch (err) {
|
|
19847
20221
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
19848
20222
|
workDir,
|
|
@@ -19901,11 +20275,11 @@ async function renderSpan(projectDir, config, span, outputPath) {
|
|
|
19901
20275
|
if (config.cfr === true) {
|
|
19902
20276
|
throw new RenderSpanUnsupportedError("--start/--end cannot be combined with a cfr re-encode.");
|
|
19903
20277
|
}
|
|
19904
|
-
const workDir = mkdtempSync7(
|
|
20278
|
+
const workDir = mkdtempSync7(join45(tmpdir4(), "hf-render-span-"));
|
|
19905
20279
|
try {
|
|
19906
|
-
const planDir =
|
|
19907
|
-
const partsDir =
|
|
19908
|
-
|
|
20280
|
+
const planDir = join45(workDir, "plan");
|
|
20281
|
+
const partsDir = join45(workDir, "parts");
|
|
20282
|
+
mkdirSync20(partsDir, { recursive: true });
|
|
19909
20283
|
const planned = await plan(
|
|
19910
20284
|
projectDir,
|
|
19911
20285
|
{ ...config, rejectOnSystemFonts: false, failClosedFontFetch: false },
|
|
@@ -19919,7 +20293,7 @@ async function renderSpan(projectDir, config, span, outputPath) {
|
|
|
19919
20293
|
planned.totalFrames
|
|
19920
20294
|
);
|
|
19921
20295
|
const slices = JSON.parse(
|
|
19922
|
-
readFileSync14(
|
|
20296
|
+
readFileSync14(join45(planDir, "meta", "chunks.json"), "utf-8")
|
|
19923
20297
|
);
|
|
19924
20298
|
const parts = [];
|
|
19925
20299
|
for (const slice of slices) {
|
|
@@ -19929,7 +20303,7 @@ async function renderSpan(projectDir, config, span, outputPath) {
|
|
|
19929
20303
|
};
|
|
19930
20304
|
if (range.endFrame <= range.startFrame) continue;
|
|
19931
20305
|
throwIfAborted(config.abortSignal);
|
|
19932
|
-
const part =
|
|
20306
|
+
const part = join45(partsDir, `part-${String(parts.length).padStart(4, "0")}.mp4`);
|
|
19933
20307
|
await renderChunk(planDir, slice.index, part, { frameRange: range });
|
|
19934
20308
|
parts.push(part);
|
|
19935
20309
|
}
|
|
@@ -19942,7 +20316,7 @@ async function renderSpan(projectDir, config, span, outputPath) {
|
|
|
19942
20316
|
framesEncoded: endFrame - startFrame
|
|
19943
20317
|
};
|
|
19944
20318
|
} finally {
|
|
19945
|
-
|
|
20319
|
+
rmSync18(workDir, { recursive: true, force: true });
|
|
19946
20320
|
}
|
|
19947
20321
|
}
|
|
19948
20322
|
|
|
@@ -20518,7 +20892,7 @@ import {
|
|
|
20518
20892
|
} from "@hyperframes/engine";
|
|
20519
20893
|
|
|
20520
20894
|
// src/index.ts
|
|
20521
|
-
import { createVideoFrameInjector as
|
|
20895
|
+
import { createVideoFrameInjector as createVideoFrameInjector4 } from "@hyperframes/engine";
|
|
20522
20896
|
|
|
20523
20897
|
// src/config.ts
|
|
20524
20898
|
import {
|
|
@@ -20528,15 +20902,15 @@ import {
|
|
|
20528
20902
|
|
|
20529
20903
|
// src/server.ts
|
|
20530
20904
|
import {
|
|
20531
|
-
existsSync as
|
|
20532
|
-
mkdirSync as
|
|
20533
|
-
statSync as
|
|
20905
|
+
existsSync as existsSync31,
|
|
20906
|
+
mkdirSync as mkdirSync21,
|
|
20907
|
+
statSync as statSync13,
|
|
20534
20908
|
mkdtempSync as mkdtempSync8,
|
|
20535
|
-
writeFileSync as
|
|
20536
|
-
rmSync as
|
|
20909
|
+
writeFileSync as writeFileSync14,
|
|
20910
|
+
rmSync as rmSync19,
|
|
20537
20911
|
createReadStream as createReadStream5
|
|
20538
20912
|
} from "node:fs";
|
|
20539
|
-
import { resolve as resolve12, dirname as
|
|
20913
|
+
import { resolve as resolve12, dirname as dirname19, join as join48 } from "node:path";
|
|
20540
20914
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
20541
20915
|
import { parseArgs } from "node:util";
|
|
20542
20916
|
import crypto from "node:crypto";
|
|
@@ -20551,15 +20925,15 @@ import {
|
|
|
20551
20925
|
|
|
20552
20926
|
// src/services/hyperframeLint.ts
|
|
20553
20927
|
import {
|
|
20554
|
-
existsSync as
|
|
20928
|
+
existsSync as existsSync29,
|
|
20555
20929
|
readFileSync as readFileSync15,
|
|
20556
|
-
statSync as
|
|
20930
|
+
statSync as statSync12,
|
|
20557
20931
|
openSync as openSync7,
|
|
20558
20932
|
fstatSync as fstatSync3,
|
|
20559
20933
|
closeSync as closeSync7,
|
|
20560
20934
|
constants as constants3
|
|
20561
20935
|
} from "node:fs";
|
|
20562
|
-
import { resolve as resolve11, join as
|
|
20936
|
+
import { resolve as resolve11, join as join46, relative as relative7, isAbsolute as isAbsolute5, sep as sep4 } from "node:path";
|
|
20563
20937
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
20564
20938
|
function isStringRecord(value) {
|
|
20565
20939
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -20593,7 +20967,7 @@ function readEntryHtml(filePath) {
|
|
|
20593
20967
|
} catch (error) {
|
|
20594
20968
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
20595
20969
|
return null;
|
|
20596
|
-
if (!
|
|
20970
|
+
if (!statSync12(filePath, { throwIfNoEntry: false })?.isFile()) return null;
|
|
20597
20971
|
throw error;
|
|
20598
20972
|
}
|
|
20599
20973
|
try {
|
|
@@ -20605,7 +20979,7 @@ function readEntryHtml(filePath) {
|
|
|
20605
20979
|
}
|
|
20606
20980
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
20607
20981
|
const absProjectDir = resolve11(projectDir);
|
|
20608
|
-
if (!
|
|
20982
|
+
if (!existsSync29(absProjectDir) || !statSync12(absProjectDir).isDirectory()) {
|
|
20609
20983
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
20610
20984
|
}
|
|
20611
20985
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
@@ -20627,7 +21001,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
20627
21001
|
}
|
|
20628
21002
|
}
|
|
20629
21003
|
return {
|
|
20630
|
-
error: `No HTML entry file found in project directory: ${
|
|
21004
|
+
error: `No HTML entry file found in project directory: ${join46(absProjectDir, preferredEntryFile || "index.html")}`
|
|
20631
21005
|
};
|
|
20632
21006
|
}
|
|
20633
21007
|
function prepareHyperframeLintBody(body) {
|
|
@@ -20673,8 +21047,8 @@ async function runHyperframeLint(prepared) {
|
|
|
20673
21047
|
// src/services/healthWorker.ts
|
|
20674
21048
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
20675
21049
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
20676
|
-
import { dirname as
|
|
20677
|
-
import { existsSync as
|
|
21050
|
+
import { dirname as dirname18, join as join47 } from "node:path";
|
|
21051
|
+
import { existsSync as existsSync30 } from "node:fs";
|
|
20678
21052
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
20679
21053
|
async function startHealthWorker(options = {}) {
|
|
20680
21054
|
const log = options.logger ?? defaultLogger2();
|
|
@@ -20748,10 +21122,10 @@ function defaultLogger2() {
|
|
|
20748
21122
|
};
|
|
20749
21123
|
}
|
|
20750
21124
|
function resolveWorkerEntry2() {
|
|
20751
|
-
const here =
|
|
20752
|
-
const candidates = [
|
|
21125
|
+
const here = dirname18(fileURLToPath6(import.meta.url));
|
|
21126
|
+
const candidates = [join47(here, "healthWorkerThread.js"), join47(here, "healthWorkerThread.ts")];
|
|
20753
21127
|
for (const candidate of candidates) {
|
|
20754
|
-
if (
|
|
21128
|
+
if (existsSync30(candidate)) return candidate;
|
|
20755
21129
|
}
|
|
20756
21130
|
return void 0;
|
|
20757
21131
|
}
|
|
@@ -20896,8 +21270,8 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
20896
21270
|
function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
20897
21271
|
const { input, cleanupProjectDir } = prepared;
|
|
20898
21272
|
const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
|
|
20899
|
-
const outputDir =
|
|
20900
|
-
if (!
|
|
21273
|
+
const outputDir = dirname19(absoluteOutputPath);
|
|
21274
|
+
if (!existsSync31(outputDir)) mkdirSync21(outputDir, { recursive: true });
|
|
20901
21275
|
return { input, cleanupProjectDir, absoluteOutputPath };
|
|
20902
21276
|
}
|
|
20903
21277
|
function validateRenderOverrides(body) {
|
|
@@ -20938,11 +21312,11 @@ function prepareProjectDirectory(projectDir, options) {
|
|
|
20938
21312
|
const candidate = nonEmptyString(projectDir);
|
|
20939
21313
|
if (!candidate) return null;
|
|
20940
21314
|
const absProjectDir = resolve12(candidate);
|
|
20941
|
-
if (!
|
|
21315
|
+
if (!existsSync31(absProjectDir) || !statSync13(absProjectDir).isDirectory()) {
|
|
20942
21316
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
20943
21317
|
}
|
|
20944
21318
|
const entry = options.entryFile || "index.html";
|
|
20945
|
-
if (!
|
|
21319
|
+
if (!existsSync31(resolve12(absProjectDir, entry))) {
|
|
20946
21320
|
return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
|
|
20947
21321
|
}
|
|
20948
21322
|
return { prepared: { input: { projectDir: absProjectDir, ...options } } };
|
|
@@ -20963,8 +21337,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
20963
21337
|
}
|
|
20964
21338
|
function materializeInlineProject(html, options) {
|
|
20965
21339
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir5();
|
|
20966
|
-
const tempProjectDir = mkdtempSync8(
|
|
20967
|
-
|
|
21340
|
+
const tempProjectDir = mkdtempSync8(join48(tempRoot, "producer-project-"));
|
|
21341
|
+
writeFileSync14(join48(tempProjectDir, "index.html"), html, "utf-8");
|
|
20968
21342
|
return {
|
|
20969
21343
|
prepared: {
|
|
20970
21344
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -21025,7 +21399,7 @@ function createArtifactStore(ttlMs) {
|
|
|
21025
21399
|
function cleanupTempDir(dir, log) {
|
|
21026
21400
|
if (!dir) return;
|
|
21027
21401
|
try {
|
|
21028
|
-
|
|
21402
|
+
rmSync19(dir, { recursive: true, force: true });
|
|
21029
21403
|
} catch (error) {
|
|
21030
21404
|
log.warn("Failed to cleanup temp project dir", {
|
|
21031
21405
|
cleanupProjectDir: dir,
|
|
@@ -21034,7 +21408,7 @@ function cleanupTempDir(dir, log) {
|
|
|
21034
21408
|
}
|
|
21035
21409
|
}
|
|
21036
21410
|
function outputFileSize(path) {
|
|
21037
|
-
return
|
|
21411
|
+
return existsSync31(path) ? statSync13(path).size : 0;
|
|
21038
21412
|
}
|
|
21039
21413
|
function createBlockingProgressReporter(log, requestId) {
|
|
21040
21414
|
let lastLoggedPct = -10;
|
|
@@ -21338,11 +21712,11 @@ function createRenderHandlers(options = {}) {
|
|
|
21338
21712
|
if (!artifact) {
|
|
21339
21713
|
return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
|
|
21340
21714
|
}
|
|
21341
|
-
if (!
|
|
21715
|
+
if (!existsSync31(artifact.path)) {
|
|
21342
21716
|
store.delete(token);
|
|
21343
21717
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
21344
21718
|
}
|
|
21345
|
-
const stats =
|
|
21719
|
+
const stats = statSync13(artifact.path);
|
|
21346
21720
|
return new Response(createReadStream5(artifact.path), {
|
|
21347
21721
|
headers: {
|
|
21348
21722
|
"content-type": "video/mp4",
|
|
@@ -21433,9 +21807,9 @@ import {
|
|
|
21433
21807
|
import { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes/engine";
|
|
21434
21808
|
|
|
21435
21809
|
// src/services/distributed/planV2Execution.ts
|
|
21436
|
-
import { mkdtempSync as mkdtempSync10, rmSync as
|
|
21810
|
+
import { mkdtempSync as mkdtempSync10, rmSync as rmSync21 } from "node:fs";
|
|
21437
21811
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
21438
|
-
import { join as
|
|
21812
|
+
import { join as join50 } from "node:path";
|
|
21439
21813
|
|
|
21440
21814
|
// src/services/distributed/assemble.ts
|
|
21441
21815
|
import {
|
|
@@ -21444,16 +21818,16 @@ import {
|
|
|
21444
21818
|
} from "@hyperframes/engine";
|
|
21445
21819
|
import {
|
|
21446
21820
|
cpSync as cpSync3,
|
|
21447
|
-
existsSync as
|
|
21448
|
-
mkdirSync as
|
|
21821
|
+
existsSync as existsSync32,
|
|
21822
|
+
mkdirSync as mkdirSync22,
|
|
21449
21823
|
mkdtempSync as mkdtempSync9,
|
|
21450
21824
|
readFileSync as readFileSync16,
|
|
21451
|
-
readdirSync as
|
|
21452
|
-
rmSync as
|
|
21453
|
-
statSync as
|
|
21454
|
-
writeFileSync as
|
|
21825
|
+
readdirSync as readdirSync12,
|
|
21826
|
+
rmSync as rmSync20,
|
|
21827
|
+
statSync as statSync14,
|
|
21828
|
+
writeFileSync as writeFileSync15
|
|
21455
21829
|
} from "node:fs";
|
|
21456
|
-
import { dirname as
|
|
21830
|
+
import { dirname as dirname20, join as join49 } from "node:path";
|
|
21457
21831
|
import {
|
|
21458
21832
|
appendRateCapArgs as appendRateCapArgs2,
|
|
21459
21833
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
@@ -21494,13 +21868,13 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
21494
21868
|
outputPath,
|
|
21495
21869
|
durationMs: Date.now() - start,
|
|
21496
21870
|
framesEncoded: plan2.totalFrames,
|
|
21497
|
-
fileSize:
|
|
21871
|
+
fileSize: statSync14(outputPath).size
|
|
21498
21872
|
};
|
|
21499
21873
|
}
|
|
21500
21874
|
const format = plan2.dimensions.format;
|
|
21501
21875
|
const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
|
|
21502
|
-
if (!
|
|
21503
|
-
|
|
21876
|
+
if (!existsSync32(dirname20(outputPath))) {
|
|
21877
|
+
mkdirSync22(dirname20(outputPath), { recursive: true });
|
|
21504
21878
|
}
|
|
21505
21879
|
const workDir = mkdtempSync9(`${outputPath}.assemble-work-`);
|
|
21506
21880
|
const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
@@ -21522,8 +21896,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
21522
21896
|
};
|
|
21523
21897
|
try {
|
|
21524
21898
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
21525
|
-
const finalPath =
|
|
21526
|
-
const audio = audioPath !== null &&
|
|
21899
|
+
const finalPath = join49(workDir, `output.${format}`);
|
|
21900
|
+
const audio = audioPath !== null && existsSync32(audioPath) ? audioPath : null;
|
|
21527
21901
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
21528
21902
|
renameWithRetry11(finalPath, outputPath);
|
|
21529
21903
|
log.info("[assemble] stage timings", {
|
|
@@ -21533,7 +21907,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
21533
21907
|
});
|
|
21534
21908
|
} finally {
|
|
21535
21909
|
try {
|
|
21536
|
-
|
|
21910
|
+
rmSync20(workDir, { recursive: true, force: true });
|
|
21537
21911
|
} catch (err) {
|
|
21538
21912
|
log.warn("[assemble] failed to remove work dir", {
|
|
21539
21913
|
workDir,
|
|
@@ -21541,7 +21915,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
21541
21915
|
});
|
|
21542
21916
|
}
|
|
21543
21917
|
}
|
|
21544
|
-
const fileSize =
|
|
21918
|
+
const fileSize = existsSync32(outputPath) ? statSync14(outputPath).size : 0;
|
|
21545
21919
|
return {
|
|
21546
21920
|
...referenceBackgroundResult8(plan2[REFERENCE_BACKGROUND_FIELD6]),
|
|
21547
21921
|
outputPath,
|
|
@@ -21551,15 +21925,15 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
21551
21925
|
};
|
|
21552
21926
|
}
|
|
21553
21927
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
21554
|
-
const planJsonPath =
|
|
21555
|
-
const chunksJsonPath =
|
|
21556
|
-
if (!
|
|
21928
|
+
const planJsonPath = join49(planDir, "plan.json");
|
|
21929
|
+
const chunksJsonPath = join49(planDir, "meta", "chunks.json");
|
|
21930
|
+
if (!existsSync32(planJsonPath)) {
|
|
21557
21931
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
21558
21932
|
}
|
|
21559
21933
|
const plan2 = JSON.parse(readFileSync16(planJsonPath, "utf-8"));
|
|
21560
21934
|
readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
|
|
21561
21935
|
validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
|
|
21562
|
-
if (!
|
|
21936
|
+
if (!existsSync32(chunksJsonPath)) {
|
|
21563
21937
|
throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
|
|
21564
21938
|
}
|
|
21565
21939
|
const chunks = JSON.parse(readFileSync16(chunksJsonPath, "utf-8"));
|
|
@@ -21574,7 +21948,7 @@ function readAssemblePlan(planDir, chunkPaths) {
|
|
|
21574
21948
|
}
|
|
21575
21949
|
}
|
|
21576
21950
|
for (const path of chunkPaths.flat()) {
|
|
21577
|
-
if (!
|
|
21951
|
+
if (!existsSync32(path)) {
|
|
21578
21952
|
throw new Error(`[assemble] chunk path does not exist: ${path}`);
|
|
21579
21953
|
}
|
|
21580
21954
|
}
|
|
@@ -21603,7 +21977,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
21603
21977
|
return args;
|
|
21604
21978
|
}
|
|
21605
21979
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
21606
|
-
const cfrOutputPath =
|
|
21980
|
+
const cfrOutputPath = join49(ctx.workDir, `cfr.${ctx.format}`);
|
|
21607
21981
|
const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
|
|
21608
21982
|
appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
|
|
21609
21983
|
cfrArgs.push("-y", cfrOutputPath);
|
|
@@ -21630,7 +22004,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
21630
22004
|
);
|
|
21631
22005
|
return;
|
|
21632
22006
|
}
|
|
21633
|
-
const paddedAudioPath =
|
|
22007
|
+
const paddedAudioPath = join49(ctx.workDir, "audio-padded.m4a");
|
|
21634
22008
|
const padTrimResult = await ctx.timed(
|
|
21635
22009
|
"audioMs",
|
|
21636
22010
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -21670,9 +22044,9 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
21670
22044
|
}
|
|
21671
22045
|
}
|
|
21672
22046
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
21673
|
-
const listPath =
|
|
22047
|
+
const listPath = join49(workDir, "concat-list.txt");
|
|
21674
22048
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
21675
|
-
|
|
22049
|
+
writeFileSync15(listPath, `${body}
|
|
21676
22050
|
`, "utf-8");
|
|
21677
22051
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
21678
22052
|
}
|
|
@@ -21708,12 +22082,12 @@ function stitchesSeparateAudio(planDir) {
|
|
|
21708
22082
|
return lockedAudio(planDir).separateAudio === true;
|
|
21709
22083
|
}
|
|
21710
22084
|
function planFormat(planDir) {
|
|
21711
|
-
const plan2 = JSON.parse(readFileSync16(
|
|
22085
|
+
const plan2 = JSON.parse(readFileSync16(join49(planDir, "plan.json"), "utf-8"));
|
|
21712
22086
|
return plan2.dimensions.format;
|
|
21713
22087
|
}
|
|
21714
22088
|
function lockedAudio(planDir) {
|
|
21715
|
-
const encoderJsonPath =
|
|
21716
|
-
if (!
|
|
22089
|
+
const encoderJsonPath = join49(planDir, "meta", "encoder.json");
|
|
22090
|
+
if (!existsSync32(encoderJsonPath)) return {};
|
|
21717
22091
|
return JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
|
|
21718
22092
|
}
|
|
21719
22093
|
function readCfrEncoder(planDir, format) {
|
|
@@ -21722,8 +22096,8 @@ function readCfrEncoder(planDir, format) {
|
|
|
21722
22096
|
`[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.`
|
|
21723
22097
|
);
|
|
21724
22098
|
}
|
|
21725
|
-
const encoderJsonPath =
|
|
21726
|
-
if (!
|
|
22099
|
+
const encoderJsonPath = join49(planDir, "meta", "encoder.json");
|
|
22100
|
+
if (!existsSync32(encoderJsonPath)) {
|
|
21727
22101
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
21728
22102
|
}
|
|
21729
22103
|
const encoderJson = JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
|
|
@@ -21735,22 +22109,22 @@ function readCfrEncoder(planDir, format) {
|
|
|
21735
22109
|
return encoderJson;
|
|
21736
22110
|
}
|
|
21737
22111
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
21738
|
-
if (
|
|
21739
|
-
|
|
22112
|
+
if (existsSync32(outputPath)) rmSync20(outputPath, { recursive: true, force: true });
|
|
22113
|
+
mkdirSync22(outputPath, { recursive: true });
|
|
21740
22114
|
let globalIdx = 0;
|
|
21741
22115
|
for (const chunkDir of chunkPaths) {
|
|
21742
|
-
if (!
|
|
22116
|
+
if (!statSync14(chunkDir).isDirectory()) {
|
|
21743
22117
|
throw new Error(
|
|
21744
22118
|
`[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
|
|
21745
22119
|
);
|
|
21746
22120
|
}
|
|
21747
|
-
const frames =
|
|
22121
|
+
const frames = readdirSync12(chunkDir).filter((name) => name.endsWith(".png")).sort();
|
|
21748
22122
|
if (frames.length === 0) {
|
|
21749
22123
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
21750
22124
|
}
|
|
21751
22125
|
for (const frame of frames) {
|
|
21752
|
-
const dst =
|
|
21753
|
-
cpSync3(
|
|
22126
|
+
const dst = join49(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
22127
|
+
cpSync3(join49(chunkDir, frame), dst);
|
|
21754
22128
|
globalIdx += 1;
|
|
21755
22129
|
}
|
|
21756
22130
|
}
|
|
@@ -21759,14 +22133,14 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
21759
22133
|
`[assemble] png-sequence frame count mismatch: merged ${globalIdx} frames vs plan.totalFrames=${totalFrames}. Using on-disk count.`
|
|
21760
22134
|
);
|
|
21761
22135
|
}
|
|
21762
|
-
if (audioPath !== null &&
|
|
21763
|
-
const sidecar =
|
|
22136
|
+
if (audioPath !== null && existsSync32(audioPath)) {
|
|
22137
|
+
const sidecar = join49(outputPath, MIXED_AUDIO_FILENAME4);
|
|
21764
22138
|
cpSync3(audioPath, sidecar);
|
|
21765
22139
|
}
|
|
21766
22140
|
let fileSize = 0;
|
|
21767
|
-
for (const name of
|
|
22141
|
+
for (const name of readdirSync12(outputPath)) {
|
|
21768
22142
|
try {
|
|
21769
|
-
fileSize +=
|
|
22143
|
+
fileSize += statSync14(join49(outputPath, name)).size;
|
|
21770
22144
|
} catch {
|
|
21771
22145
|
}
|
|
21772
22146
|
}
|
|
@@ -21780,18 +22154,18 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
21780
22154
|
|
|
21781
22155
|
// src/services/distributed/planV2Execution.ts
|
|
21782
22156
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
21783
|
-
const workRoot = mkdtempSync10(
|
|
21784
|
-
const materializedPlanDir =
|
|
22157
|
+
const workRoot = mkdtempSync10(join50(tmpdir6(), "hf-plan-v2-chunk-"));
|
|
22158
|
+
const materializedPlanDir = join50(workRoot, "plan");
|
|
21785
22159
|
try {
|
|
21786
22160
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
21787
22161
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
21788
22162
|
} finally {
|
|
21789
|
-
|
|
22163
|
+
rmSync21(workRoot, { recursive: true, force: true });
|
|
21790
22164
|
}
|
|
21791
22165
|
}
|
|
21792
22166
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
21793
|
-
const workRoot = mkdtempSync10(
|
|
21794
|
-
const materializedPlanDir =
|
|
22167
|
+
const workRoot = mkdtempSync10(join50(tmpdir6(), "hf-plan-v2-assembler-"));
|
|
22168
|
+
const materializedPlanDir = join50(workRoot, "plan");
|
|
21795
22169
|
try {
|
|
21796
22170
|
const materialized = materializePlanV2Target(
|
|
21797
22171
|
planV2Dir,
|
|
@@ -21806,7 +22180,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
21806
22180
|
options
|
|
21807
22181
|
);
|
|
21808
22182
|
} finally {
|
|
21809
|
-
|
|
22183
|
+
rmSync21(workRoot, { recursive: true, force: true });
|
|
21810
22184
|
}
|
|
21811
22185
|
}
|
|
21812
22186
|
|
|
@@ -21857,7 +22231,7 @@ export {
|
|
|
21857
22231
|
createRenderHandlers,
|
|
21858
22232
|
createRenderJob,
|
|
21859
22233
|
createRenderRequest,
|
|
21860
|
-
|
|
22234
|
+
createVideoFrameInjector4 as createVideoFrameInjector,
|
|
21861
22235
|
defaultLogger,
|
|
21862
22236
|
distributedConfigFromRequest,
|
|
21863
22237
|
executeRenderJob,
|