@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/public-server.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import {
|
|
5
|
-
existsSync as
|
|
6
|
-
mkdirSync as
|
|
7
|
-
statSync as
|
|
5
|
+
existsSync as existsSync23,
|
|
6
|
+
mkdirSync as mkdirSync13,
|
|
7
|
+
statSync as statSync11,
|
|
8
8
|
mkdtempSync as mkdtempSync4,
|
|
9
|
-
writeFileSync as
|
|
10
|
-
rmSync as
|
|
9
|
+
writeFileSync as writeFileSync8,
|
|
10
|
+
rmSync as rmSync13,
|
|
11
11
|
createReadStream as createReadStream3
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { resolve as resolve10, dirname as
|
|
13
|
+
import { resolve as resolve10, dirname as dirname15, join as join35 } from "node:path";
|
|
14
14
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
16
|
import crypto from "node:crypto";
|
|
@@ -26,12 +26,12 @@ import {
|
|
|
26
26
|
// src/services/renderOrchestrator.ts
|
|
27
27
|
import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
|
|
28
28
|
import {
|
|
29
|
-
existsSync as
|
|
30
|
-
mkdirSync as
|
|
31
|
-
readdirSync as
|
|
32
|
-
rmSync as
|
|
33
|
-
statSync as
|
|
34
|
-
writeFileSync as
|
|
29
|
+
existsSync as existsSync20,
|
|
30
|
+
mkdirSync as mkdirSync12,
|
|
31
|
+
readdirSync as readdirSync6,
|
|
32
|
+
rmSync as rmSync12,
|
|
33
|
+
statSync as statSync9,
|
|
34
|
+
writeFileSync as writeFileSync7,
|
|
35
35
|
copyFileSync as copyFileSync5,
|
|
36
36
|
appendFileSync
|
|
37
37
|
} from "fs";
|
|
@@ -46,16 +46,14 @@ import {
|
|
|
46
46
|
toFps as toFps2
|
|
47
47
|
} from "@hyperframes/core";
|
|
48
48
|
import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
|
|
49
|
-
import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
|
|
50
49
|
import { HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE } from "@hyperframes/parsers";
|
|
51
|
-
import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
|
|
52
50
|
import {
|
|
53
51
|
extractMediaMetadata as extractMediaMetadata4,
|
|
54
52
|
resolveConfig as resolveConfig2,
|
|
55
53
|
closeCaptureSession as closeCaptureSession8,
|
|
56
54
|
createCaptureSession as createCaptureSession8,
|
|
57
55
|
initializeSession as initializeSession8,
|
|
58
|
-
createVideoFrameInjector,
|
|
56
|
+
createVideoFrameInjector as createVideoFrameInjector2,
|
|
59
57
|
getEncoderPreset as getEncoderPreset3,
|
|
60
58
|
distributeFrames as distributeFrames2,
|
|
61
59
|
distributeFramesInterleaved as distributeFramesInterleaved2,
|
|
@@ -79,13 +77,14 @@ import {
|
|
|
79
77
|
isDrawElementCaptureError,
|
|
80
78
|
getDrawElementVerificationDetails,
|
|
81
79
|
augmentProtocolTimeoutError,
|
|
82
|
-
augmentPageNavigationTimeoutError
|
|
80
|
+
augmentPageNavigationTimeoutError,
|
|
81
|
+
displaySize
|
|
83
82
|
} from "@hyperframes/engine";
|
|
84
83
|
import {
|
|
85
84
|
referenceBackgroundResult,
|
|
86
85
|
createReferenceBackgroundRecorder
|
|
87
86
|
} from "@hyperframes/engine";
|
|
88
|
-
import { join as
|
|
87
|
+
import { join as join31, dirname as dirname13, resolve as resolve8 } from "path";
|
|
89
88
|
import { totalmem as totalmem2 } from "node:os";
|
|
90
89
|
import { randomUUID } from "crypto";
|
|
91
90
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -2030,6 +2029,7 @@ import {
|
|
|
2030
2029
|
getCapturePerfSummary as getCapturePerfSummary2,
|
|
2031
2030
|
initializeSession as initializeSession2,
|
|
2032
2031
|
prepareCaptureSessionForReuse as prepareCaptureSessionForReuse2,
|
|
2032
|
+
resolveDeVerifyMinDb as resolveDeVerifyMinDb2,
|
|
2033
2033
|
shouldDisableBrowserPoolForParallelWorker,
|
|
2034
2034
|
spawnStreamingEncoder as spawnStreamingEncoder2
|
|
2035
2035
|
} from "@hyperframes/engine";
|
|
@@ -3900,42 +3900,47 @@ function createDrainFrameGuard(args) {
|
|
|
3900
3900
|
sizes.push(buf.length);
|
|
3901
3901
|
if (sizes.length > 60) sizes.shift();
|
|
3902
3902
|
}
|
|
3903
|
-
|
|
3904
|
-
if (truth) {
|
|
3905
|
-
let db;
|
|
3906
|
-
try {
|
|
3907
|
-
db = await psnrDb(buf, truth);
|
|
3908
|
-
} catch (err) {
|
|
3909
|
-
log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
|
|
3910
|
-
frame: idx,
|
|
3911
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3912
|
-
});
|
|
3913
|
-
return buf;
|
|
3914
|
-
}
|
|
3915
|
-
if (db < verifyMinDb) {
|
|
3916
|
-
const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
|
|
3917
|
-
if (dumpDir) {
|
|
3918
|
-
await Promise.all([
|
|
3919
|
-
writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
|
|
3920
|
-
writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
|
|
3921
|
-
]).catch(() => {
|
|
3922
|
-
});
|
|
3923
|
-
}
|
|
3924
|
-
throw new DrawElementVerificationError(
|
|
3925
|
-
`drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
|
|
3926
|
-
{ kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
|
|
3927
|
-
);
|
|
3928
|
-
}
|
|
3929
|
-
stats.verifyChecked += 1;
|
|
3930
|
-
stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
|
|
3931
|
-
log.info("[Render] drawElement self-verify passed", {
|
|
3932
|
-
frame: idx,
|
|
3933
|
-
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
|
|
3934
|
-
});
|
|
3935
|
-
}
|
|
3903
|
+
await verifyDrawElementSample(session, idx, buf, { log, stats, verifyMinDb });
|
|
3936
3904
|
return buf;
|
|
3937
3905
|
};
|
|
3938
3906
|
}
|
|
3907
|
+
async function verifyDrawElementSample(session, idx, buf, ctx) {
|
|
3908
|
+
const { log, stats, verifyMinDb } = ctx;
|
|
3909
|
+
const truth = session.deVerifyFrames?.get(idx);
|
|
3910
|
+
if (!truth) return;
|
|
3911
|
+
let db;
|
|
3912
|
+
try {
|
|
3913
|
+
db = await psnrDb(buf, truth);
|
|
3914
|
+
} catch (err) {
|
|
3915
|
+
log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
|
|
3916
|
+
frame: idx,
|
|
3917
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3918
|
+
});
|
|
3919
|
+
return;
|
|
3920
|
+
}
|
|
3921
|
+
if (db < verifyMinDb) {
|
|
3922
|
+
const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
|
|
3923
|
+
if (dumpDir) {
|
|
3924
|
+
await Promise.all([
|
|
3925
|
+
writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
|
|
3926
|
+
writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
|
|
3927
|
+
]).catch(() => {
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
throw new DrawElementVerificationError(
|
|
3931
|
+
`drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
|
|
3932
|
+
{ kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
if (stats) {
|
|
3936
|
+
stats.verifyChecked += 1;
|
|
3937
|
+
stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
|
|
3938
|
+
}
|
|
3939
|
+
log.info("[Render] drawElement self-verify passed", {
|
|
3940
|
+
frame: idx,
|
|
3941
|
+
psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3939
3944
|
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, resume, risk) {
|
|
3940
3945
|
let prev = null;
|
|
3941
3946
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
@@ -4399,6 +4404,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4399
4404
|
);
|
|
4400
4405
|
} else {
|
|
4401
4406
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
4407
|
+
const verifyMinDb = resolveDeVerifyMinDb();
|
|
4402
4408
|
let lastProgressAt = Date.now();
|
|
4403
4409
|
for (let i = resume.from; i < totalFrames; i++) {
|
|
4404
4410
|
assertNotAborted();
|
|
@@ -4415,6 +4421,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4415
4421
|
{ session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
|
|
4416
4422
|
abortSignal
|
|
4417
4423
|
);
|
|
4424
|
+
await verifyDrawElementSample(session, i, buffer, { log, verifyMinDb });
|
|
4418
4425
|
await reorderBuffer.waitForFrame(i);
|
|
4419
4426
|
ensureFrameWritten(await currentEncoder.writeFrame(buffer), i, currentEncoder);
|
|
4420
4427
|
reorderBuffer.advanceTo(i + 1);
|
|
@@ -4523,6 +4530,10 @@ async function captureSegmentFrames(ctx, segment, encoder) {
|
|
|
4523
4530
|
},
|
|
4524
4531
|
ctx.abortSignal
|
|
4525
4532
|
);
|
|
4533
|
+
await verifyDrawElementSample(ctx.session, i, buffer, {
|
|
4534
|
+
log: ctx.log,
|
|
4535
|
+
verifyMinDb: ctx.verifyMinDb
|
|
4536
|
+
});
|
|
4526
4537
|
ensureFrameWritten(await encoder.writeFrame(buffer), i, encoder);
|
|
4527
4538
|
ctx.job.framesRendered = i + 1;
|
|
4528
4539
|
lastProgressAt = Date.now();
|
|
@@ -4537,6 +4548,25 @@ async function captureSegmentFrames(ctx, segment, encoder) {
|
|
|
4537
4548
|
}
|
|
4538
4549
|
}
|
|
4539
4550
|
async function captureSegment(ctx, segment) {
|
|
4551
|
+
const { frames } = ctx;
|
|
4552
|
+
if (!frames) return captureSegmentToEncoder(ctx, segment);
|
|
4553
|
+
try {
|
|
4554
|
+
reportFrameProgress(
|
|
4555
|
+
ctx.job,
|
|
4556
|
+
`Reading video for segment ${segment.index + 1}/${ctx.segmentCount}`,
|
|
4557
|
+
Math.round(25 + segment.startFrame / ctx.totalFrames * 55),
|
|
4558
|
+
ctx.onProgress,
|
|
4559
|
+
segment.startFrame,
|
|
4560
|
+
ctx.totalFrames
|
|
4561
|
+
);
|
|
4562
|
+
await frames.prepare(segment);
|
|
4563
|
+
ctx.assertNotAborted();
|
|
4564
|
+
return await captureSegmentToEncoder(ctx, segment);
|
|
4565
|
+
} finally {
|
|
4566
|
+
frames.release();
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
async function captureSegmentToEncoder(ctx, segment) {
|
|
4540
4570
|
const segmentPath = segmentOutputPath(ctx.segmentDir, segment.index);
|
|
4541
4571
|
let encoder;
|
|
4542
4572
|
try {
|
|
@@ -4589,16 +4619,16 @@ var DEFAULT_DEPS = {
|
|
|
4589
4619
|
function defaultSessionFactory(input) {
|
|
4590
4620
|
let probeSession = input.probeSession;
|
|
4591
4621
|
return {
|
|
4592
|
-
create: async () => {
|
|
4622
|
+
create: async (injector = input.createRenderVideoFrameInjector) => {
|
|
4593
4623
|
const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
|
|
4594
4624
|
if (probeSession && probeCannotCaptureEveryFrame) {
|
|
4595
4625
|
await closeCaptureSession2(probeSession).catch(() => {
|
|
4596
4626
|
});
|
|
4597
4627
|
probeSession = null;
|
|
4598
4628
|
}
|
|
4599
|
-
const session = probeSession ?? await openSegmentedSession(input);
|
|
4629
|
+
const session = probeSession ?? await openSegmentedSession(input, injector);
|
|
4600
4630
|
if (probeSession) {
|
|
4601
|
-
openSegmentedSessionReuse(input, probeSession);
|
|
4631
|
+
openSegmentedSessionReuse(input, probeSession, injector);
|
|
4602
4632
|
probeSession = null;
|
|
4603
4633
|
}
|
|
4604
4634
|
try {
|
|
@@ -4613,12 +4643,12 @@ function defaultSessionFactory(input) {
|
|
|
4613
4643
|
}
|
|
4614
4644
|
};
|
|
4615
4645
|
}
|
|
4616
|
-
function openSegmentedSessionReuse(input, session) {
|
|
4617
|
-
prepareCaptureSessionForReuse2(session, input.framesDir,
|
|
4646
|
+
function openSegmentedSessionReuse(input, session, injector) {
|
|
4647
|
+
prepareCaptureSessionForReuse2(session, input.framesDir, injector());
|
|
4618
4648
|
}
|
|
4619
|
-
async function openSegmentedSession(input) {
|
|
4649
|
+
async function openSegmentedSession(input, injector) {
|
|
4620
4650
|
const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
|
|
4621
|
-
const videoInjector =
|
|
4651
|
+
const videoInjector = injector();
|
|
4622
4652
|
const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
|
|
4623
4653
|
const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
|
|
4624
4654
|
parallel: (input.workerCount ?? 1) > 1
|
|
@@ -4631,6 +4661,19 @@ async function openSegmentedSession(input) {
|
|
|
4631
4661
|
ownBrowser ? { ...captureCfg, enableBrowserPool: false } : captureCfg
|
|
4632
4662
|
);
|
|
4633
4663
|
}
|
|
4664
|
+
function removeJoinedSegments(run, segmentPaths) {
|
|
4665
|
+
if (!run.input.segmentVideoFrames || run.input.job.config.keepSegments === true) return;
|
|
4666
|
+
for (const path of segmentPaths) {
|
|
4667
|
+
try {
|
|
4668
|
+
run.deps.removeFile(path);
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
run.input.log.warn("[Render] could not remove a joined segment file", {
|
|
4671
|
+
path,
|
|
4672
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4634
4677
|
async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal, cfg) {
|
|
4635
4678
|
const result = await concat(segmentPaths, videoOnlyPath, abortSignal, cfg);
|
|
4636
4679
|
if (result.success) return;
|
|
@@ -4642,12 +4685,14 @@ async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal,
|
|
|
4642
4685
|
async function createSegmentWorker(run, id) {
|
|
4643
4686
|
const { input, deps } = run;
|
|
4644
4687
|
const factory = input.sessionFactoryForWorker?.(id) ?? run.sessionFactory;
|
|
4688
|
+
const frames = input.segmentVideoFrames?.forWorker();
|
|
4645
4689
|
return {
|
|
4646
4690
|
id,
|
|
4647
4691
|
factory,
|
|
4648
4692
|
sessionSegments: 0,
|
|
4649
4693
|
ctx: {
|
|
4650
|
-
session: await factory.create(),
|
|
4694
|
+
session: await factory.create(frames && (() => frames.hook)),
|
|
4695
|
+
frames,
|
|
4651
4696
|
job: input.job,
|
|
4652
4697
|
cfg: input.cfg,
|
|
4653
4698
|
totalFrames: input.totalFrames,
|
|
@@ -4658,6 +4703,8 @@ async function createSegmentWorker(run, id) {
|
|
|
4658
4703
|
spawnEncoder: deps.spawnEncoder,
|
|
4659
4704
|
captureFrame: deps.captureFrame,
|
|
4660
4705
|
stallTimeoutMs: resolveCaptureStallTimeoutMs(),
|
|
4706
|
+
log: input.log,
|
|
4707
|
+
verifyMinDb: resolveDeVerifyMinDb2(),
|
|
4661
4708
|
abortSignal: input.abortSignal,
|
|
4662
4709
|
assertNotAborted: input.assertNotAborted,
|
|
4663
4710
|
onProgress: input.onProgress,
|
|
@@ -4677,7 +4724,8 @@ async function recycleWorkerSession(run, worker, why) {
|
|
|
4677
4724
|
run.input.dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
|
|
4678
4725
|
const memory = worker.ctx.session.chromeMemory?.stats();
|
|
4679
4726
|
await run.deps.closeSession(worker.ctx.session);
|
|
4680
|
-
|
|
4727
|
+
const { frames } = worker.ctx;
|
|
4728
|
+
worker.ctx.session = await worker.factory.create(frames && (() => frames.hook));
|
|
4681
4729
|
worker.sessionSegments = 0;
|
|
4682
4730
|
if (why === "cadence") run.totals.browserRecycles += 1;
|
|
4683
4731
|
run.input.log.info(`[Render] segment browser recycled (${why})`, {
|
|
@@ -4745,6 +4793,7 @@ async function concatResumedRun(run, segmentPaths) {
|
|
|
4745
4793
|
input.abortSignal,
|
|
4746
4794
|
input.cfg
|
|
4747
4795
|
);
|
|
4796
|
+
removeJoinedSegments(run, segmentPaths);
|
|
4748
4797
|
return {
|
|
4749
4798
|
success: true,
|
|
4750
4799
|
encodeMs: 0,
|
|
@@ -4819,6 +4868,7 @@ async function runCaptureSegmentedStage(input) {
|
|
|
4819
4868
|
input.abortSignal,
|
|
4820
4869
|
input.cfg
|
|
4821
4870
|
);
|
|
4871
|
+
removeJoinedSegments(run, segmentPaths);
|
|
4822
4872
|
return {
|
|
4823
4873
|
success: true,
|
|
4824
4874
|
encodeMs: run.totals.encodeMs,
|
|
@@ -5250,10 +5300,273 @@ function checkPageNeverReady(input) {
|
|
|
5250
5300
|
return new PageNeverReadyError(error);
|
|
5251
5301
|
}
|
|
5252
5302
|
|
|
5303
|
+
// src/services/render/deferredVideoFrames.ts
|
|
5304
|
+
import { mkdirSync as mkdirSync4, rmSync as rmSync7 } from "node:fs";
|
|
5305
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
5306
|
+
import {
|
|
5307
|
+
createFrameLookupTable,
|
|
5308
|
+
extractVideoFramesRange,
|
|
5309
|
+
runVideoExtractionWithRetry
|
|
5310
|
+
} from "@hyperframes/engine";
|
|
5311
|
+
function contiguousRuns(sortedIndexes) {
|
|
5312
|
+
const runs = [];
|
|
5313
|
+
for (const index of sortedIndexes) {
|
|
5314
|
+
const last = runs[runs.length - 1];
|
|
5315
|
+
if (last && last.firstFrame + last.frames === index) last.frames++;
|
|
5316
|
+
else runs.push({ firstFrame: index, frames: 1 });
|
|
5317
|
+
}
|
|
5318
|
+
return runs;
|
|
5319
|
+
}
|
|
5320
|
+
function shownDeferredFrameIndexes(source, frames, fps) {
|
|
5321
|
+
const shown = /* @__PURE__ */ new Map();
|
|
5322
|
+
const deferred = source.extracted.filter((video) => video.deferredRange);
|
|
5323
|
+
if (deferred.length === 0) return shown;
|
|
5324
|
+
const table = createFrameLookupTable(
|
|
5325
|
+
source.videos,
|
|
5326
|
+
deferred.map((video) => ({
|
|
5327
|
+
...video,
|
|
5328
|
+
outputDir: "",
|
|
5329
|
+
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
5330
|
+
})),
|
|
5331
|
+
resolveRenderFpsConfig(fps).value
|
|
5332
|
+
);
|
|
5333
|
+
for (const frame of frames) {
|
|
5334
|
+
const globalTime = frame * fps.den / fps.num;
|
|
5335
|
+
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
5336
|
+
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
5337
|
+
indexes.add(payload.frameIndex);
|
|
5338
|
+
shown.set(videoId, indexes);
|
|
5339
|
+
}
|
|
5340
|
+
}
|
|
5341
|
+
return shown;
|
|
5342
|
+
}
|
|
5343
|
+
async function extractDeferredFrames(input) {
|
|
5344
|
+
const deferred = input.extracted.filter((video) => video.deferredRange);
|
|
5345
|
+
if (deferred.length === 0) return [];
|
|
5346
|
+
const shown = shownDeferredFrameIndexes(input, input.frames, input.fps);
|
|
5347
|
+
return Promise.all(
|
|
5348
|
+
deferred.map(async (video) => {
|
|
5349
|
+
const outputDir = join12(input.outputRoot, video.videoId);
|
|
5350
|
+
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
5351
|
+
if (indexes.length === 0) {
|
|
5352
|
+
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
5353
|
+
}
|
|
5354
|
+
const range = video.deferredRange;
|
|
5355
|
+
const extracted = await extractVideoFramesRange(
|
|
5356
|
+
input.sourceOf(video),
|
|
5357
|
+
video.videoId,
|
|
5358
|
+
range.startTime,
|
|
5359
|
+
range.durationSeconds,
|
|
5360
|
+
{
|
|
5361
|
+
fps: input.fps,
|
|
5362
|
+
outputDir: input.outputRoot,
|
|
5363
|
+
format: range.format,
|
|
5364
|
+
frameRanges: contiguousRuns(indexes),
|
|
5365
|
+
shownBox: input.shownBoxes?.get(video.videoId)
|
|
5366
|
+
},
|
|
5367
|
+
input.signal,
|
|
5368
|
+
input.cfg
|
|
5369
|
+
);
|
|
5370
|
+
const missing = indexes.find((index) => !extracted.framePaths.has(index));
|
|
5371
|
+
if (missing !== void 0) throw input.onMissingFrame(video, missing);
|
|
5372
|
+
return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
|
|
5373
|
+
})
|
|
5374
|
+
);
|
|
5375
|
+
}
|
|
5376
|
+
async function extractAllDeferredFrames(extracted, input) {
|
|
5377
|
+
const framesTotal = extracted.reduce(
|
|
5378
|
+
(sum, video) => sum + (video.deferredRange ? video.totalFrames : 0),
|
|
5379
|
+
0
|
|
5380
|
+
);
|
|
5381
|
+
let framesDone = 0;
|
|
5382
|
+
for (const video of extracted) {
|
|
5383
|
+
const range = video.deferredRange;
|
|
5384
|
+
if (!range) continue;
|
|
5385
|
+
const { result: filled } = await runVideoExtractionWithRetry(
|
|
5386
|
+
() => extractVideoFramesRange(
|
|
5387
|
+
input.sourceOf?.(video) ?? video.srcPath,
|
|
5388
|
+
video.videoId,
|
|
5389
|
+
range.startTime,
|
|
5390
|
+
range.durationSeconds,
|
|
5391
|
+
{ fps: input.fps, outputDir: dirname5(video.outputDir), format: range.format },
|
|
5392
|
+
input.signal,
|
|
5393
|
+
input.cfg
|
|
5394
|
+
),
|
|
5395
|
+
{
|
|
5396
|
+
signal: input.signal,
|
|
5397
|
+
maxTransientRetries: input.maxTransientRetries,
|
|
5398
|
+
onRetry: () => {
|
|
5399
|
+
rmSync7(video.outputDir, { recursive: true, force: true });
|
|
5400
|
+
mkdirSync4(video.outputDir, { recursive: true });
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
);
|
|
5404
|
+
if (filled.framePaths.size < video.totalFrames) {
|
|
5405
|
+
throw new Error(
|
|
5406
|
+
`[Render] video ${JSON.stringify(video.videoId)} has ${filled.framePaths.size} frames; it was expected to have ${video.totalFrames} frames.`
|
|
5407
|
+
);
|
|
5408
|
+
}
|
|
5409
|
+
video.framePaths = filled.framePaths;
|
|
5410
|
+
delete video.deferredRange;
|
|
5411
|
+
framesDone += video.totalFrames;
|
|
5412
|
+
input.onProgress?.(framesDone, framesTotal);
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
// src/services/render/segmentVideoFrames.ts
|
|
5417
|
+
import { mkdirSync as mkdirSync5, rmSync as rmSync8 } from "node:fs";
|
|
5418
|
+
import { join as join13 } from "node:path";
|
|
5419
|
+
import {
|
|
5420
|
+
createFrameLookupTable as createFrameLookupTable2,
|
|
5421
|
+
createVideoFrameInjector
|
|
5422
|
+
} from "@hyperframes/engine";
|
|
5423
|
+
function segmentFramesDir(rootDir, index) {
|
|
5424
|
+
return join13(rootDir, "segment-frames", `segment_${String(index).padStart(5, "0")}`);
|
|
5425
|
+
}
|
|
5426
|
+
function createSegmentVideoFrames(input) {
|
|
5427
|
+
const extractFrames = input.extractFrames ?? extractDeferredFrames;
|
|
5428
|
+
const sourceOf = input.sourceOf ?? ((video) => video.srcPath);
|
|
5429
|
+
const fullyExtracted = input.extracted.filter((video) => !video.deferredRange);
|
|
5430
|
+
const tableFor = async (frames, dir) => {
|
|
5431
|
+
const sliced = await extractFrames({
|
|
5432
|
+
videos: input.videos,
|
|
5433
|
+
extracted: input.extracted,
|
|
5434
|
+
frames,
|
|
5435
|
+
fps: input.fps,
|
|
5436
|
+
outputRoot: dir,
|
|
5437
|
+
cfg: input.cfg,
|
|
5438
|
+
signal: input.abortSignal,
|
|
5439
|
+
sourceOf,
|
|
5440
|
+
onMissingFrame: (video, index) => new Error(
|
|
5441
|
+
`[Render] video ${JSON.stringify(video.videoId)} has no frame ${index}; it was expected to have ${video.totalFrames} frames.`
|
|
5442
|
+
)
|
|
5443
|
+
});
|
|
5444
|
+
return createFrameLookupTable2(
|
|
5445
|
+
input.videos,
|
|
5446
|
+
[...fullyExtracted, ...sliced],
|
|
5447
|
+
resolveRenderFpsConfig(input.fps).value
|
|
5448
|
+
);
|
|
5449
|
+
};
|
|
5450
|
+
const injectorFor = async (frames, dir) => createVideoFrameInjector(await tableFor(frames, dir), input.injectorOptions);
|
|
5451
|
+
let onDemandCalls = 0;
|
|
5452
|
+
const onDemandHook = () => Object.assign(
|
|
5453
|
+
async (page, time, heldVideoTime) => {
|
|
5454
|
+
const dir = join13(
|
|
5455
|
+
input.rootDir,
|
|
5456
|
+
"segment-frames",
|
|
5457
|
+
`on-demand_${String(onDemandCalls++).padStart(5, "0")}`
|
|
5458
|
+
);
|
|
5459
|
+
try {
|
|
5460
|
+
const frames = [time, heldVideoTime].filter((t) => t !== void 0).map((t) => Math.round(t * input.fps.num / input.fps.den));
|
|
5461
|
+
const injector = await injectorFor(frames, dir);
|
|
5462
|
+
await injector?.(page, time, heldVideoTime);
|
|
5463
|
+
} finally {
|
|
5464
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
5465
|
+
}
|
|
5466
|
+
},
|
|
5467
|
+
{ fresh: onDemandHook }
|
|
5468
|
+
);
|
|
5469
|
+
return {
|
|
5470
|
+
async forFrames(frames, name) {
|
|
5471
|
+
const dir = join13(input.rootDir, "segment-frames", name);
|
|
5472
|
+
const release = () => rmSync8(dir, { recursive: true, force: true });
|
|
5473
|
+
try {
|
|
5474
|
+
release();
|
|
5475
|
+
mkdirSync5(dir, { recursive: true });
|
|
5476
|
+
const table = await tableFor([...frames], dir);
|
|
5477
|
+
return { injector: () => createVideoFrameInjector(table, input.injectorOptions), release };
|
|
5478
|
+
} catch (error) {
|
|
5479
|
+
release();
|
|
5480
|
+
throw error;
|
|
5481
|
+
}
|
|
5482
|
+
},
|
|
5483
|
+
forWorker() {
|
|
5484
|
+
let injector = null;
|
|
5485
|
+
let folder = null;
|
|
5486
|
+
const onDemand = onDemandHook();
|
|
5487
|
+
const release = () => {
|
|
5488
|
+
injector = null;
|
|
5489
|
+
if (folder) rmSync8(folder, { recursive: true, force: true });
|
|
5490
|
+
folder = null;
|
|
5491
|
+
};
|
|
5492
|
+
return {
|
|
5493
|
+
hook: Object.assign(
|
|
5494
|
+
(page, time, heldVideoTime) => (injector ?? onDemand)(page, time, heldVideoTime),
|
|
5495
|
+
{ fresh: onDemandHook }
|
|
5496
|
+
),
|
|
5497
|
+
release,
|
|
5498
|
+
async prepare(segment) {
|
|
5499
|
+
release();
|
|
5500
|
+
const dir = segmentFramesDir(input.rootDir, segment.index);
|
|
5501
|
+
folder = dir;
|
|
5502
|
+
try {
|
|
5503
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
5504
|
+
mkdirSync5(dir, { recursive: true });
|
|
5505
|
+
injector = await injectorFor(
|
|
5506
|
+
Array.from(
|
|
5507
|
+
{ length: segment.endFrame - segment.startFrame },
|
|
5508
|
+
(_, i) => segment.startFrame + i
|
|
5509
|
+
),
|
|
5510
|
+
dir
|
|
5511
|
+
);
|
|
5512
|
+
} catch (error) {
|
|
5513
|
+
release();
|
|
5514
|
+
throw error;
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
};
|
|
5518
|
+
}
|
|
5519
|
+
};
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
// src/services/render/videoFrameWindow.ts
|
|
5523
|
+
import { readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
5524
|
+
import { join as join14 } from "node:path";
|
|
5525
|
+
function shouldWindowVideoFrames(args) {
|
|
5526
|
+
return args.segmented && args.env.HF_VIDEO_FRAME_WINDOW?.trim().toLowerCase() === "true";
|
|
5527
|
+
}
|
|
5528
|
+
function directoryBytes(path) {
|
|
5529
|
+
let bytes = 0;
|
|
5530
|
+
let entries;
|
|
5531
|
+
try {
|
|
5532
|
+
entries = readdirSync3(path, { withFileTypes: true });
|
|
5533
|
+
} catch {
|
|
5534
|
+
return 0;
|
|
5535
|
+
}
|
|
5536
|
+
for (const entry of entries) {
|
|
5537
|
+
const child2 = join14(path, entry.name);
|
|
5538
|
+
try {
|
|
5539
|
+
bytes += entry.isDirectory() ? directoryBytes(child2) : statSync4(child2).size;
|
|
5540
|
+
} catch {
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
return bytes;
|
|
5544
|
+
}
|
|
5545
|
+
function trackSegmentFramePeak(frames, rootDir, onPeak) {
|
|
5546
|
+
let peak = 0;
|
|
5547
|
+
return {
|
|
5548
|
+
forFrames: (frameIndexes, name) => frames.forFrames(frameIndexes, name),
|
|
5549
|
+
forWorker() {
|
|
5550
|
+
const worker = frames.forWorker();
|
|
5551
|
+
return {
|
|
5552
|
+
...worker,
|
|
5553
|
+
async prepare(segment) {
|
|
5554
|
+
await worker.prepare(segment);
|
|
5555
|
+
const held = directoryBytes(join14(rootDir, "segment-frames"));
|
|
5556
|
+
if (held > peak) {
|
|
5557
|
+
peak = held;
|
|
5558
|
+
onPeak(peak);
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
};
|
|
5562
|
+
}
|
|
5563
|
+
};
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5253
5566
|
// src/services/render/segmentManifest.ts
|
|
5254
5567
|
import { createHash as createHash2 } from "node:crypto";
|
|
5255
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
5256
|
-
import { join as
|
|
5568
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5569
|
+
import { join as join15 } from "node:path";
|
|
5257
5570
|
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
5258
5571
|
var MANIFEST_FILENAME = "segments.json";
|
|
5259
5572
|
function computeSegmentPlanHash(input) {
|
|
@@ -5281,7 +5594,7 @@ function computeSegmentPlanHash(input) {
|
|
|
5281
5594
|
return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
|
|
5282
5595
|
}
|
|
5283
5596
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
5284
|
-
return
|
|
5597
|
+
return join15(projectRendersDir, ".hf-segments", planHash);
|
|
5285
5598
|
}
|
|
5286
5599
|
function hasNumber(value, key) {
|
|
5287
5600
|
return key in value && typeof Reflect.get(value, key) === "number";
|
|
@@ -5299,7 +5612,7 @@ function isManifest(value) {
|
|
|
5299
5612
|
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
5300
5613
|
}
|
|
5301
5614
|
function readSegmentManifest(segmentDir) {
|
|
5302
|
-
const path =
|
|
5615
|
+
const path = join15(segmentDir, MANIFEST_FILENAME);
|
|
5303
5616
|
if (!existsSync7(path)) return null;
|
|
5304
5617
|
try {
|
|
5305
5618
|
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
@@ -5309,10 +5622,10 @@ function readSegmentManifest(segmentDir) {
|
|
|
5309
5622
|
}
|
|
5310
5623
|
}
|
|
5311
5624
|
function writeSegmentManifest(segmentDir, manifest) {
|
|
5312
|
-
|
|
5313
|
-
const tmp =
|
|
5625
|
+
mkdirSync6(segmentDir, { recursive: true });
|
|
5626
|
+
const tmp = join15(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
5314
5627
|
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
5315
|
-
renameWithRetry2(tmp,
|
|
5628
|
+
renameWithRetry2(tmp, join15(segmentDir, MANIFEST_FILENAME));
|
|
5316
5629
|
}
|
|
5317
5630
|
async function probeSegmentFrameCount(path, probe) {
|
|
5318
5631
|
try {
|
|
@@ -5330,7 +5643,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
|
5330
5643
|
if (manifest.planHash !== expectedHash) return ok;
|
|
5331
5644
|
for (const entry of manifest.completed) {
|
|
5332
5645
|
if (!existsSync7(entry.path)) continue;
|
|
5333
|
-
const size =
|
|
5646
|
+
const size = statSync5(entry.path).size;
|
|
5334
5647
|
if (size <= 0 || size !== entry.bytes) continue;
|
|
5335
5648
|
const frames = await probeFrames(entry.path);
|
|
5336
5649
|
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
@@ -5873,7 +6186,7 @@ function computeVideoFrameCoverage(videos, extracted, fps) {
|
|
|
5873
6186
|
for (const video of videos) {
|
|
5874
6187
|
const entry = byId.get(video.id);
|
|
5875
6188
|
const expectedFrames = expectedFramesForVideo(video, entry, fps);
|
|
5876
|
-
const capturedFrames = entry ? entry.framePaths.size : 0;
|
|
6189
|
+
const capturedFrames = entry ? entry.deferredRange ? entry.totalFrames : entry.framePaths.size : 0;
|
|
5877
6190
|
const ratio = expectedFrames === 0 ? 1 : capturedFrames / expectedFrames;
|
|
5878
6191
|
reports.push({
|
|
5879
6192
|
videoId: video.id,
|
|
@@ -5908,14 +6221,17 @@ function countAuthoredTimedClips(html) {
|
|
|
5908
6221
|
}
|
|
5909
6222
|
|
|
5910
6223
|
// src/services/render/stages/compileStage.ts
|
|
5911
|
-
import {
|
|
6224
|
+
import { existsSync as existsSync11, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6225
|
+
import { join as join19 } from "node:path";
|
|
5912
6226
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
6227
|
+
import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
|
|
6228
|
+
import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
|
|
5913
6229
|
|
|
5914
6230
|
// src/services/htmlCompiler.ts
|
|
5915
6231
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
5916
6232
|
import { createHash as createHash6 } from "node:crypto";
|
|
5917
|
-
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as
|
|
5918
|
-
import { join as
|
|
6233
|
+
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
|
|
6234
|
+
import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
|
|
5919
6235
|
import { parseHTML as parseHTML5 } from "linkedom";
|
|
5920
6236
|
import {
|
|
5921
6237
|
COMPOSITION_SOURCE_URL,
|
|
@@ -6161,8 +6477,8 @@ import {
|
|
|
6161
6477
|
|
|
6162
6478
|
// src/services/animatedGifPrep.ts
|
|
6163
6479
|
import { createHash as createHash4 } from "node:crypto";
|
|
6164
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as
|
|
6165
|
-
import { dirname as
|
|
6480
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
|
|
6481
|
+
import { dirname as dirname6, isAbsolute as isAbsolute2, join as join16, resolve as resolve5 } from "node:path";
|
|
6166
6482
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
6167
6483
|
import { parseAnimatedGifMetadata } from "@hyperframes/core";
|
|
6168
6484
|
import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
|
|
@@ -6211,7 +6527,7 @@ function resolveLocalImageSourcePath(src, options) {
|
|
|
6211
6527
|
}
|
|
6212
6528
|
function isUsableFile(path) {
|
|
6213
6529
|
try {
|
|
6214
|
-
const stat =
|
|
6530
|
+
const stat = statSync6(path);
|
|
6215
6531
|
return stat.isFile() && stat.size > 0;
|
|
6216
6532
|
} catch {
|
|
6217
6533
|
return false;
|
|
@@ -6318,11 +6634,11 @@ async function runAnimatedGifTranscode(request) {
|
|
|
6318
6634
|
throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
|
|
6319
6635
|
}
|
|
6320
6636
|
async function ensurePreparedWebm(input) {
|
|
6321
|
-
if (!existsSync8(
|
|
6322
|
-
|
|
6637
|
+
if (!existsSync8(dirname6(input.cachePath))) {
|
|
6638
|
+
mkdirSync7(dirname6(input.cachePath), { recursive: true });
|
|
6323
6639
|
}
|
|
6324
|
-
if (!existsSync8(
|
|
6325
|
-
|
|
6640
|
+
if (!existsSync8(dirname6(input.outputPath))) {
|
|
6641
|
+
mkdirSync7(dirname6(input.outputPath), { recursive: true });
|
|
6326
6642
|
}
|
|
6327
6643
|
if (!isUsableFile(input.cachePath)) {
|
|
6328
6644
|
const tmpPath = `${input.cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -6346,10 +6662,10 @@ async function ensurePreparedWebm(input) {
|
|
|
6346
6662
|
if (!isUsableFile(input.cachePath)) {
|
|
6347
6663
|
renameWithRetry3(tmpPath, input.cachePath);
|
|
6348
6664
|
} else {
|
|
6349
|
-
|
|
6665
|
+
rmSync9(tmpPath, { force: true });
|
|
6350
6666
|
}
|
|
6351
6667
|
} catch (error) {
|
|
6352
|
-
|
|
6668
|
+
rmSync9(tmpPath, { force: true });
|
|
6353
6669
|
throw error;
|
|
6354
6670
|
}
|
|
6355
6671
|
}
|
|
@@ -6405,7 +6721,7 @@ function replaceImageWithVideo(input) {
|
|
|
6405
6721
|
return video;
|
|
6406
6722
|
}
|
|
6407
6723
|
async function prepareAnimatedGifInputs(html, options) {
|
|
6408
|
-
const outputDir = options.outputDir ??
|
|
6724
|
+
const outputDir = options.outputDir ?? join16(options.downloadDir, PREPARED_GIF_SUBDIR);
|
|
6409
6725
|
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
|
|
6410
6726
|
const cacheDir = options.cacheDir ?? outputDir;
|
|
6411
6727
|
const { document: document2 } = parseHTML3(html);
|
|
@@ -6432,8 +6748,8 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
6432
6748
|
);
|
|
6433
6749
|
const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
|
|
6434
6750
|
const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
|
|
6435
|
-
const cachePath =
|
|
6436
|
-
const outputPath =
|
|
6751
|
+
const cachePath = join16(cacheDir, filename);
|
|
6752
|
+
const outputPath = join16(outputDir, filename);
|
|
6437
6753
|
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
6438
6754
|
await ensurePreparedWebm({
|
|
6439
6755
|
sourcePath,
|
|
@@ -6470,15 +6786,15 @@ import { createHash as createHash5 } from "node:crypto";
|
|
|
6470
6786
|
import {
|
|
6471
6787
|
closeSync as closeSync4,
|
|
6472
6788
|
copyFileSync as copyFileSync3,
|
|
6473
|
-
mkdirSync as
|
|
6789
|
+
mkdirSync as mkdirSync8,
|
|
6474
6790
|
openSync as openSync4,
|
|
6475
6791
|
readFileSync as readFileSync6,
|
|
6476
6792
|
readSync as readSync3,
|
|
6477
|
-
rmSync as
|
|
6478
|
-
statSync as
|
|
6793
|
+
rmSync as rmSync10,
|
|
6794
|
+
statSync as statSync7,
|
|
6479
6795
|
writeFileSync as writeFileSync4
|
|
6480
6796
|
} from "node:fs";
|
|
6481
|
-
import { dirname as
|
|
6797
|
+
import { dirname as dirname7, join as join17, resolve as resolve6 } from "node:path";
|
|
6482
6798
|
import { parseHTML as parseHTML4 } from "linkedom";
|
|
6483
6799
|
import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
|
|
6484
6800
|
|
|
@@ -6718,14 +7034,14 @@ function resolveImageSource(src, options) {
|
|
|
6718
7034
|
}
|
|
6719
7035
|
function isUsableFile2(path) {
|
|
6720
7036
|
try {
|
|
6721
|
-
return
|
|
7037
|
+
return statSync7(path).size > 0;
|
|
6722
7038
|
} catch {
|
|
6723
7039
|
return false;
|
|
6724
7040
|
}
|
|
6725
7041
|
}
|
|
6726
7042
|
async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
6727
|
-
|
|
6728
|
-
|
|
7043
|
+
mkdirSync8(dirname7(cachePath), { recursive: true });
|
|
7044
|
+
mkdirSync8(dirname7(outputPath), { recursive: true });
|
|
6729
7045
|
if (!isUsableFile2(cachePath)) {
|
|
6730
7046
|
const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
|
|
6731
7047
|
const inlinePath = `${tmpPath}.heic`;
|
|
@@ -6743,8 +7059,8 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
|
6743
7059
|
const reason = error instanceof Error ? error.message : String(error);
|
|
6744
7060
|
throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
|
|
6745
7061
|
} finally {
|
|
6746
|
-
|
|
6747
|
-
|
|
7062
|
+
rmSync10(tmpPath, { force: true });
|
|
7063
|
+
rmSync10(inlinePath, { force: true });
|
|
6748
7064
|
}
|
|
6749
7065
|
}
|
|
6750
7066
|
if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
|
|
@@ -6752,7 +7068,7 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
|
|
|
6752
7068
|
}
|
|
6753
7069
|
}
|
|
6754
7070
|
async function prepareHeifImageInputs(html, options) {
|
|
6755
|
-
const outputDir = options.outputDir ??
|
|
7071
|
+
const outputDir = options.outputDir ?? join17(options.downloadDir, PREPARED_HEIF_SUBDIR);
|
|
6756
7072
|
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
|
|
6757
7073
|
const cacheDir = options.cacheDir ?? outputDir;
|
|
6758
7074
|
const preparedAssets = /* @__PURE__ */ new Map();
|
|
@@ -6773,7 +7089,7 @@ async function prepareHeifImageInputs(html, options) {
|
|
|
6773
7089
|
await Promise.all(
|
|
6774
7090
|
[...unique].map(
|
|
6775
7091
|
([filename, source]) => withMediaProbeSlot(
|
|
6776
|
-
() => ensurePreparedJpeg(source,
|
|
7092
|
+
() => ensurePreparedJpeg(source, join17(cacheDir, filename), join17(outputDir, filename), options)
|
|
6777
7093
|
).catch((error) => {
|
|
6778
7094
|
if (!options.onFailure) throw error;
|
|
6779
7095
|
failed.add(filename);
|
|
@@ -6785,8 +7101,8 @@ async function prepareHeifImageInputs(html, options) {
|
|
|
6785
7101
|
if (failed.has(filename)) continue;
|
|
6786
7102
|
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
6787
7103
|
img.setAttribute("src", outputSrc);
|
|
6788
|
-
preparedAssets.set(outputSrc,
|
|
6789
|
-
preparedImages.push({ sourceSrc: src, outputSrc, outputPath:
|
|
7104
|
+
preparedAssets.set(outputSrc, join17(outputDir, filename));
|
|
7105
|
+
preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join17(outputDir, filename) });
|
|
6790
7106
|
}
|
|
6791
7107
|
return { html: document2.toString(), preparedAssets, preparedImages };
|
|
6792
7108
|
}
|
|
@@ -7110,8 +7426,8 @@ function detectShaderTransitionUsage(html) {
|
|
|
7110
7426
|
async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
|
|
7111
7427
|
let filePath = src;
|
|
7112
7428
|
if (isHttpUrl(src)) {
|
|
7113
|
-
const remoteDir =
|
|
7114
|
-
if (!existsSync10(remoteDir))
|
|
7429
|
+
const remoteDir = join18(downloadDir, REMOTE_MEDIA_SUBDIR);
|
|
7430
|
+
if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
|
|
7115
7431
|
try {
|
|
7116
7432
|
filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
|
|
7117
7433
|
onTelemetry: logRemoteDownloadTelemetry
|
|
@@ -7121,7 +7437,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
7121
7437
|
}
|
|
7122
7438
|
} else {
|
|
7123
7439
|
filePath = decodedUrlPath(src);
|
|
7124
|
-
if (!filePath.startsWith("/")) filePath =
|
|
7440
|
+
if (!filePath.startsWith("/")) filePath = join18(baseDir, filePath);
|
|
7125
7441
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
7126
7442
|
}
|
|
7127
7443
|
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
@@ -7282,7 +7598,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
|
|
|
7282
7598
|
workItems.map(async (item) => {
|
|
7283
7599
|
const { html: compiledSub } = await compileHtmlFile(
|
|
7284
7600
|
rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
|
|
7285
|
-
|
|
7601
|
+
dirname8(item.filePath),
|
|
7286
7602
|
downloadDir,
|
|
7287
7603
|
void 0,
|
|
7288
7604
|
urlByAbsentLocalPath,
|
|
@@ -7712,7 +8028,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
|
|
|
7712
8028
|
var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
7713
8029
|
async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
|
|
7714
8030
|
if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
|
|
7715
|
-
if (!existsSync10(remoteDir))
|
|
8031
|
+
if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
|
|
7716
8032
|
const urlToLocal = /* @__PURE__ */ new Map();
|
|
7717
8033
|
await Promise.all(
|
|
7718
8034
|
[...urlSet].map(async (url) => {
|
|
@@ -7759,7 +8075,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
7759
8075
|
return downloadAndRewriteUrls(
|
|
7760
8076
|
urlSet,
|
|
7761
8077
|
html,
|
|
7762
|
-
|
|
8078
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7763
8079
|
"Remote media download failed",
|
|
7764
8080
|
"Localized remote media source(s)"
|
|
7765
8081
|
);
|
|
@@ -7774,7 +8090,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
7774
8090
|
return downloadAndRewriteUrls(
|
|
7775
8091
|
urlSet,
|
|
7776
8092
|
html,
|
|
7777
|
-
|
|
8093
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7778
8094
|
"Remote image download failed",
|
|
7779
8095
|
"Localized remote image source(s)"
|
|
7780
8096
|
);
|
|
@@ -7790,7 +8106,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
7790
8106
|
return downloadAndRewriteUrls(
|
|
7791
8107
|
urlSet,
|
|
7792
8108
|
html,
|
|
7793
|
-
|
|
8109
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7794
8110
|
"Remote background-image download failed",
|
|
7795
8111
|
"Localized remote background-image(s)",
|
|
7796
8112
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -7900,7 +8216,7 @@ function projectStylesheetFontFamilies(html, projectDir) {
|
|
|
7900
8216
|
const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
7901
8217
|
const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
|
|
7902
8218
|
const path = decodedUrlPath(url);
|
|
7903
|
-
const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(
|
|
8219
|
+
const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname8(sheet), path);
|
|
7904
8220
|
return isPathInside2(file, projectDir) && existsSync10(file);
|
|
7905
8221
|
});
|
|
7906
8222
|
if (family && shipsFile) families.push(family);
|
|
@@ -7942,7 +8258,7 @@ function localizeFontFaceUrls(html, downloadDir) {
|
|
|
7942
8258
|
return downloadAndRewriteUrls(
|
|
7943
8259
|
collectFontFaceUrls(html),
|
|
7944
8260
|
html,
|
|
7945
|
-
|
|
8261
|
+
join18(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
7946
8262
|
"Remote font download failed",
|
|
7947
8263
|
"Localized remote font face(s)",
|
|
7948
8264
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -8614,11 +8930,36 @@ function adaptAspectAgnosticResolution(requested, aspectAgnostic, width, height,
|
|
|
8614
8930
|
});
|
|
8615
8931
|
return flipped;
|
|
8616
8932
|
}
|
|
8933
|
+
function resolveEntryHtmlPath(input) {
|
|
8934
|
+
const { projectDir, workDir, entryFile, log } = input;
|
|
8935
|
+
const htmlPath = join19(projectDir, entryFile);
|
|
8936
|
+
const rawEntry = readTextFile2(htmlPath);
|
|
8937
|
+
if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
|
|
8938
|
+
const projectIndexPath = join19(projectDir, "index.html");
|
|
8939
|
+
if (!existsSync11(projectIndexPath)) {
|
|
8940
|
+
throw new Error(
|
|
8941
|
+
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
8942
|
+
);
|
|
8943
|
+
}
|
|
8944
|
+
const standaloneHtml = extractStandaloneEntryFromIndex(
|
|
8945
|
+
readTextFile2(projectIndexPath),
|
|
8946
|
+
entryFile,
|
|
8947
|
+
rawEntry
|
|
8948
|
+
);
|
|
8949
|
+
if (!standaloneHtml) {
|
|
8950
|
+
throw new Error(
|
|
8951
|
+
`Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
|
|
8952
|
+
);
|
|
8953
|
+
}
|
|
8954
|
+
const wrapperPath = join19(workDir, "standalone-entry.html");
|
|
8955
|
+
writeFileSync5(wrapperPath, standaloneHtml, "utf-8");
|
|
8956
|
+
log.info("Extracted standalone entry from index.html host context", { entryFile });
|
|
8957
|
+
return wrapperPath;
|
|
8958
|
+
}
|
|
8617
8959
|
async function runCompileStage(input) {
|
|
8618
8960
|
const {
|
|
8619
8961
|
projectDir,
|
|
8620
8962
|
workDir,
|
|
8621
|
-
htmlPath,
|
|
8622
8963
|
entryFile,
|
|
8623
8964
|
job,
|
|
8624
8965
|
cfg,
|
|
@@ -8629,8 +8970,9 @@ async function runCompileStage(input) {
|
|
|
8629
8970
|
allowSystemFontCapture,
|
|
8630
8971
|
abortSignal
|
|
8631
8972
|
} = input;
|
|
8973
|
+
const htmlPath = resolveEntryHtmlPath({ projectDir, workDir, entryFile, log });
|
|
8632
8974
|
const compileStart = Date.now();
|
|
8633
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
8975
|
+
const compiled = await compileForRender(projectDir, htmlPath, join19(workDir, "downloads"), {
|
|
8634
8976
|
log,
|
|
8635
8977
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
8636
8978
|
allowSystemFontCapture,
|
|
@@ -8638,7 +8980,7 @@ async function runCompileStage(input) {
|
|
|
8638
8980
|
variables: input.variables,
|
|
8639
8981
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
8640
8982
|
pageMotionBlur: input.pageMotionBlur,
|
|
8641
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
8983
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join19(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
8642
8984
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
8643
8985
|
});
|
|
8644
8986
|
assertNotAborted();
|
|
@@ -8695,7 +9037,7 @@ async function runCompileStage(input) {
|
|
|
8695
9037
|
};
|
|
8696
9038
|
await preflightCompositionAssetMediaTypes({
|
|
8697
9039
|
projectDir,
|
|
8698
|
-
compiledDir:
|
|
9040
|
+
compiledDir: join19(workDir, "compiled"),
|
|
8699
9041
|
composition,
|
|
8700
9042
|
signal: abortSignal
|
|
8701
9043
|
});
|
|
@@ -8741,7 +9083,7 @@ async function runCompileStage(input) {
|
|
|
8741
9083
|
|
|
8742
9084
|
// src/services/render/stages/probeStage.ts
|
|
8743
9085
|
import { createHash as createHash7 } from "node:crypto";
|
|
8744
|
-
import { basename as basename5, join as
|
|
9086
|
+
import { basename as basename5, join as join20 } from "node:path";
|
|
8745
9087
|
import { parseHTML as parseHTML6 } from "linkedom";
|
|
8746
9088
|
import {
|
|
8747
9089
|
closeCaptureSession as closeCaptureSession4,
|
|
@@ -8950,7 +9292,7 @@ async function runProbeStage(input) {
|
|
|
8950
9292
|
});
|
|
8951
9293
|
fileServer = await createFileServer({
|
|
8952
9294
|
projectDir,
|
|
8953
|
-
compiledDir:
|
|
9295
|
+
compiledDir: join20(workDir, "compiled"),
|
|
8954
9296
|
port: 0,
|
|
8955
9297
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
8956
9298
|
fps: job.config.fps
|
|
@@ -8980,7 +9322,7 @@ async function runProbeStage(input) {
|
|
|
8980
9322
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
8981
9323
|
probeSession = await createCaptureSession4(
|
|
8982
9324
|
fileServer.url,
|
|
8983
|
-
|
|
9325
|
+
join20(workDir, "probe"),
|
|
8984
9326
|
captureOpts,
|
|
8985
9327
|
null,
|
|
8986
9328
|
probeCfg
|
|
@@ -9065,7 +9407,7 @@ async function runProbeStage(input) {
|
|
|
9065
9407
|
});
|
|
9066
9408
|
probeSession = await createCaptureSession4(
|
|
9067
9409
|
fileServer.url,
|
|
9068
|
-
|
|
9410
|
+
join20(workDir, "probe-screenshot"),
|
|
9069
9411
|
captureOpts,
|
|
9070
9412
|
null,
|
|
9071
9413
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -9103,7 +9445,7 @@ async function runProbeStage(input) {
|
|
|
9103
9445
|
compiled,
|
|
9104
9446
|
resolutions,
|
|
9105
9447
|
projectDir,
|
|
9106
|
-
|
|
9448
|
+
join20(workDir, "downloads")
|
|
9107
9449
|
);
|
|
9108
9450
|
assertNotAborted();
|
|
9109
9451
|
composition.videos = compiled.videos;
|
|
@@ -9294,7 +9636,7 @@ async function runProbeStage(input) {
|
|
|
9294
9636
|
}
|
|
9295
9637
|
await preflightCompositionAssetMediaTypes({
|
|
9296
9638
|
projectDir,
|
|
9297
|
-
compiledDir:
|
|
9639
|
+
compiledDir: join20(workDir, "compiled"),
|
|
9298
9640
|
composition,
|
|
9299
9641
|
signal: abortSignal
|
|
9300
9642
|
});
|
|
@@ -9428,11 +9770,11 @@ function validateRenderDuration(input) {
|
|
|
9428
9770
|
}
|
|
9429
9771
|
|
|
9430
9772
|
// src/services/render/stages/extractVideosStage.ts
|
|
9431
|
-
import { existsSync as
|
|
9432
|
-
import { join as
|
|
9773
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
9774
|
+
import { join as join21 } from "node:path";
|
|
9433
9775
|
import {
|
|
9434
9776
|
classifyVideoExtractionError,
|
|
9435
|
-
createFrameLookupTable,
|
|
9777
|
+
createFrameLookupTable as createFrameLookupTable3,
|
|
9436
9778
|
detectTransfer,
|
|
9437
9779
|
extractAllVideoFrames,
|
|
9438
9780
|
extractMediaMetadata as extractMediaMetadata3,
|
|
@@ -9444,7 +9786,7 @@ import {
|
|
|
9444
9786
|
resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
|
|
9445
9787
|
isHttpUrl as isHttpUrl2,
|
|
9446
9788
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
9447
|
-
runVideoExtractionWithRetry,
|
|
9789
|
+
runVideoExtractionWithRetry as runVideoExtractionWithRetry2,
|
|
9448
9790
|
safeVideoExtractionSourceIdentity,
|
|
9449
9791
|
videoAudioId as videoAudioId2
|
|
9450
9792
|
} from "@hyperframes/engine";
|
|
@@ -9698,9 +10040,9 @@ async function runExtractVideosStage(input) {
|
|
|
9698
10040
|
const probeFailures = await Promise.all(
|
|
9699
10041
|
composition.videos.map(async (v) => {
|
|
9700
10042
|
const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
|
|
9701
|
-
if (!
|
|
10043
|
+
if (!existsSync12(videoPath)) return null;
|
|
9702
10044
|
try {
|
|
9703
|
-
const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await
|
|
10045
|
+
const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
|
|
9704
10046
|
signal: abortSignal,
|
|
9705
10047
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
9706
10048
|
onRetry: () => {
|
|
@@ -9736,7 +10078,7 @@ async function runExtractVideosStage(input) {
|
|
|
9736
10078
|
const probed = await Promise.all(
|
|
9737
10079
|
composition.images.map(async (img) => {
|
|
9738
10080
|
const imgPath = resolveProjectRelativeSrc3(img.src, projectDir, compiledDir);
|
|
9739
|
-
if (!
|
|
10081
|
+
if (!existsSync12(imgPath)) return null;
|
|
9740
10082
|
const colorSpace = await probeColorSpaceSafely(imgPath, log);
|
|
9741
10083
|
if (isHdrColorSpace2(colorSpace)) {
|
|
9742
10084
|
nativeHdrImageIds.add(img.id);
|
|
@@ -9770,14 +10112,14 @@ async function runExtractVideosStage(input) {
|
|
|
9770
10112
|
// because short boundary counts can differ by one frame.
|
|
9771
10113
|
{
|
|
9772
10114
|
fps: job.config.fps,
|
|
9773
|
-
outputDir:
|
|
10115
|
+
outputDir: join21(compiledDir, "__hyperframes_video_frames"),
|
|
9774
10116
|
format: job.config.videoFrameFormat ?? "auto",
|
|
9775
10117
|
toneMapHdrToSdr: rendersSdrOnly(job.config, composition),
|
|
9776
10118
|
timelineEnd: composition.duration,
|
|
9777
10119
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
9778
10120
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
9779
10121
|
deferRangeExtraction,
|
|
9780
|
-
contentKeyedDirs: [
|
|
10122
|
+
contentKeyedDirs: [join21(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
9781
10123
|
urlByAbsentLocalPath,
|
|
9782
10124
|
onProgress: ({ planned = 0, done = 0 }) => {
|
|
9783
10125
|
framesPlanned += planned;
|
|
@@ -9797,7 +10139,7 @@ async function runExtractVideosStage(input) {
|
|
|
9797
10139
|
materializeSymlinks
|
|
9798
10140
|
});
|
|
9799
10141
|
if (extractionResult.extracted.length > 0) {
|
|
9800
|
-
frameLookup =
|
|
10142
|
+
frameLookup = createFrameLookupTable3(
|
|
9801
10143
|
composition.videos,
|
|
9802
10144
|
extractionResult.extracted,
|
|
9803
10145
|
resolveRenderFpsConfig(job.config.fps).value
|
|
@@ -9856,7 +10198,7 @@ async function appendVideoAudioFromProbe(input) {
|
|
|
9856
10198
|
videos.map(async (video) => {
|
|
9857
10199
|
const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
|
|
9858
10200
|
const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
|
|
9859
|
-
if (!isHttpUrl2(readFrom) && !
|
|
10201
|
+
if (!isHttpUrl2(readFrom) && !existsSync12(readFrom)) return null;
|
|
9860
10202
|
const metadata = await extractMediaMetadata3(readFrom);
|
|
9861
10203
|
const window2 = await resolveFinalFrameExtractionWindow2(
|
|
9862
10204
|
readFrom,
|
|
@@ -9876,7 +10218,7 @@ async function appendVideoAudioFromProbe(input) {
|
|
|
9876
10218
|
}
|
|
9877
10219
|
|
|
9878
10220
|
// src/services/render/videoLayerPassthroughPlan.ts
|
|
9879
|
-
import { existsSync as
|
|
10221
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
9880
10222
|
import {
|
|
9881
10223
|
buildVideoLayerInput,
|
|
9882
10224
|
isHttpUrl as isHttpUrl3,
|
|
@@ -9944,7 +10286,7 @@ async function planVideoLayerPassthrough(input) {
|
|
|
9944
10286
|
const { video } = q;
|
|
9945
10287
|
if (isHttpUrl3(video.src)) return { reason: "remote_source" };
|
|
9946
10288
|
const path = resolveProjectRelativeSrc4(video.src, input.projectDir, input.compiledDir);
|
|
9947
|
-
if (!
|
|
10289
|
+
if (!existsSync13(path)) return { reason: "source_missing" };
|
|
9948
10290
|
const layout = await probeVideoLayout(input.page, video.id, input.canvas, input.duration);
|
|
9949
10291
|
if ("reason" in layout) return layout;
|
|
9950
10292
|
const layer = await buildVideoLayerInput(
|
|
@@ -9959,8 +10301,8 @@ async function planVideoLayerPassthrough(input) {
|
|
|
9959
10301
|
}
|
|
9960
10302
|
|
|
9961
10303
|
// src/services/render/stages/audioStage.ts
|
|
9962
|
-
import { existsSync as
|
|
9963
|
-
import { join as
|
|
10304
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
10305
|
+
import { join as join24 } from "node:path";
|
|
9964
10306
|
import {
|
|
9965
10307
|
MIXED_AUDIO_FILENAME,
|
|
9966
10308
|
audioFailureDiagnostics,
|
|
@@ -9970,8 +10312,8 @@ import {
|
|
|
9970
10312
|
} from "@hyperframes/engine";
|
|
9971
10313
|
|
|
9972
10314
|
// src/services/render/audioDeliveryLimit.ts
|
|
9973
|
-
import { existsSync as
|
|
9974
|
-
import { dirname as
|
|
10315
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
10316
|
+
import { dirname as dirname10, join as join23 } from "node:path";
|
|
9975
10317
|
import {
|
|
9976
10318
|
applyAudioFxChain,
|
|
9977
10319
|
describeFfmpegFailure as describeFfmpegFailure3,
|
|
@@ -9982,7 +10324,7 @@ import {
|
|
|
9982
10324
|
// src/services/render/audioPadTrim.ts
|
|
9983
10325
|
import { spawn } from "node:child_process";
|
|
9984
10326
|
import { mkdtempSync as mkdtempSync3 } from "node:fs";
|
|
9985
|
-
import { dirname as
|
|
10327
|
+
import { dirname as dirname9, join as join22 } from "node:path";
|
|
9986
10328
|
import {
|
|
9987
10329
|
DEFAULT_CONFIG,
|
|
9988
10330
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -10261,7 +10603,7 @@ async function constrainAacTruePeak(input) {
|
|
|
10261
10603
|
}
|
|
10262
10604
|
async function cutSource(input, scratchDir, attenuationDb) {
|
|
10263
10605
|
if (!input.sourcePath || !input.rewriteSource) return true;
|
|
10264
|
-
const cutPath =
|
|
10606
|
+
const cutPath = join22(scratchDir, "source.wav");
|
|
10265
10607
|
try {
|
|
10266
10608
|
const result = await input.rewriteSource(input.sourcePath, cutPath, gainFilter(attenuationDb));
|
|
10267
10609
|
if (!result.success) throw new Error(result.error);
|
|
@@ -10275,8 +10617,8 @@ async function cutSource(input, scratchDir, attenuationDb) {
|
|
|
10275
10617
|
async function enforceAacTruePeak(input) {
|
|
10276
10618
|
let scratchDir;
|
|
10277
10619
|
try {
|
|
10278
|
-
scratchDir = mkdtempSync3(
|
|
10279
|
-
const correctedPath =
|
|
10620
|
+
scratchDir = mkdtempSync3(join22(dirname9(input.audioPath), ".true-peak-"));
|
|
10621
|
+
const correctedPath = join22(scratchDir, "audio.m4a");
|
|
10280
10622
|
const measurements = [];
|
|
10281
10623
|
let attenuationDb = 0;
|
|
10282
10624
|
let sourcePeakDbfs;
|
|
@@ -10467,31 +10809,57 @@ async function runFfprobeJson(args, signal) {
|
|
|
10467
10809
|
}
|
|
10468
10810
|
|
|
10469
10811
|
// src/services/render/audioDeliveryLimit.ts
|
|
10470
|
-
var
|
|
10471
|
-
var
|
|
10812
|
+
var LIMIT_PASSES = 3;
|
|
10813
|
+
var LOWEST_CEILING_DBTP = AAC_DELIVERY_TRUE_PEAK_DBFS - 2;
|
|
10814
|
+
var PASS_HEADROOM_DB = 0.05;
|
|
10815
|
+
var limitChain = (ceiling) => ({
|
|
10472
10816
|
version: 1,
|
|
10473
|
-
nodes: [{ type: "truepeak", id: "delivery", params: { ceiling
|
|
10474
|
-
};
|
|
10817
|
+
nodes: [{ type: "truepeak", id: "delivery", params: { ceiling } }]
|
|
10818
|
+
});
|
|
10475
10819
|
async function limitDeliveryPeaks(input, deps = {
|
|
10476
10820
|
measure: measureTruePeakDbfs,
|
|
10477
10821
|
applyChain: applyAudioFxChain,
|
|
10478
10822
|
encode: encodeAac2,
|
|
10479
10823
|
rename: renameWithRetry6,
|
|
10480
|
-
sourceExists:
|
|
10824
|
+
sourceExists: existsSync14
|
|
10481
10825
|
}) {
|
|
10482
10826
|
const { audioPath, sourcePath, signal, timeoutMs } = input;
|
|
10483
10827
|
if (!deps.sourceExists(sourcePath)) return { success: true };
|
|
10484
10828
|
const peak = await deps.measure(audioPath, { timeoutMs, signal });
|
|
10485
10829
|
if (peak <= AAC_DELIVERY_TRUE_PEAK_DBFS) return { success: true };
|
|
10486
|
-
const
|
|
10830
|
+
const alternating = ["a", "b"].map(
|
|
10831
|
+
(k) => join23(dirname10(sourcePath), `audio-source-limited-${k}.wav`)
|
|
10832
|
+
);
|
|
10833
|
+
let encodedFrom;
|
|
10834
|
+
let ceiling = AAC_DELIVERY_TRUE_PEAK_DBFS;
|
|
10835
|
+
for (let pass = 0; pass < LIMIT_PASSES; pass++) {
|
|
10836
|
+
const limitedPath = alternating[pass % 2];
|
|
10837
|
+
const result = await limitPass(input, deps, ceiling, limitedPath);
|
|
10838
|
+
if ("encodeFailure" in result) return result.encodeFailure;
|
|
10839
|
+
if ("limitFailed" in result) break;
|
|
10840
|
+
encodedFrom = limitedPath;
|
|
10841
|
+
if (result.peak <= AAC_DELIVERY_TRUE_PEAK_DBFS || ceiling <= LOWEST_CEILING_DBTP) break;
|
|
10842
|
+
ceiling = Math.max(
|
|
10843
|
+
ceiling - (result.peak - AAC_DELIVERY_TRUE_PEAK_DBFS) - PASS_HEADROOM_DB,
|
|
10844
|
+
LOWEST_CEILING_DBTP
|
|
10845
|
+
);
|
|
10846
|
+
}
|
|
10847
|
+
if (encodedFrom) deps.rename(encodedFrom, sourcePath, { copyFallback: true });
|
|
10848
|
+
return { success: true };
|
|
10849
|
+
}
|
|
10850
|
+
async function limitPass(input, deps, ceiling, limitedPath) {
|
|
10851
|
+
const { audioPath, sourcePath, signal, timeoutMs } = input;
|
|
10487
10852
|
try {
|
|
10488
|
-
await deps.applyChain(sourcePath,
|
|
10853
|
+
await deps.applyChain(sourcePath, limitChain(ceiling), limitedPath, {
|
|
10854
|
+
trackId: "delivery",
|
|
10855
|
+
signal
|
|
10856
|
+
});
|
|
10489
10857
|
} catch (error) {
|
|
10490
10858
|
if (signal?.aborted) throw error;
|
|
10491
10859
|
input.warn?.("[audio] true-peak limit failed, the whole-mix cut applies", {
|
|
10492
10860
|
error: error instanceof Error ? error.message : String(error)
|
|
10493
10861
|
});
|
|
10494
|
-
return {
|
|
10862
|
+
return { limitFailed: true };
|
|
10495
10863
|
}
|
|
10496
10864
|
const encoded = await deps.encode(limitedPath, audioPath, input.targetDurationSeconds, {
|
|
10497
10865
|
signal,
|
|
@@ -10499,13 +10867,14 @@ async function limitDeliveryPeaks(input, deps = {
|
|
|
10499
10867
|
});
|
|
10500
10868
|
if (!encoded.success) {
|
|
10501
10869
|
return {
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10870
|
+
encodeFailure: {
|
|
10871
|
+
success: false,
|
|
10872
|
+
error: `[audioDeliveryLimit] ${describeFfmpegFailure3(encoded, timeoutMs)}`,
|
|
10873
|
+
failureReason: encoded.failureReason
|
|
10874
|
+
}
|
|
10505
10875
|
};
|
|
10506
10876
|
}
|
|
10507
|
-
deps.
|
|
10508
|
-
return { success: true };
|
|
10877
|
+
return { peak: await deps.measure(audioPath, { timeoutMs, signal }) };
|
|
10509
10878
|
}
|
|
10510
10879
|
|
|
10511
10880
|
// src/services/render/stages/audioStage.ts
|
|
@@ -10531,7 +10900,7 @@ function runAudioStage(input) {
|
|
|
10531
10900
|
}
|
|
10532
10901
|
async function runAudioStageScoped(input) {
|
|
10533
10902
|
const stageStart = Date.now();
|
|
10534
|
-
const pcmSourcePath =
|
|
10903
|
+
const pcmSourcePath = join24(input.workDir, "audio-source.wav");
|
|
10535
10904
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
10536
10905
|
let deliverySourcePath;
|
|
10537
10906
|
try {
|
|
@@ -10547,7 +10916,7 @@ async function runAudioStageScoped(input) {
|
|
|
10547
10916
|
);
|
|
10548
10917
|
audioLoweredDb = guard.loweredDb;
|
|
10549
10918
|
input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
|
|
10550
|
-
if (input.keepDeliverySource && !guard.sourceStale &&
|
|
10919
|
+
if (input.keepDeliverySource && !guard.sourceStale && existsSync15(pcmSourcePath)) {
|
|
10551
10920
|
deliverySourcePath = pcmSourcePath;
|
|
10552
10921
|
}
|
|
10553
10922
|
}
|
|
@@ -10596,7 +10965,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
10596
10965
|
assertNotAborted,
|
|
10597
10966
|
log
|
|
10598
10967
|
} = input;
|
|
10599
|
-
const audioOutputPath =
|
|
10968
|
+
const audioOutputPath = join24(workDir, MIXED_AUDIO_FILENAME);
|
|
10600
10969
|
let hasAudio = false;
|
|
10601
10970
|
let audioError;
|
|
10602
10971
|
let audioFailures;
|
|
@@ -10607,7 +10976,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
10607
10976
|
audioResult = await processCompositionAudio(
|
|
10608
10977
|
audios,
|
|
10609
10978
|
projectDir,
|
|
10610
|
-
|
|
10979
|
+
join24(workDir, "audio-work"),
|
|
10611
10980
|
audioOutputPath,
|
|
10612
10981
|
duration,
|
|
10613
10982
|
abortSignal,
|
|
@@ -10675,8 +11044,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
10675
11044
|
}
|
|
10676
11045
|
|
|
10677
11046
|
// src/services/render/stages/captureStage.ts
|
|
10678
|
-
import { existsSync as
|
|
10679
|
-
import { join as
|
|
11047
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
|
|
11048
|
+
import { join as join25 } from "node:path";
|
|
10680
11049
|
import {
|
|
10681
11050
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
10682
11051
|
frameFileExtension,
|
|
@@ -10746,16 +11115,16 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
|
|
|
10746
11115
|
}
|
|
10747
11116
|
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
10748
11117
|
let total = sampleDirectoryBytes(framesDir);
|
|
10749
|
-
if (!
|
|
10750
|
-
for (const attempt of
|
|
11118
|
+
if (!existsSync16(workDir)) return total;
|
|
11119
|
+
for (const attempt of readdirSync4(workDir)) {
|
|
10751
11120
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
10752
|
-
total += sampleDirectoryBytes(
|
|
11121
|
+
total += sampleDirectoryBytes(join25(workDir, attempt));
|
|
10753
11122
|
continue;
|
|
10754
11123
|
}
|
|
10755
11124
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
10756
|
-
const attemptDir =
|
|
10757
|
-
for (const name of
|
|
10758
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
11125
|
+
const attemptDir = join25(workDir, attempt);
|
|
11126
|
+
for (const name of readdirSync4(attemptDir)) {
|
|
11127
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join25(attemptDir, name));
|
|
10759
11128
|
}
|
|
10760
11129
|
}
|
|
10761
11130
|
return total;
|
|
@@ -11049,8 +11418,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
|
|
|
11049
11418
|
}
|
|
11050
11419
|
|
|
11051
11420
|
// src/services/render/stages/captureHdrStage.ts
|
|
11052
|
-
import { existsSync as
|
|
11053
|
-
import { join as
|
|
11421
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "node:fs";
|
|
11422
|
+
import { join as join29 } from "node:path";
|
|
11054
11423
|
import {
|
|
11055
11424
|
closeCaptureSession as closeCaptureSession7,
|
|
11056
11425
|
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
@@ -11122,7 +11491,7 @@ function createHdrImageTransferCache(options = {}) {
|
|
|
11122
11491
|
}
|
|
11123
11492
|
|
|
11124
11493
|
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
11125
|
-
import { join as
|
|
11494
|
+
import { join as join26 } from "node:path";
|
|
11126
11495
|
import {
|
|
11127
11496
|
TRANSITIONS,
|
|
11128
11497
|
crossfade
|
|
@@ -11247,7 +11616,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
11247
11616
|
);
|
|
11248
11617
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
11249
11618
|
writeFileExclusiveSync(
|
|
11250
|
-
|
|
11619
|
+
join26(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
11251
11620
|
normalCanvas
|
|
11252
11621
|
);
|
|
11253
11622
|
}
|
|
@@ -11279,7 +11648,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
11279
11648
|
}
|
|
11280
11649
|
|
|
11281
11650
|
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
11282
|
-
import { join as
|
|
11651
|
+
import { join as join28 } from "node:path";
|
|
11283
11652
|
import {
|
|
11284
11653
|
TRANSITIONS as TRANSITIONS2,
|
|
11285
11654
|
closeCaptureSession as closeCaptureSession6,
|
|
@@ -11293,9 +11662,9 @@ import {
|
|
|
11293
11662
|
// src/services/shaderTransitionWorkerPool.ts
|
|
11294
11663
|
import { Worker } from "node:worker_threads";
|
|
11295
11664
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
11296
|
-
import { dirname as
|
|
11665
|
+
import { dirname as dirname11, join as join27 } from "node:path";
|
|
11297
11666
|
import { createRequire } from "node:module";
|
|
11298
|
-
import { existsSync as
|
|
11667
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
11299
11668
|
import { cpus as cpus2 } from "node:os";
|
|
11300
11669
|
function resolveWorkerEntry(explicit) {
|
|
11301
11670
|
if (explicit && explicit.length > 0) {
|
|
@@ -11306,10 +11675,10 @@ function resolveWorkerEntry(explicit) {
|
|
|
11306
11675
|
const isTs = override.endsWith(".ts");
|
|
11307
11676
|
return { path: override, isTs };
|
|
11308
11677
|
}
|
|
11309
|
-
const moduleDir =
|
|
11310
|
-
const jsPath =
|
|
11311
|
-
if (
|
|
11312
|
-
const tsPath =
|
|
11678
|
+
const moduleDir = dirname11(fileURLToPath3(import.meta.url));
|
|
11679
|
+
const jsPath = join27(moduleDir, "shaderTransitionWorker.js");
|
|
11680
|
+
if (existsSync17(jsPath)) return { path: jsPath, isTs: false };
|
|
11681
|
+
const tsPath = join27(moduleDir, "shaderTransitionWorker.ts");
|
|
11313
11682
|
return { path: tsPath, isTs: true };
|
|
11314
11683
|
}
|
|
11315
11684
|
function buildExecArgv(entryIsTs) {
|
|
@@ -11677,7 +12046,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
11677
12046
|
);
|
|
11678
12047
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
11679
12048
|
writeFileExclusiveSync(
|
|
11680
|
-
|
|
12049
|
+
join28(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
11681
12050
|
canvas
|
|
11682
12051
|
);
|
|
11683
12052
|
}
|
|
@@ -11868,9 +12237,9 @@ async function runCaptureHdrStage(input) {
|
|
|
11868
12237
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
11869
12238
|
}
|
|
11870
12239
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
11871
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
11872
|
-
if (debugDumpDir && !
|
|
11873
|
-
|
|
12240
|
+
const debugDumpDir = debugDumpEnabled ? join29(framesDir, "debug-composite") : null;
|
|
12241
|
+
if (debugDumpDir && !existsSync18(debugDumpDir)) {
|
|
12242
|
+
mkdirSync10(debugDumpDir, { recursive: true });
|
|
11874
12243
|
}
|
|
11875
12244
|
const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
|
|
11876
12245
|
const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
|
|
@@ -12026,16 +12395,16 @@ async function runCaptureHdrStage(input) {
|
|
|
12026
12395
|
// src/services/render/stages/encodeStage.ts
|
|
12027
12396
|
import {
|
|
12028
12397
|
copyFileSync as copyFileSync4,
|
|
12029
|
-
existsSync as
|
|
12030
|
-
mkdirSync as
|
|
12031
|
-
readdirSync as
|
|
12398
|
+
existsSync as existsSync19,
|
|
12399
|
+
mkdirSync as mkdirSync11,
|
|
12400
|
+
readdirSync as readdirSync5,
|
|
12032
12401
|
readFileSync as readFileSync8,
|
|
12033
|
-
rmSync as
|
|
12034
|
-
statSync as
|
|
12035
|
-
writeFileSync as
|
|
12402
|
+
rmSync as rmSync11,
|
|
12403
|
+
statSync as statSync8,
|
|
12404
|
+
writeFileSync as writeFileSync6
|
|
12036
12405
|
} from "node:fs";
|
|
12037
12406
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
12038
|
-
import { dirname as
|
|
12407
|
+
import { dirname as dirname12, join as join30 } from "node:path";
|
|
12039
12408
|
import {
|
|
12040
12409
|
encodeFramesChunkedConcat,
|
|
12041
12410
|
encodeFramesFromDir,
|
|
@@ -12128,12 +12497,12 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
|
12128
12497
|
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
12129
12498
|
return gifEncodeFailure(startTime, outputPath, error);
|
|
12130
12499
|
}
|
|
12131
|
-
|
|
12500
|
+
writeFileSync6(outputPath, gif);
|
|
12132
12501
|
return null;
|
|
12133
12502
|
}
|
|
12134
12503
|
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
12135
12504
|
const startTime = Date.now();
|
|
12136
|
-
const files =
|
|
12505
|
+
const files = readdirSync5(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
|
|
12137
12506
|
const frameCount = files.length;
|
|
12138
12507
|
if (frameCount === 0) {
|
|
12139
12508
|
return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
|
|
@@ -12160,7 +12529,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
12160
12529
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
12161
12530
|
if (failure) return failure;
|
|
12162
12531
|
}
|
|
12163
|
-
const fileSize =
|
|
12532
|
+
const fileSize = existsSync19(outputPath) ? statSync8(outputPath).size : 0;
|
|
12164
12533
|
return {
|
|
12165
12534
|
success: true,
|
|
12166
12535
|
outputPath,
|
|
@@ -12169,7 +12538,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
12169
12538
|
fileSize
|
|
12170
12539
|
};
|
|
12171
12540
|
} finally {
|
|
12172
|
-
|
|
12541
|
+
rmSync11(input.palettePath, { force: true });
|
|
12173
12542
|
}
|
|
12174
12543
|
}
|
|
12175
12544
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -12246,7 +12615,7 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
12246
12615
|
await new Promise((resolve11) => wake = resolve11);
|
|
12247
12616
|
}
|
|
12248
12617
|
if (signal.aborted) return;
|
|
12249
|
-
const mergedPath =
|
|
12618
|
+
const mergedPath = join30(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
12250
12619
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
12251
12620
|
landed.delete(next);
|
|
12252
12621
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -12321,19 +12690,19 @@ async function runEncodeStage(input) {
|
|
|
12321
12690
|
const stage5Start = Date.now();
|
|
12322
12691
|
if (isPngSequence) {
|
|
12323
12692
|
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
12324
|
-
if (!
|
|
12325
|
-
const captured =
|
|
12693
|
+
if (!existsSync19(outputPath)) mkdirSync11(outputPath, { recursive: true });
|
|
12694
|
+
const captured = readdirSync5(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
12326
12695
|
if (captured.length === 0) {
|
|
12327
12696
|
throw new Error(
|
|
12328
12697
|
`[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
|
|
12329
12698
|
);
|
|
12330
12699
|
}
|
|
12331
12700
|
captured.forEach((name, i) => {
|
|
12332
|
-
const dst =
|
|
12333
|
-
copyFileSync4(
|
|
12701
|
+
const dst = join30(outputPath, formatExportFrameName(i, "png"));
|
|
12702
|
+
copyFileSync4(join30(framesDir, name), dst);
|
|
12334
12703
|
});
|
|
12335
|
-
if (hasAudio && audioOutputPath &&
|
|
12336
|
-
copyFileSync4(audioOutputPath,
|
|
12704
|
+
if (hasAudio && audioOutputPath && existsSync19(audioOutputPath)) {
|
|
12705
|
+
copyFileSync4(audioOutputPath, join30(outputPath, MIXED_AUDIO_FILENAME2));
|
|
12337
12706
|
log.info(
|
|
12338
12707
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
12339
12708
|
);
|
|
@@ -12353,7 +12722,7 @@ async function runEncodeStage(input) {
|
|
|
12353
12722
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
12354
12723
|
fps: job.config.fps,
|
|
12355
12724
|
loop,
|
|
12356
|
-
palettePath:
|
|
12725
|
+
palettePath: join30(dirname12(videoOnlyPath), "gif-palette.png"),
|
|
12357
12726
|
preserveAlpha: needsAlpha,
|
|
12358
12727
|
signal: abortSignal,
|
|
12359
12728
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -12559,14 +12928,14 @@ function sampleDirectoryBytes(dir) {
|
|
|
12559
12928
|
if (!current) continue;
|
|
12560
12929
|
let entries = [];
|
|
12561
12930
|
try {
|
|
12562
|
-
entries =
|
|
12931
|
+
entries = readdirSync6(current);
|
|
12563
12932
|
} catch {
|
|
12564
12933
|
continue;
|
|
12565
12934
|
}
|
|
12566
12935
|
for (const name of entries) {
|
|
12567
|
-
const full =
|
|
12936
|
+
const full = join31(current, name);
|
|
12568
12937
|
try {
|
|
12569
|
-
const st =
|
|
12938
|
+
const st = statSync9(full);
|
|
12570
12939
|
if (st.isDirectory()) {
|
|
12571
12940
|
stack.push(full);
|
|
12572
12941
|
} else if (st.isFile()) {
|
|
@@ -12714,18 +13083,14 @@ function hasUsableVideoDimensions(metadata) {
|
|
|
12714
13083
|
return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
|
|
12715
13084
|
}
|
|
12716
13085
|
function collectVideoMetadataHints(extractedVideos) {
|
|
12717
|
-
return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
|
|
12718
|
-
id: video.videoId,
|
|
12719
|
-
width: video.metadata.width,
|
|
12720
|
-
height: video.metadata.height
|
|
12721
|
-
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
13086
|
+
return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({ id: video.videoId, ...displaySize(video.metadata) })).sort((a, b) => a.id.localeCompare(b.id));
|
|
12722
13087
|
}
|
|
12723
13088
|
function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
12724
13089
|
const ranges = [];
|
|
12725
13090
|
let rangeStart = null;
|
|
12726
13091
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
12727
|
-
const framePath =
|
|
12728
|
-
const missing = !
|
|
13092
|
+
const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
13093
|
+
const missing = !existsSync20(framePath) || statSync9(framePath).size <= 8;
|
|
12729
13094
|
if (missing && rangeStart === null) {
|
|
12730
13095
|
rangeStart = frameIndex;
|
|
12731
13096
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -12741,7 +13106,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
12741
13106
|
function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
|
|
12742
13107
|
const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
|
|
12743
13108
|
const batches = [];
|
|
12744
|
-
const outputDirFor = (batchIndex, workerId) =>
|
|
13109
|
+
const outputDirFor = (batchIndex, workerId) => join31(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
|
|
12745
13110
|
if (stride > 1) {
|
|
12746
13111
|
const spans = /* @__PURE__ */ new Map();
|
|
12747
13112
|
for (const range of ranges) {
|
|
@@ -12801,8 +13166,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
12801
13166
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
12802
13167
|
}
|
|
12803
13168
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
12804
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
12805
|
-
return
|
|
13169
|
+
if (platform2 === "win32" || platform2 === "darwin") return join31(systemTempDir, "hf-render-");
|
|
13170
|
+
return join31(dirname13(outputPath), `work-${jobId}-`);
|
|
12806
13171
|
}
|
|
12807
13172
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
12808
13173
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -12849,8 +13214,8 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
12849
13214
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
12850
13215
|
let captured = 0;
|
|
12851
13216
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
12852
|
-
const framePath =
|
|
12853
|
-
if (
|
|
13217
|
+
const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
13218
|
+
if (existsSync20(framePath)) captured++;
|
|
12854
13219
|
}
|
|
12855
13220
|
return captured;
|
|
12856
13221
|
}
|
|
@@ -12875,7 +13240,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
12875
13240
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
12876
13241
|
});
|
|
12877
13242
|
pendingTransientRetry = false;
|
|
12878
|
-
const attemptWorkDir =
|
|
13243
|
+
const attemptWorkDir = join31(options.workDir, `capture-attempt-${attempt}`);
|
|
12879
13244
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
12880
13245
|
missingRanges,
|
|
12881
13246
|
currentWorkers,
|
|
@@ -13507,19 +13872,19 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
13507
13872
|
};
|
|
13508
13873
|
}
|
|
13509
13874
|
function resolveRenderDebugDir() {
|
|
13510
|
-
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(
|
|
13511
|
-
return
|
|
13875
|
+
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname13(fileURLToPath4(import.meta.url)), "../..");
|
|
13876
|
+
return join31(producerRoot, ".debug");
|
|
13512
13877
|
}
|
|
13513
13878
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
13514
13879
|
validateHlsRenderConfig(job.config);
|
|
13515
13880
|
validateDeliveryEncoderConfig(job.config);
|
|
13516
13881
|
const debugDir = resolveRenderDebugDir();
|
|
13517
|
-
const outputDir =
|
|
13518
|
-
if (!
|
|
13519
|
-
const workDir = job.config.debug ?
|
|
13882
|
+
const outputDir = dirname13(outputPath);
|
|
13883
|
+
if (!existsSync20(outputDir)) mkdirSync12(outputDir, { recursive: true });
|
|
13884
|
+
const workDir = job.config.debug ? join31(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
13520
13885
|
const pipelineStart = Date.now();
|
|
13521
13886
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
13522
|
-
const logPath = job.config.debug ?
|
|
13887
|
+
const logPath = job.config.debug ? join31(workDir, "render.log") : null;
|
|
13523
13888
|
const execution = new RenderExecutionContext({
|
|
13524
13889
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
13525
13890
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -13533,7 +13898,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
13533
13898
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
13534
13899
|
return;
|
|
13535
13900
|
}
|
|
13536
|
-
|
|
13901
|
+
rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
13537
13902
|
});
|
|
13538
13903
|
try {
|
|
13539
13904
|
await executeRenderPipeline({
|
|
@@ -13581,7 +13946,7 @@ async function executeRenderPipeline(input) {
|
|
|
13581
13946
|
imageDecodeFailures: 0
|
|
13582
13947
|
};
|
|
13583
13948
|
let hdrPerf;
|
|
13584
|
-
const perfOutputPath =
|
|
13949
|
+
const perfOutputPath = join31(workDir, "perf-summary.json");
|
|
13585
13950
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
13586
13951
|
const observability = new RenderObservabilityRecorder({
|
|
13587
13952
|
pipelineStartMs: pipelineStart,
|
|
@@ -13657,7 +14022,7 @@ async function executeRenderPipeline(input) {
|
|
|
13657
14022
|
job.startedAt = /* @__PURE__ */ new Date();
|
|
13658
14023
|
assertNotAborted();
|
|
13659
14024
|
assertConfiguredFfmpegBinariesExist();
|
|
13660
|
-
if (!
|
|
14025
|
+
if (!existsSync20(workDir)) mkdirSync12(workDir, { recursive: true });
|
|
13661
14026
|
if (job.config.debug) {
|
|
13662
14027
|
log.info("[Render] Debug artifacts enabled", { workDir, logPath });
|
|
13663
14028
|
}
|
|
@@ -13686,36 +14051,11 @@ async function executeRenderPipeline(input) {
|
|
|
13686
14051
|
requestedWorkers: job.config.workers ?? "auto"
|
|
13687
14052
|
});
|
|
13688
14053
|
const entryFile = job.config.entryFile || "index.html";
|
|
13689
|
-
|
|
13690
|
-
if (!
|
|
14054
|
+
const htmlPath = join31(projectDir, entryFile);
|
|
14055
|
+
if (!existsSync20(htmlPath)) {
|
|
13691
14056
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
13692
14057
|
}
|
|
13693
14058
|
assertNotAborted();
|
|
13694
|
-
const rawEntry = readTextFile2(htmlPath);
|
|
13695
|
-
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
13696
|
-
const wrapperPath = join28(workDir, "standalone-entry.html");
|
|
13697
|
-
const projectIndexPath = join28(projectDir, "index.html");
|
|
13698
|
-
if (!existsSync19(projectIndexPath)) {
|
|
13699
|
-
throw new Error(
|
|
13700
|
-
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
13701
|
-
);
|
|
13702
|
-
}
|
|
13703
|
-
const standaloneHtml = extractStandaloneEntryFromIndex(
|
|
13704
|
-
readTextFile2(projectIndexPath),
|
|
13705
|
-
entryFile,
|
|
13706
|
-
rawEntry
|
|
13707
|
-
);
|
|
13708
|
-
if (!standaloneHtml) {
|
|
13709
|
-
throw new Error(
|
|
13710
|
-
`Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
|
|
13711
|
-
);
|
|
13712
|
-
}
|
|
13713
|
-
writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
|
|
13714
|
-
htmlPath = wrapperPath;
|
|
13715
|
-
log.info("Extracted standalone entry from index.html host context", {
|
|
13716
|
-
entryFile
|
|
13717
|
-
});
|
|
13718
|
-
}
|
|
13719
14059
|
const stage1Start = Date.now();
|
|
13720
14060
|
updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress, {
|
|
13721
14061
|
code: "compile"
|
|
@@ -13727,7 +14067,6 @@ async function executeRenderPipeline(input) {
|
|
|
13727
14067
|
() => runCompileStage({
|
|
13728
14068
|
projectDir,
|
|
13729
14069
|
workDir,
|
|
13730
|
-
htmlPath,
|
|
13731
14070
|
entryFile,
|
|
13732
14071
|
job,
|
|
13733
14072
|
cfg,
|
|
@@ -13889,7 +14228,7 @@ async function executeRenderPipeline(input) {
|
|
|
13889
14228
|
canvas: { width, height },
|
|
13890
14229
|
page: probeSession.page,
|
|
13891
14230
|
projectDir,
|
|
13892
|
-
compiledDir:
|
|
14231
|
+
compiledDir: join31(workDir, "compiled")
|
|
13893
14232
|
});
|
|
13894
14233
|
if (videoLayerPlan.videoId === void 0) {
|
|
13895
14234
|
log.info(`[Render] Video layer passthrough off: ${videoLayerPlan.reason}`);
|
|
@@ -13907,7 +14246,22 @@ async function executeRenderPipeline(input) {
|
|
|
13907
14246
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
13908
14247
|
code: "extract_video"
|
|
13909
14248
|
});
|
|
13910
|
-
const compiledDir =
|
|
14249
|
+
const compiledDir = join31(workDir, "compiled");
|
|
14250
|
+
const segmentsCapture = (layeredOrEffectRoute) => shouldSegmentCapture({
|
|
14251
|
+
env: process.env,
|
|
14252
|
+
durationSeconds: probeResult.duration,
|
|
14253
|
+
outputFormat,
|
|
14254
|
+
layeredOrEffectRoute,
|
|
14255
|
+
streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, probeResult.duration)
|
|
14256
|
+
});
|
|
14257
|
+
const segmentedForWindow = segmentsCapture(compiled.hasShaderTransitions);
|
|
14258
|
+
const windowVideoFrames = job.config.motionBlur === void 0 && shouldWindowVideoFrames({ env: process.env, segmented: segmentedForWindow });
|
|
14259
|
+
if (!windowVideoFrames && shouldWindowVideoFrames({ env: process.env, segmented: true })) {
|
|
14260
|
+
const reason = job.config.motionBlur !== void 0 ? "motion blur" : outputFormat !== "mp4" && outputFormat !== "mov" ? `${outputFormat} output` : "capture is not segmented";
|
|
14261
|
+
log.info("[Render] HF_VIDEO_FRAME_WINDOW is set but this render extracts every frame", {
|
|
14262
|
+
reason
|
|
14263
|
+
});
|
|
14264
|
+
}
|
|
13911
14265
|
const extractResult = await observeRenderStage(
|
|
13912
14266
|
observability,
|
|
13913
14267
|
"video_extract",
|
|
@@ -13924,6 +14278,7 @@ async function executeRenderPipeline(input) {
|
|
|
13924
14278
|
assertNotAborted,
|
|
13925
14279
|
// Local staging can use links; distributed plan() alone requires real copies.
|
|
13926
14280
|
materializeSymlinks: false,
|
|
14281
|
+
deferRangeExtraction: windowVideoFrames ? true : void 0,
|
|
13927
14282
|
onProgress
|
|
13928
14283
|
})
|
|
13929
14284
|
);
|
|
@@ -14020,7 +14375,7 @@ async function executeRenderPipeline(input) {
|
|
|
14020
14375
|
try {
|
|
14021
14376
|
fileServer = await createFileServer({
|
|
14022
14377
|
projectDir,
|
|
14023
|
-
compiledDir:
|
|
14378
|
+
compiledDir: join31(workDir, "compiled"),
|
|
14024
14379
|
port: 0,
|
|
14025
14380
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
14026
14381
|
fps: job.config.fps
|
|
@@ -14038,8 +14393,8 @@ async function executeRenderPipeline(input) {
|
|
|
14038
14393
|
if (!activeFileServer) {
|
|
14039
14394
|
throw new Error("File server failed to initialize before frame capture");
|
|
14040
14395
|
}
|
|
14041
|
-
const framesDir =
|
|
14042
|
-
if (!
|
|
14396
|
+
const framesDir = join31(workDir, "captured-frames");
|
|
14397
|
+
if (!existsSync20(framesDir)) mkdirSync12(framesDir, { recursive: true });
|
|
14043
14398
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
14044
14399
|
code: "start_browsers"
|
|
14045
14400
|
});
|
|
@@ -14117,7 +14472,55 @@ async function executeRenderPipeline(input) {
|
|
|
14117
14472
|
});
|
|
14118
14473
|
const buildPrepCaptureOptions = () => stopPrepSessionOnScriptFailure(buildCaptureOptions());
|
|
14119
14474
|
const buildWorkerCaptureOptions = () => withScriptFailureStop(buildCaptureOptions(), job, log);
|
|
14120
|
-
|
|
14475
|
+
let videoFrameWindow = windowVideoFrames && (extractionResult?.extracted.some((video) => video.deferredRange) ?? false);
|
|
14476
|
+
if (windowVideoFrames) {
|
|
14477
|
+
const extracted = extractionResult?.extracted ?? [];
|
|
14478
|
+
log.info("[Render] Video frame window", {
|
|
14479
|
+
windowed: extracted.filter((video) => video.deferredRange).length,
|
|
14480
|
+
fullyExtracted: extracted.filter((video) => !video.deferredRange).length
|
|
14481
|
+
});
|
|
14482
|
+
}
|
|
14483
|
+
const buildSegmentVideoFrames = () => trackSegmentFramePeak(
|
|
14484
|
+
createSegmentVideoFrames({
|
|
14485
|
+
videos: composition.videos,
|
|
14486
|
+
extracted: extractionResult?.extracted ?? [],
|
|
14487
|
+
fps: job.config.fps,
|
|
14488
|
+
rootDir: workDir,
|
|
14489
|
+
cfg,
|
|
14490
|
+
injectorOptions: frameCacheOptions(cfg, workerCount),
|
|
14491
|
+
abortSignal: executionSignal
|
|
14492
|
+
}),
|
|
14493
|
+
workDir,
|
|
14494
|
+
(bytes) => updateCaptureObservability({ videoFrameWindowPeakBytes: bytes })
|
|
14495
|
+
);
|
|
14496
|
+
const createRenderVideoFrameInjector = () => createVideoFrameInjector2(frameLookup, frameCacheOptions(cfg, workerCount));
|
|
14497
|
+
const extractDeferredVideoFrames = async () => {
|
|
14498
|
+
videoFrameWindow = false;
|
|
14499
|
+
updateCaptureObservability({ videoFrameWindow: "off" });
|
|
14500
|
+
const extracted = extractionResult?.extracted ?? [];
|
|
14501
|
+
const videoCount = extracted.filter((video) => video.deferredRange).length;
|
|
14502
|
+
const startedAt = Date.now();
|
|
14503
|
+
log.info("[Render] Extracting the frames of the videos a window would have held back", {
|
|
14504
|
+
videoCount
|
|
14505
|
+
});
|
|
14506
|
+
await observeRenderStage(
|
|
14507
|
+
observability,
|
|
14508
|
+
"video_extract_deferred",
|
|
14509
|
+
{ videoCount },
|
|
14510
|
+
() => extractAllDeferredFrames(extracted, {
|
|
14511
|
+
fps: job.config.fps,
|
|
14512
|
+
cfg,
|
|
14513
|
+
signal: executionSignal,
|
|
14514
|
+
maxTransientRetries: resolveVideoExtractionPolicy().maxTransientRetries,
|
|
14515
|
+
onProgress: (done, total) => reportExtractProgress(job, done, total, onProgress)
|
|
14516
|
+
})
|
|
14517
|
+
);
|
|
14518
|
+
assertNotAborted();
|
|
14519
|
+
log.info("[Render] Extracted the deferred video frames", {
|
|
14520
|
+
videoCount,
|
|
14521
|
+
elapsedMs: Date.now() - startedAt
|
|
14522
|
+
});
|
|
14523
|
+
};
|
|
14121
14524
|
let captureCalibration;
|
|
14122
14525
|
const htmlInCanvasDetected = compiled.renderModeHints.reasons.some(
|
|
14123
14526
|
(r) => r.code === "htmlInCanvas"
|
|
@@ -14283,32 +14686,41 @@ async function executeRenderPipeline(input) {
|
|
|
14283
14686
|
updateJobStatus(job, "rendering", "Measuring capture speed", 25, onProgress, {
|
|
14284
14687
|
code: "start_browsers"
|
|
14285
14688
|
});
|
|
14286
|
-
const
|
|
14287
|
-
|
|
14288
|
-
"
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
14307
|
-
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
14689
|
+
const calibrationFrames = videoFrameWindow ? await buildSegmentVideoFrames().forFrames(
|
|
14690
|
+
selectCaptureCalibrationFrames(totalFrames),
|
|
14691
|
+
"calibration"
|
|
14692
|
+
) : void 0;
|
|
14693
|
+
let outcome;
|
|
14694
|
+
try {
|
|
14695
|
+
outcome = await observeRenderStage(
|
|
14696
|
+
observability,
|
|
14697
|
+
"capture_calibration",
|
|
14698
|
+
captureStageObservationData({
|
|
14699
|
+
forceScreenshot: captureForceScreenshot,
|
|
14700
|
+
// Override the default `capturing` — calibration writes probe
|
|
14701
|
+
// frames only, not `job.framesRendered`, so heartbeats reporting
|
|
14702
|
+
// `framesCompleted: 0` misread as broken. Field signal
|
|
14703
|
+
// ts=1784019503.
|
|
14704
|
+
stagePhase: "calibrating"
|
|
14705
|
+
}),
|
|
14706
|
+
() => runCaptureCalibration({
|
|
14707
|
+
cfg: calibrationCaptureConfig(cfg),
|
|
14708
|
+
fileServer: activeFileServer,
|
|
14709
|
+
workDir,
|
|
14710
|
+
log,
|
|
14711
|
+
job,
|
|
14712
|
+
totalFrames,
|
|
14713
|
+
forceScreenshot: captureForceScreenshot,
|
|
14714
|
+
probeSession,
|
|
14715
|
+
buildCaptureOptions: buildPrepCaptureOptions,
|
|
14716
|
+
createRenderVideoFrameInjector: calibrationFrames ? calibrationFrames.injector : createRenderVideoFrameInjector,
|
|
14717
|
+
assertNotAborted
|
|
14718
|
+
}),
|
|
14719
|
+
{ heartbeatMessage: "browser calibrating (frames not started)" }
|
|
14720
|
+
);
|
|
14721
|
+
} finally {
|
|
14722
|
+
calibrationFrames?.release();
|
|
14723
|
+
}
|
|
14312
14724
|
captureCalibration = outcome.calibration;
|
|
14313
14725
|
calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
|
|
14314
14726
|
captureForceScreenshot = outcome.forceScreenshot;
|
|
@@ -14540,7 +14952,7 @@ async function executeRenderPipeline(input) {
|
|
|
14540
14952
|
hls: ".mp4"
|
|
14541
14953
|
};
|
|
14542
14954
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
14543
|
-
const videoOnlyPath =
|
|
14955
|
+
const videoOnlyPath = join31(workDir, `video-only${videoExt}`);
|
|
14544
14956
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
14545
14957
|
if (usePageSideCompositingForTransitions) {
|
|
14546
14958
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -14641,13 +15053,7 @@ async function executeRenderPipeline(input) {
|
|
|
14641
15053
|
// excluded routes are the ones whose concat-copy or capture loop the
|
|
14642
15054
|
// segment contract does not cover (webm VP9 concat is fragile, HDR and
|
|
14643
15055
|
// shader transitions run their own compositor).
|
|
14644
|
-
useSegmentedCapture:
|
|
14645
|
-
env: process.env,
|
|
14646
|
-
durationSeconds: job.duration,
|
|
14647
|
-
outputFormat,
|
|
14648
|
-
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
14649
|
-
streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration)
|
|
14650
|
-
}),
|
|
15056
|
+
useSegmentedCapture: segmentsCapture(hasHdrContent || compiled.hasShaderTransitions),
|
|
14651
15057
|
useLayeredComposite,
|
|
14652
15058
|
usePageSideCompositing: usePageSideCompositingForTransitions,
|
|
14653
15059
|
hasHdrContent,
|
|
@@ -14687,10 +15093,14 @@ async function executeRenderPipeline(input) {
|
|
|
14687
15093
|
deWorkerInversion,
|
|
14688
15094
|
deParallelRouter
|
|
14689
15095
|
});
|
|
15096
|
+
if (videoFrameWindow && capturePlan.kind !== "sdr_segmented")
|
|
15097
|
+
await extractDeferredVideoFrames();
|
|
15098
|
+
updateCaptureObservability({ videoFrameWindow: videoFrameWindow ? "on" : "off" });
|
|
14690
15099
|
observability.checkpoint("capture_strategy", "resolved", {
|
|
14691
15100
|
plan: capturePlan.kind,
|
|
14692
15101
|
workerCount: capturePlan.workerCount,
|
|
14693
15102
|
forceScreenshot: capturePlan.forceScreenshot,
|
|
15103
|
+
videoFrameWindow: videoFrameWindow ? "on" : "off",
|
|
14694
15104
|
captureBeyondViewport: resolvedCaptureBeyondViewport ?? null,
|
|
14695
15105
|
useStreamingEncode: capturePlan.kind === "sdr_streaming",
|
|
14696
15106
|
useLayeredComposite: capturePlan.kind === "hdr_layered",
|
|
@@ -14828,7 +15238,7 @@ async function executeRenderPipeline(input) {
|
|
|
14828
15238
|
pageMotionBlur: job.config.pageMotionBlur,
|
|
14829
15239
|
captureQuality: captureOptions.quality
|
|
14830
15240
|
});
|
|
14831
|
-
const segmentDir = segmentDirFor(
|
|
15241
|
+
const segmentDir = segmentDirFor(join31(projectDir, "renders"), segmentPlanHash);
|
|
14832
15242
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
14833
15243
|
if (job.config.resumeSegments === true) {
|
|
14834
15244
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -14843,10 +15253,10 @@ async function executeRenderPipeline(input) {
|
|
|
14843
15253
|
});
|
|
14844
15254
|
}
|
|
14845
15255
|
if (completedSegments.size === 0) {
|
|
14846
|
-
|
|
15256
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14847
15257
|
}
|
|
14848
15258
|
} else {
|
|
14849
|
-
|
|
15259
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14850
15260
|
}
|
|
14851
15261
|
const emptySegmentManifest = () => ({
|
|
14852
15262
|
version: 1,
|
|
@@ -14896,6 +15306,7 @@ async function executeRenderPipeline(input) {
|
|
|
14896
15306
|
},
|
|
14897
15307
|
buildCaptureOptions: buildWorkerCaptureOptions,
|
|
14898
15308
|
createRenderVideoFrameInjector,
|
|
15309
|
+
segmentVideoFrames: videoFrameWindow ? buildSegmentVideoFrames() : void 0,
|
|
14899
15310
|
abortSignal: executionSignal,
|
|
14900
15311
|
assertNotAborted,
|
|
14901
15312
|
onProgress,
|
|
@@ -14947,7 +15358,7 @@ async function executeRenderPipeline(input) {
|
|
|
14947
15358
|
probeSession = null;
|
|
14948
15359
|
await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
|
|
14949
15360
|
}
|
|
14950
|
-
|
|
15361
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14951
15362
|
completedSegments = /* @__PURE__ */ new Set();
|
|
14952
15363
|
segmentManifest = emptySegmentManifest();
|
|
14953
15364
|
dedupPerfs.length = 0;
|
|
@@ -14986,11 +15397,12 @@ async function executeRenderPipeline(input) {
|
|
|
14986
15397
|
);
|
|
14987
15398
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
14988
15399
|
if (job.config.keepSegments !== true) {
|
|
14989
|
-
|
|
15400
|
+
rmSync12(segmentDir, { recursive: true, force: true });
|
|
14990
15401
|
}
|
|
14991
15402
|
} else {
|
|
14992
15403
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
14993
15404
|
syncCapturePlan();
|
|
15405
|
+
if (videoFrameWindow) await extractDeferredVideoFrames();
|
|
14994
15406
|
probeSession = null;
|
|
14995
15407
|
observability.checkpoint(
|
|
14996
15408
|
"capture_segmented",
|
|
@@ -15261,8 +15673,8 @@ async function executeRenderPipeline(input) {
|
|
|
15261
15673
|
"capture_disk",
|
|
15262
15674
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
15263
15675
|
);
|
|
15264
|
-
|
|
15265
|
-
|
|
15676
|
+
rmSync12(framesDir, { recursive: true, force: true });
|
|
15677
|
+
mkdirSync12(framesDir, { recursive: true });
|
|
15266
15678
|
resetCaptureAttemptProgress(job);
|
|
15267
15679
|
dedupPerfs.length = 0;
|
|
15268
15680
|
cfg.useDrawElement = false;
|
|
@@ -15367,7 +15779,7 @@ async function executeRenderPipeline(input) {
|
|
|
15367
15779
|
})
|
|
15368
15780
|
);
|
|
15369
15781
|
const totalElapsed = Date.now() - pipelineStart;
|
|
15370
|
-
const tmpPeakBytes =
|
|
15782
|
+
const tmpPeakBytes = existsSync20(workDir) ? sampleDirectoryBytes(workDir) : 0;
|
|
15371
15783
|
recordTransientRetryObservability();
|
|
15372
15784
|
observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
|
|
15373
15785
|
const observabilitySummary = observability.summary({
|
|
@@ -15439,7 +15851,7 @@ async function executeRenderPipeline(input) {
|
|
|
15439
15851
|
job.perfSummary = perfSummary;
|
|
15440
15852
|
if (job.config.debug) {
|
|
15441
15853
|
try {
|
|
15442
|
-
|
|
15854
|
+
writeFileSync7(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
|
|
15443
15855
|
} catch (err) {
|
|
15444
15856
|
log.debug("Failed to write perf summary", {
|
|
15445
15857
|
perfOutputPath,
|
|
@@ -15448,8 +15860,8 @@ async function executeRenderPipeline(input) {
|
|
|
15448
15860
|
}
|
|
15449
15861
|
}
|
|
15450
15862
|
if (job.config.debug) {
|
|
15451
|
-
if (!isPngSequence && !isHls &&
|
|
15452
|
-
const debugOutput =
|
|
15863
|
+
if (!isPngSequence && !isHls && existsSync20(stagedOutputPath)) {
|
|
15864
|
+
const debugOutput = join31(workDir, `output${videoExt}`);
|
|
15453
15865
|
copyFileSync5(stagedOutputPath, debugOutput);
|
|
15454
15866
|
}
|
|
15455
15867
|
}
|
|
@@ -15560,15 +15972,15 @@ async function executeRenderPipeline(input) {
|
|
|
15560
15972
|
|
|
15561
15973
|
// src/services/hyperframeLint.ts
|
|
15562
15974
|
import {
|
|
15563
|
-
existsSync as
|
|
15975
|
+
existsSync as existsSync21,
|
|
15564
15976
|
readFileSync as readFileSync9,
|
|
15565
|
-
statSync as
|
|
15977
|
+
statSync as statSync10,
|
|
15566
15978
|
openSync as openSync5,
|
|
15567
15979
|
fstatSync as fstatSync3,
|
|
15568
15980
|
closeSync as closeSync5,
|
|
15569
15981
|
constants as constants3
|
|
15570
15982
|
} from "node:fs";
|
|
15571
|
-
import { resolve as resolve9, join as
|
|
15983
|
+
import { resolve as resolve9, join as join32, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
|
|
15572
15984
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
15573
15985
|
function isStringRecord(value) {
|
|
15574
15986
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -15602,7 +16014,7 @@ function readEntryHtml(filePath) {
|
|
|
15602
16014
|
} catch (error) {
|
|
15603
16015
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
15604
16016
|
return null;
|
|
15605
|
-
if (!
|
|
16017
|
+
if (!statSync10(filePath, { throwIfNoEntry: false })?.isFile()) return null;
|
|
15606
16018
|
throw error;
|
|
15607
16019
|
}
|
|
15608
16020
|
try {
|
|
@@ -15614,7 +16026,7 @@ function readEntryHtml(filePath) {
|
|
|
15614
16026
|
}
|
|
15615
16027
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
15616
16028
|
const absProjectDir = resolve9(projectDir);
|
|
15617
|
-
if (!
|
|
16029
|
+
if (!existsSync21(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
|
|
15618
16030
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
15619
16031
|
}
|
|
15620
16032
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
@@ -15636,7 +16048,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
15636
16048
|
}
|
|
15637
16049
|
}
|
|
15638
16050
|
return {
|
|
15639
|
-
error: `No HTML entry file found in project directory: ${
|
|
16051
|
+
error: `No HTML entry file found in project directory: ${join32(absProjectDir, preferredEntryFile || "index.html")}`
|
|
15640
16052
|
};
|
|
15641
16053
|
}
|
|
15642
16054
|
function prepareHyperframeLintBody(body) {
|
|
@@ -15682,8 +16094,8 @@ async function runHyperframeLint(prepared) {
|
|
|
15682
16094
|
// src/services/healthWorker.ts
|
|
15683
16095
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
15684
16096
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
15685
|
-
import { dirname as
|
|
15686
|
-
import { existsSync as
|
|
16097
|
+
import { dirname as dirname14, join as join33 } from "node:path";
|
|
16098
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
15687
16099
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
15688
16100
|
async function startHealthWorker(options = {}) {
|
|
15689
16101
|
const log = options.logger ?? defaultLogger2();
|
|
@@ -15757,10 +16169,10 @@ function defaultLogger2() {
|
|
|
15757
16169
|
};
|
|
15758
16170
|
}
|
|
15759
16171
|
function resolveWorkerEntry2() {
|
|
15760
|
-
const here =
|
|
15761
|
-
const candidates = [
|
|
16172
|
+
const here = dirname14(fileURLToPath5(import.meta.url));
|
|
16173
|
+
const candidates = [join33(here, "healthWorkerThread.js"), join33(here, "healthWorkerThread.ts")];
|
|
15762
16174
|
for (const candidate of candidates) {
|
|
15763
|
-
if (
|
|
16175
|
+
if (existsSync22(candidate)) return candidate;
|
|
15764
16176
|
}
|
|
15765
16177
|
return void 0;
|
|
15766
16178
|
}
|
|
@@ -15800,7 +16212,7 @@ import {
|
|
|
15800
16212
|
createReferenceBackgroundRecorder as createReferenceBackgroundRecorder2
|
|
15801
16213
|
} from "@hyperframes/engine";
|
|
15802
16214
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
15803
|
-
import { extname as extname4, isAbsolute as isAbsolute4, join as
|
|
16215
|
+
import { extname as extname4, isAbsolute as isAbsolute4, join as join34, relative as relative4, sep as sep3 } from "node:path";
|
|
15804
16216
|
import {
|
|
15805
16217
|
fpsToNumber as fpsToNumber9,
|
|
15806
16218
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -15819,16 +16231,14 @@ import {
|
|
|
15819
16231
|
import {
|
|
15820
16232
|
closeCaptureSession as closeCaptureSession9,
|
|
15821
16233
|
createCaptureSession as createCaptureSession9,
|
|
15822
|
-
createFrameLookupTable as
|
|
16234
|
+
createFrameLookupTable as createFrameLookupTable4,
|
|
15823
16235
|
initializeSession as initializeSession9
|
|
15824
16236
|
} from "@hyperframes/engine";
|
|
15825
16237
|
|
|
15826
16238
|
// src/services/distributed/chunkFrames.ts
|
|
15827
16239
|
import {
|
|
15828
16240
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
15829
|
-
|
|
15830
|
-
createVideoFrameInjector as createVideoFrameInjector2,
|
|
15831
|
-
extractVideoFramesRange
|
|
16241
|
+
createVideoFrameInjector as createVideoFrameInjector3
|
|
15832
16242
|
} from "@hyperframes/engine";
|
|
15833
16243
|
|
|
15834
16244
|
// src/services/distributed/shared.ts
|
|
@@ -15919,9 +16329,9 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
15919
16329
|
var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
15920
16330
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
15921
16331
|
"plan.json",
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
|
|
16332
|
+
join34("meta", "composition.json"),
|
|
16333
|
+
join34("meta", "encoder.json"),
|
|
16334
|
+
join34("meta", "chunks.json")
|
|
15925
16335
|
];
|
|
15926
16336
|
|
|
15927
16337
|
// src/services/distributed/renderConfigValidation.ts
|
|
@@ -16309,8 +16719,8 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
16309
16719
|
function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
16310
16720
|
const { input, cleanupProjectDir } = prepared;
|
|
16311
16721
|
const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
|
|
16312
|
-
const outputDir =
|
|
16313
|
-
if (!
|
|
16722
|
+
const outputDir = dirname15(absoluteOutputPath);
|
|
16723
|
+
if (!existsSync23(outputDir)) mkdirSync13(outputDir, { recursive: true });
|
|
16314
16724
|
return { input, cleanupProjectDir, absoluteOutputPath };
|
|
16315
16725
|
}
|
|
16316
16726
|
function validateRenderOverrides(body) {
|
|
@@ -16351,11 +16761,11 @@ function prepareProjectDirectory(projectDir, options) {
|
|
|
16351
16761
|
const candidate = nonEmptyString(projectDir);
|
|
16352
16762
|
if (!candidate) return null;
|
|
16353
16763
|
const absProjectDir = resolve10(candidate);
|
|
16354
|
-
if (!
|
|
16764
|
+
if (!existsSync23(absProjectDir) || !statSync11(absProjectDir).isDirectory()) {
|
|
16355
16765
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
16356
16766
|
}
|
|
16357
16767
|
const entry = options.entryFile || "index.html";
|
|
16358
|
-
if (!
|
|
16768
|
+
if (!existsSync23(resolve10(absProjectDir, entry))) {
|
|
16359
16769
|
return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
|
|
16360
16770
|
}
|
|
16361
16771
|
return { prepared: { input: { projectDir: absProjectDir, ...options } } };
|
|
@@ -16376,8 +16786,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
16376
16786
|
}
|
|
16377
16787
|
function materializeInlineProject(html, options) {
|
|
16378
16788
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
16379
|
-
const tempProjectDir = mkdtempSync4(
|
|
16380
|
-
|
|
16789
|
+
const tempProjectDir = mkdtempSync4(join35(tempRoot, "producer-project-"));
|
|
16790
|
+
writeFileSync8(join35(tempProjectDir, "index.html"), html, "utf-8");
|
|
16381
16791
|
return {
|
|
16382
16792
|
prepared: {
|
|
16383
16793
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -16438,7 +16848,7 @@ function createArtifactStore(ttlMs) {
|
|
|
16438
16848
|
function cleanupTempDir(dir, log) {
|
|
16439
16849
|
if (!dir) return;
|
|
16440
16850
|
try {
|
|
16441
|
-
|
|
16851
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
16442
16852
|
} catch (error) {
|
|
16443
16853
|
log.warn("Failed to cleanup temp project dir", {
|
|
16444
16854
|
cleanupProjectDir: dir,
|
|
@@ -16447,7 +16857,7 @@ function cleanupTempDir(dir, log) {
|
|
|
16447
16857
|
}
|
|
16448
16858
|
}
|
|
16449
16859
|
function outputFileSize(path) {
|
|
16450
|
-
return
|
|
16860
|
+
return existsSync23(path) ? statSync11(path).size : 0;
|
|
16451
16861
|
}
|
|
16452
16862
|
function createBlockingProgressReporter(log, requestId) {
|
|
16453
16863
|
let lastLoggedPct = -10;
|
|
@@ -16751,11 +17161,11 @@ function createRenderHandlers(options = {}) {
|
|
|
16751
17161
|
if (!artifact) {
|
|
16752
17162
|
return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
|
|
16753
17163
|
}
|
|
16754
|
-
if (!
|
|
17164
|
+
if (!existsSync23(artifact.path)) {
|
|
16755
17165
|
store.delete(token);
|
|
16756
17166
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
16757
17167
|
}
|
|
16758
|
-
const stats =
|
|
17168
|
+
const stats = statSync11(artifact.path);
|
|
16759
17169
|
return new Response(createReadStream3(artifact.path), {
|
|
16760
17170
|
headers: {
|
|
16761
17171
|
"content-type": "video/mp4",
|