@hyperframes/producer 0.8.181 → 0.8.183
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.d.ts +2 -2
- package/dist/distributed.js +411 -220
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +17 -17
- package/dist/index.js +433 -238
- package/dist/public-server.js +155 -105
- package/dist/services/distributed/assemble.d.ts +2 -1
- package/dist/services/distributed/chunkFrames.d.ts +6 -1
- package/dist/services/distributed/mp4Stitch.d.ts +4 -4
- package/dist/services/distributed/plan.d.ts +11 -0
- package/dist/services/distributed/planV2.d.ts +1 -0
- package/dist/services/distributed/projectHash.d.ts +1 -4
- package/dist/services/render/artifactTransaction.d.ts +7 -0
- package/dist/services/render/audioPadTrim.d.ts +20 -15
- package/dist/services/render/audioTruePeak.d.ts +1 -5
- package/dist/services/render/perfSummary.d.ts +2 -0
- package/dist/services/render/stages/audioStage.d.ts +2 -0
- package/dist/services/render/stages/captureStage.d.ts +9 -3
- package/dist/services/render/stages/freezePlan.d.ts +1 -0
- package/dist/services/renderOrchestrator.d.ts +2 -2
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
extractMediaMetadata as extractMediaMetadata4,
|
|
49
49
|
resolveConfig as resolveConfig2,
|
|
50
50
|
closeCaptureSession as closeCaptureSession8,
|
|
51
|
+
createCaptureSession as createCaptureSession8,
|
|
52
|
+
initializeSession as initializeSession8,
|
|
51
53
|
createVideoFrameInjector,
|
|
52
54
|
getEncoderPreset as getEncoderPreset3,
|
|
53
55
|
distributeFrames as distributeFrames2,
|
|
@@ -1421,7 +1423,7 @@ function pidScope() {
|
|
|
1421
1423
|
return {
|
|
1422
1424
|
host: hostname(),
|
|
1423
1425
|
// Without /proc (macOS, Windows) the boot time, to the minute, stands in for the boot id.
|
|
1424
|
-
boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? String(Math.round(Date.now() / 6e4 - uptime() / 60)),
|
|
1426
|
+
boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? attempt(() => String(Math.round(Date.now() / 6e4 - uptime() / 60))),
|
|
1425
1427
|
pidns: attempt(() => readlinkSync("/proc/self/ns/pid"))
|
|
1426
1428
|
};
|
|
1427
1429
|
}
|
|
@@ -1439,9 +1441,14 @@ function alive(pid) {
|
|
|
1439
1441
|
return error.code === "EPERM";
|
|
1440
1442
|
}
|
|
1441
1443
|
}
|
|
1442
|
-
function
|
|
1444
|
+
function knownPidScope() {
|
|
1443
1445
|
const scope = pidScope();
|
|
1444
|
-
|
|
1446
|
+
const unknown = scope.boot === void 0 || process.platform === "linux" && scope.pidns === void 0;
|
|
1447
|
+
return unknown ? void 0 : scope;
|
|
1448
|
+
}
|
|
1449
|
+
function inThisPidScope(owner) {
|
|
1450
|
+
const scope = knownPidScope();
|
|
1451
|
+
return scope !== void 0 && owner.host === scope.host && owner.boot === scope.boot && owner.pidns === scope.pidns;
|
|
1445
1452
|
}
|
|
1446
1453
|
function ownerState(dir) {
|
|
1447
1454
|
let owner;
|
|
@@ -1506,7 +1513,9 @@ async function defaultArtifactDurationProbe(path) {
|
|
|
1506
1513
|
const meta = await extractMediaMetadata(path);
|
|
1507
1514
|
return {
|
|
1508
1515
|
durationSeconds: meta.durationSeconds,
|
|
1509
|
-
frames: meta.frames
|
|
1516
|
+
frames: meta.frames,
|
|
1517
|
+
width: meta.width,
|
|
1518
|
+
height: meta.height
|
|
1510
1519
|
};
|
|
1511
1520
|
}
|
|
1512
1521
|
function buildArtifactExpectation(input) {
|
|
@@ -1515,7 +1524,9 @@ function buildArtifactExpectation(input) {
|
|
|
1515
1524
|
const expectation = {
|
|
1516
1525
|
expectedDurationSeconds: input.durationSeconds,
|
|
1517
1526
|
fps: input.fps,
|
|
1518
|
-
expectedFrames: input.expectedFrames
|
|
1527
|
+
expectedFrames: input.expectedFrames,
|
|
1528
|
+
expectedWidth: input.width,
|
|
1529
|
+
expectedHeight: input.height
|
|
1519
1530
|
};
|
|
1520
1531
|
if (input.outputFormat !== "hls") return expectation;
|
|
1521
1532
|
return {
|
|
@@ -1583,6 +1594,16 @@ function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPa
|
|
|
1583
1594
|
`Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
|
|
1584
1595
|
);
|
|
1585
1596
|
}
|
|
1597
|
+
function assertSizeMatches(expected, probed, stagingPath) {
|
|
1598
|
+
const { expectedWidth: width, expectedHeight: height } = expected;
|
|
1599
|
+
if (width === void 0 || height === void 0) return;
|
|
1600
|
+
if (probed.width === void 0 || probed.height === void 0) return;
|
|
1601
|
+
const fits = (want, got) => got === want || got === want + want % 2;
|
|
1602
|
+
if (fits(width, probed.width) && fits(height, probed.height)) return;
|
|
1603
|
+
throw new Error(
|
|
1604
|
+
`Render artifact has the wrong size: expected ${width}x${height}, probed ${probed.width}x${probed.height}. Artifact: ${stagingPath}`
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1586
1607
|
var ArtifactTransaction = class {
|
|
1587
1608
|
constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
|
|
1588
1609
|
this.kind = kind;
|
|
@@ -1653,6 +1674,7 @@ var ArtifactTransaction = class {
|
|
|
1653
1674
|
if (expected.expectedFrames !== void 0) {
|
|
1654
1675
|
assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
|
|
1655
1676
|
}
|
|
1677
|
+
assertSizeMatches(expected, probed, probePath);
|
|
1656
1678
|
}
|
|
1657
1679
|
async runDurationProbe(probePath) {
|
|
1658
1680
|
try {
|
|
@@ -3201,7 +3223,9 @@ function aggregateDrawElement(perfs, de) {
|
|
|
3201
3223
|
if (perfs.length === 0) return void 0;
|
|
3202
3224
|
const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
|
|
3203
3225
|
const gateReasons = [
|
|
3204
|
-
...new Set(
|
|
3226
|
+
...new Set(
|
|
3227
|
+
[de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
|
|
3228
|
+
)
|
|
3205
3229
|
].sort();
|
|
3206
3230
|
const gpuRenderers = [
|
|
3207
3231
|
...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
|
|
@@ -8665,6 +8689,7 @@ async function runAudioStage(input) {
|
|
|
8665
8689
|
audios,
|
|
8666
8690
|
masterFxChain,
|
|
8667
8691
|
urlByAbsentLocalPath,
|
|
8692
|
+
keepEncoderInputAt,
|
|
8668
8693
|
abortSignal,
|
|
8669
8694
|
assertNotAborted,
|
|
8670
8695
|
log
|
|
@@ -8684,7 +8709,13 @@ async function runAudioStage(input) {
|
|
|
8684
8709
|
audioOutputPath,
|
|
8685
8710
|
duration,
|
|
8686
8711
|
abortSignal,
|
|
8687
|
-
{
|
|
8712
|
+
{
|
|
8713
|
+
ffmpegProcessTimeout,
|
|
8714
|
+
audioGain,
|
|
8715
|
+
masterFxChain,
|
|
8716
|
+
urlByAbsentLocalPath,
|
|
8717
|
+
keepEncoderInputAt
|
|
8718
|
+
},
|
|
8688
8719
|
compiledDir
|
|
8689
8720
|
);
|
|
8690
8721
|
} catch (err) {
|
|
@@ -8798,13 +8829,29 @@ function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, free
|
|
|
8798
8829
|
}
|
|
8799
8830
|
return { available: false, estimatedBytes, freeBytes };
|
|
8800
8831
|
}
|
|
8801
|
-
function
|
|
8832
|
+
function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
|
|
8833
|
+
const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
|
|
8834
|
+
if (workerCount > 1 && oneWorker.enabled) {
|
|
8835
|
+
return `This render stores frames because it captures with ${workerCount} workers. Free up disk space, or re-run this render with --workers 1 to stream frames into the encoder instead. One worker is slower, so keep the default worker count for renders that fit on disk.`;
|
|
8836
|
+
}
|
|
8837
|
+
switch (oneWorker.reason) {
|
|
8838
|
+
case "format_excluded":
|
|
8839
|
+
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
8840
|
+
case "disabled_by_config":
|
|
8841
|
+
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
8842
|
+
case "duration_cap":
|
|
8843
|
+
return "The streaming duration cap (PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED=true) sends renders longer than PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS to disk at any worker count. Free up disk space, unset the cap, or use --low-memory-mode, which streams past it on one worker.";
|
|
8844
|
+
default:
|
|
8845
|
+
return "Free up disk space.";
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
|
|
8802
8849
|
const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
|
|
8803
8850
|
return new Error(
|
|
8804
|
-
`Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha).
|
|
8851
|
+
`Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha). ${remedy}`
|
|
8805
8852
|
);
|
|
8806
8853
|
}
|
|
8807
|
-
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes) {
|
|
8854
|
+
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy, freeDiskBytes) {
|
|
8808
8855
|
const headroom = inspectDiskCaptureHeadroom(
|
|
8809
8856
|
framesDir,
|
|
8810
8857
|
totalFrames,
|
|
@@ -8816,7 +8863,8 @@ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeD
|
|
|
8816
8863
|
headroom.estimatedBytes,
|
|
8817
8864
|
headroom.freeBytes,
|
|
8818
8865
|
framesDir,
|
|
8819
|
-
"estimate"
|
|
8866
|
+
"estimate",
|
|
8867
|
+
remedy
|
|
8820
8868
|
);
|
|
8821
8869
|
}
|
|
8822
8870
|
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
@@ -8836,7 +8884,7 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
8836
8884
|
return total;
|
|
8837
8885
|
}
|
|
8838
8886
|
function createDiskCaptureProjection(input) {
|
|
8839
|
-
const { framesDir, totalFrames, measureBytes, freeDiskBytes = freeDiskBytesAt } = input;
|
|
8887
|
+
const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
|
|
8840
8888
|
let checked = false;
|
|
8841
8889
|
return (capturedFrames) => {
|
|
8842
8890
|
if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
|
|
@@ -8848,7 +8896,7 @@ function createDiskCaptureProjection(input) {
|
|
|
8848
8896
|
if (remaining <= 0 || freeBytes === null) return;
|
|
8849
8897
|
const needBytes = Math.ceil(measured / capturedFrames * remaining);
|
|
8850
8898
|
if (needBytes > freeBytes * 0.9) {
|
|
8851
|
-
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured");
|
|
8899
|
+
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
|
|
8852
8900
|
}
|
|
8853
8901
|
};
|
|
8854
8902
|
}
|
|
@@ -8890,11 +8938,13 @@ async function runCaptureStage(input) {
|
|
|
8890
8938
|
}
|
|
8891
8939
|
}
|
|
8892
8940
|
const captureOptions = buildCaptureOptions();
|
|
8893
|
-
|
|
8941
|
+
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
8942
|
+
assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy);
|
|
8894
8943
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
8895
8944
|
framesDir,
|
|
8896
8945
|
totalFrames,
|
|
8897
|
-
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir)
|
|
8946
|
+
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
|
|
8947
|
+
remedy
|
|
8898
8948
|
});
|
|
8899
8949
|
if (workerCount > 1) {
|
|
8900
8950
|
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
@@ -10105,8 +10155,10 @@ function fpsToFfmpegArg2(fps) {
|
|
|
10105
10155
|
}
|
|
10106
10156
|
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
10107
10157
|
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
10158
|
+
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
10108
10159
|
function framesInput(input, fpsArg) {
|
|
10109
10160
|
return [
|
|
10161
|
+
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
10110
10162
|
"-framerate",
|
|
10111
10163
|
fpsArg,
|
|
10112
10164
|
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
@@ -10357,7 +10409,8 @@ import { dirname as dirname9, join as join25 } from "node:path";
|
|
|
10357
10409
|
import {
|
|
10358
10410
|
DEFAULT_CONFIG,
|
|
10359
10411
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
10360
|
-
|
|
10412
|
+
encodeAac,
|
|
10413
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
10361
10414
|
getFfprobeBinary,
|
|
10362
10415
|
ManagedChildProcess,
|
|
10363
10416
|
runFfmpeg as runFfmpeg5,
|
|
@@ -10366,7 +10419,12 @@ import {
|
|
|
10366
10419
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
10367
10420
|
|
|
10368
10421
|
// src/services/render/audioTruePeak.ts
|
|
10369
|
-
import {
|
|
10422
|
+
import {
|
|
10423
|
+
aacSegmentCount,
|
|
10424
|
+
describeFfmpegFailure,
|
|
10425
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
10426
|
+
runFfmpeg as runFfmpeg4
|
|
10427
|
+
} from "@hyperframes/engine";
|
|
10370
10428
|
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
10371
10429
|
function checkedDbfs(value) {
|
|
10372
10430
|
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
@@ -10404,12 +10462,13 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
10404
10462
|
};
|
|
10405
10463
|
}
|
|
10406
10464
|
async function measureTruePeakDbfs(audioPath, options) {
|
|
10407
|
-
const { timeoutMs, signal
|
|
10465
|
+
const { timeoutMs, signal } = options;
|
|
10408
10466
|
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
10467
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
10409
10468
|
const block = 4800;
|
|
10410
|
-
const count =
|
|
10411
|
-
if (
|
|
10412
|
-
const cut = (k) => k * Math.floor(
|
|
10469
|
+
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
10470
|
+
if (count === 1) return wholeFile();
|
|
10471
|
+
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
10413
10472
|
const peers = new AbortController();
|
|
10414
10473
|
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
10415
10474
|
const scans = await Promise.all(
|
|
@@ -10438,24 +10497,8 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
|
10438
10497
|
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
10439
10498
|
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
10440
10499
|
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
10441
|
-
function
|
|
10442
|
-
return
|
|
10443
|
-
"-i",
|
|
10444
|
-
inputPath,
|
|
10445
|
-
"-map",
|
|
10446
|
-
"0:a:0",
|
|
10447
|
-
"-vn",
|
|
10448
|
-
"-af",
|
|
10449
|
-
`volume=${attenuationDb.toFixed(3)}dB`,
|
|
10450
|
-
"-t",
|
|
10451
|
-
formatSeconds(targetDurationSeconds),
|
|
10452
|
-
"-c:a",
|
|
10453
|
-
"aac",
|
|
10454
|
-
"-b:a",
|
|
10455
|
-
"192k",
|
|
10456
|
-
"-y",
|
|
10457
|
-
outputPath
|
|
10458
|
-
];
|
|
10500
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
10501
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
10459
10502
|
}
|
|
10460
10503
|
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
10461
10504
|
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
@@ -10470,53 +10513,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
|
|
|
10470
10513
|
if (delta > 0) {
|
|
10471
10514
|
return {
|
|
10472
10515
|
operation: "pad",
|
|
10473
|
-
steps: [
|
|
10474
|
-
{
|
|
10475
|
-
kind: "normalize",
|
|
10476
|
-
args: [
|
|
10477
|
-
"-i",
|
|
10478
|
-
audioPath,
|
|
10479
|
-
"-af",
|
|
10480
|
-
`apad,atrim=0:${targetSec}`,
|
|
10481
|
-
"-t",
|
|
10482
|
-
targetSec,
|
|
10483
|
-
"-c:a",
|
|
10484
|
-
"aac",
|
|
10485
|
-
"-b:a",
|
|
10486
|
-
"192k",
|
|
10487
|
-
"-y",
|
|
10488
|
-
outputPath
|
|
10489
|
-
]
|
|
10490
|
-
}
|
|
10491
|
-
],
|
|
10516
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
10492
10517
|
cleanupPaths: []
|
|
10493
10518
|
};
|
|
10494
10519
|
}
|
|
10495
10520
|
return {
|
|
10496
10521
|
operation: "trim",
|
|
10497
|
-
steps: [
|
|
10498
|
-
{
|
|
10499
|
-
kind: "trim",
|
|
10500
|
-
args: [
|
|
10501
|
-
"-i",
|
|
10502
|
-
audioPath,
|
|
10503
|
-
"-af",
|
|
10504
|
-
`atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
|
|
10505
|
-
// `atrim` limits decoded samples but does not cap muxer timestamps
|
|
10506
|
-
// introduced by encoder delay/flush. The output duration contract
|
|
10507
|
-
// is enforced at the container boundary as well; without `-t`, a
|
|
10508
|
-
// long primed source can retain its original tail after re-encode.
|
|
10509
|
-
"-t",
|
|
10510
|
-
targetSec,
|
|
10511
|
-
"-c:a",
|
|
10512
|
-
"aac",
|
|
10513
|
-
"-b:a",
|
|
10514
|
-
"192k",
|
|
10515
|
-
"-y",
|
|
10516
|
-
outputPath
|
|
10517
|
-
]
|
|
10518
|
-
}
|
|
10519
|
-
],
|
|
10522
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
10520
10523
|
cleanupPaths: []
|
|
10521
10524
|
};
|
|
10522
10525
|
}
|
|
@@ -10533,7 +10536,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10533
10536
|
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
10534
10537
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
10535
10538
|
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
10536
|
-
const
|
|
10539
|
+
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
10540
|
+
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
10537
10541
|
const [videoResult, audioResult] = await Promise.allSettled([
|
|
10538
10542
|
probeVideo(input.videoPath),
|
|
10539
10543
|
probeAudio(input.audioPath, input.signal)
|
|
@@ -10580,7 +10584,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10580
10584
|
);
|
|
10581
10585
|
try {
|
|
10582
10586
|
for (const step of plan.steps) {
|
|
10583
|
-
const ffmpegResult = await runner(step.args);
|
|
10587
|
+
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
10584
10588
|
if (!ffmpegResult.success) {
|
|
10585
10589
|
return {
|
|
10586
10590
|
success: false,
|
|
@@ -10612,7 +10616,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10612
10616
|
targetDurationSeconds,
|
|
10613
10617
|
signal: input.signal,
|
|
10614
10618
|
probeTruePeak,
|
|
10615
|
-
|
|
10619
|
+
encode
|
|
10616
10620
|
});
|
|
10617
10621
|
if (!correction.success) {
|
|
10618
10622
|
return {
|
|
@@ -10660,13 +10664,11 @@ async function enforceAacTruePeak(input) {
|
|
|
10660
10664
|
}
|
|
10661
10665
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
10662
10666
|
attenuationDb += correctionTargetDbfs - truePeakDbfs;
|
|
10663
|
-
const result = await input.
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
attenuationDb
|
|
10669
|
-
)
|
|
10667
|
+
const result = await input.encode(
|
|
10668
|
+
input.audioPath,
|
|
10669
|
+
correctedPath,
|
|
10670
|
+
input.targetDurationSeconds,
|
|
10671
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
10670
10672
|
);
|
|
10671
10673
|
if (!result.success) {
|
|
10672
10674
|
return {
|
|
@@ -10761,7 +10763,7 @@ function parseFrameRate(rate) {
|
|
|
10761
10763
|
return { fpsNum, fpsDen };
|
|
10762
10764
|
}
|
|
10763
10765
|
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
10764
|
-
const metadata = await
|
|
10766
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
10765
10767
|
return {
|
|
10766
10768
|
durationSeconds: metadata.durationSeconds,
|
|
10767
10769
|
sampleRate: metadata.sampleRate,
|
|
@@ -10770,7 +10772,18 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
10770
10772
|
};
|
|
10771
10773
|
}
|
|
10772
10774
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
10773
|
-
|
|
10775
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
10776
|
+
}
|
|
10777
|
+
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
10778
|
+
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
10779
|
+
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
10780
|
+
return toStepResult(
|
|
10781
|
+
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
10782
|
+
timeoutMs
|
|
10783
|
+
);
|
|
10784
|
+
};
|
|
10785
|
+
}
|
|
10786
|
+
function toStepResult(result, timeoutMs) {
|
|
10774
10787
|
if (result.success) return { success: true };
|
|
10775
10788
|
return {
|
|
10776
10789
|
success: false,
|
|
@@ -10958,7 +10971,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
|
|
|
10958
10971
|
const reason = UNSUPPORTED_REASON2[route];
|
|
10959
10972
|
if (reason === null) return;
|
|
10960
10973
|
throw new Error(
|
|
10961
|
-
`[Render] --resolution other than the composition's size is not supported on the "${route}" capture route: ${reason}. Render at the composition's size, or
|
|
10974
|
+
`[Render] --resolution other than the composition's size is not supported on the "${route}" capture route: ${reason}. Render at the composition's size; or, for HDR footage, add --sdr to render this size in standard colour; or, for shader transitions, render as mp4.`
|
|
10962
10975
|
);
|
|
10963
10976
|
}
|
|
10964
10977
|
|
|
@@ -11175,7 +11188,7 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11175
11188
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11176
11189
|
}
|
|
11177
11190
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11178
|
-
if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
|
|
11191
|
+
if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
|
|
11179
11192
|
return join26(dirname10(outputPath), `work-${jobId}-`);
|
|
11180
11193
|
}
|
|
11181
11194
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
@@ -11669,6 +11682,9 @@ function resolveDeShortBand(args) {
|
|
|
11669
11682
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
11670
11683
|
return args.workerCount > 1 && typeof args.requestedWorkers !== "number" && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
|
|
11671
11684
|
}
|
|
11685
|
+
function isProbeDeGated(session) {
|
|
11686
|
+
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
11687
|
+
}
|
|
11672
11688
|
function resolveInversionRetryPlan(args) {
|
|
11673
11689
|
if (args.deWorkerInversion !== "inverted") return null;
|
|
11674
11690
|
const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
|
|
@@ -11750,7 +11766,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
|
|
|
11750
11766
|
}
|
|
11751
11767
|
function resolveParallelCaptureMode(args) {
|
|
11752
11768
|
const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
|
|
11753
|
-
|
|
11769
|
+
const headlessShell = args.headlessShellPath !== void 0;
|
|
11770
|
+
return headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
|
|
11754
11771
|
}
|
|
11755
11772
|
function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
|
|
11756
11773
|
const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
|
|
@@ -12358,6 +12375,7 @@ async function executeRenderPipeline(input) {
|
|
|
12358
12375
|
);
|
|
12359
12376
|
const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
|
|
12360
12377
|
const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
|
|
12378
|
+
if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
|
|
12361
12379
|
const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
|
|
12362
12380
|
const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
|
|
12363
12381
|
const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
|
|
@@ -12398,7 +12416,7 @@ async function executeRenderPipeline(input) {
|
|
|
12398
12416
|
singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
|
|
12399
12417
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12400
12418
|
supersampling: deviceScaleFactor !== 1,
|
|
12401
|
-
probeDeGated: probeSession
|
|
12419
|
+
probeDeGated: isProbeDeGated(probeSession),
|
|
12402
12420
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
|
|
12403
12421
|
process.env.HF_DE_PARALLEL_STREAM === "true"
|
|
12404
12422
|
};
|
|
@@ -12407,14 +12425,14 @@ async function executeRenderPipeline(input) {
|
|
|
12407
12425
|
...deInversionArgs,
|
|
12408
12426
|
minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
|
|
12409
12427
|
});
|
|
12410
|
-
|
|
12428
|
+
let deShortBand = resolveDeShortBand({
|
|
12411
12429
|
invertAtBaseFloor,
|
|
12412
12430
|
invertAtBandFloor,
|
|
12413
12431
|
bandEnabled: deShortBandEnabled,
|
|
12414
12432
|
bandOpen: deShortBandOpen,
|
|
12415
12433
|
elementCountSource: compositionElementCountSource
|
|
12416
12434
|
});
|
|
12417
|
-
|
|
12435
|
+
let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
|
|
12418
12436
|
const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
|
|
12419
12437
|
const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
|
|
12420
12438
|
const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
|
|
@@ -12423,7 +12441,7 @@ async function executeRenderPipeline(input) {
|
|
|
12423
12441
|
const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
|
|
12424
12442
|
const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
|
|
12425
12443
|
const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
|
|
12426
|
-
|
|
12444
|
+
let deParallelRouterEligible = shouldPreferParallelDrawElement({
|
|
12427
12445
|
workerCount: WOULD_RESOLVE_MULTI_WORKER,
|
|
12428
12446
|
requestedWorkers: job.config.workers,
|
|
12429
12447
|
useDrawElement: cfg.useDrawElement,
|
|
@@ -12434,7 +12452,7 @@ async function executeRenderPipeline(input) {
|
|
|
12434
12452
|
minFrames: deParallelMinFrames,
|
|
12435
12453
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12436
12454
|
supersampling: deviceScaleFactor !== 1,
|
|
12437
|
-
probeDeGated: probeSession
|
|
12455
|
+
probeDeGated: isProbeDeGated(probeSession),
|
|
12438
12456
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12439
12457
|
routerEnabled: deParallelRouterEnabled,
|
|
12440
12458
|
// Router pins 3 workers for the streaming path; don't pin when a
|
|
@@ -12450,6 +12468,35 @@ async function executeRenderPipeline(input) {
|
|
|
12450
12468
|
totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
|
|
12451
12469
|
minMemoryMb: deParallelMinMemoryMb
|
|
12452
12470
|
});
|
|
12471
|
+
if (probeSession === null && (deInversionEligible || deParallelRouterEligible)) {
|
|
12472
|
+
try {
|
|
12473
|
+
probeSession = await createCaptureSession8(
|
|
12474
|
+
activeFileServer.url,
|
|
12475
|
+
framesDir,
|
|
12476
|
+
buildCaptureOptions(),
|
|
12477
|
+
null,
|
|
12478
|
+
{ ...cfg, forceScreenshot: captureForceScreenshot }
|
|
12479
|
+
);
|
|
12480
|
+
await initializeSession8(probeSession);
|
|
12481
|
+
} catch (error) {
|
|
12482
|
+
assertNotAborted();
|
|
12483
|
+
if (probeSession) {
|
|
12484
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
12485
|
+
await closeCaptureSession8(probeSession).catch(() => {
|
|
12486
|
+
});
|
|
12487
|
+
probeSession = null;
|
|
12488
|
+
}
|
|
12489
|
+
log.warn("[Render] Fast capture gate check failed; routing without it.", {
|
|
12490
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12491
|
+
});
|
|
12492
|
+
}
|
|
12493
|
+
if (isProbeDeGated(probeSession)) {
|
|
12494
|
+
deInversionEligible = false;
|
|
12495
|
+
deParallelRouterEligible = false;
|
|
12496
|
+
deShortBand = void 0;
|
|
12497
|
+
}
|
|
12498
|
+
}
|
|
12499
|
+
const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
|
|
12453
12500
|
let workerCount;
|
|
12454
12501
|
const captureStageObservationData = (extra = {}) => ({
|
|
12455
12502
|
stagePhase: "capturing",
|
|
@@ -12637,7 +12684,7 @@ async function executeRenderPipeline(input) {
|
|
|
12637
12684
|
}
|
|
12638
12685
|
const parallelCaptureMode = resolveParallelCaptureMode({
|
|
12639
12686
|
platform: process.platform,
|
|
12640
|
-
|
|
12687
|
+
headlessShellPath: resolveHeadlessShellPath(cfg),
|
|
12641
12688
|
forceScreenshot: captureForceScreenshot,
|
|
12642
12689
|
deviceScaleFactor: captureOptions.deviceScaleFactor
|
|
12643
12690
|
});
|
|
@@ -13409,7 +13456,9 @@ async function executeRenderPipeline(input) {
|
|
|
13409
13456
|
outputFormat,
|
|
13410
13457
|
durationSeconds: job.duration,
|
|
13411
13458
|
fps: fpsToNumber9(job.config.fps),
|
|
13412
|
-
expectedFrames: captureTotalFrames
|
|
13459
|
+
expectedFrames: captureTotalFrames,
|
|
13460
|
+
width: outputWidth,
|
|
13461
|
+
height: outputHeight
|
|
13413
13462
|
})
|
|
13414
13463
|
);
|
|
13415
13464
|
const totalElapsed = Date.now() - pipelineStart;
|
|
@@ -13445,6 +13494,7 @@ async function executeRenderPipeline(input) {
|
|
|
13445
13494
|
drawElement: {
|
|
13446
13495
|
compileGate: deCompileGate,
|
|
13447
13496
|
clampReason: deClampReason,
|
|
13497
|
+
probeGateReason: deProbeGateReason,
|
|
13448
13498
|
workerInversion: deWorkerInversion,
|
|
13449
13499
|
preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
|
|
13450
13500
|
compositionElementCount,
|
|
@@ -13846,9 +13896,9 @@ import {
|
|
|
13846
13896
|
// src/services/distributed/planFrameCost.ts
|
|
13847
13897
|
import {
|
|
13848
13898
|
closeCaptureSession as closeCaptureSession9,
|
|
13849
|
-
createCaptureSession as
|
|
13899
|
+
createCaptureSession as createCaptureSession9,
|
|
13850
13900
|
createFrameLookupTable as createFrameLookupTable3,
|
|
13851
|
-
initializeSession as
|
|
13901
|
+
initializeSession as initializeSession9
|
|
13852
13902
|
} from "@hyperframes/engine";
|
|
13853
13903
|
|
|
13854
13904
|
// src/services/distributed/chunkFrames.ts
|
|
@@ -68,6 +68,7 @@ export declare function assemble(planDir: string, chunkOutputs: AssembleChunkPat
|
|
|
68
68
|
*/
|
|
69
69
|
cfr?: boolean;
|
|
70
70
|
}): Promise<AssembleResult>;
|
|
71
|
-
/** Chunks
|
|
71
|
+
/** Chunks carrying audio, or with it as a separate file, stitch under one index like S3's. */
|
|
72
72
|
export declare function chunksAreStitchedMp4(planDir: string, cfr: boolean): boolean;
|
|
73
|
+
export declare function stitchesSeparateAudio(planDir: string): boolean;
|
|
73
74
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BeforeCaptureHook, BROWSER_GPU_NOT_SOFTWARE, type CaptureOptions, type EngineConfig, type ExtractedFrames, type FrameLookupTable } from "@hyperframes/engine";
|
|
1
|
+
import { type BeforeCaptureHook, BROWSER_GPU_NOT_SOFTWARE, type CaptureOptions, type EngineConfig, type ExtractedFrames, type FrameLookupTable, type ShownBox } from "@hyperframes/engine";
|
|
2
2
|
import type { LockedRenderConfig } from "../render/stages/freezePlan.js";
|
|
3
3
|
import type { PlanV2RemoteSources } from "./planV2.js";
|
|
4
4
|
import { type DistributedFormat, INVALID_VIDEO_METADATA, type PlanVideosJson } from "./shared.js";
|
|
@@ -39,6 +39,10 @@ export declare function buildChunkCaptureOptions(input: {
|
|
|
39
39
|
videoMetadataHints?: CaptureOptions["videoMetadataHints"];
|
|
40
40
|
}): CaptureOptions;
|
|
41
41
|
/** Extracts the given frames of deferred videos under the names a full extraction gives them. */
|
|
42
|
+
export declare function shownDeferredFrameIndexes(planVideos: PlanVideosJson, frames: Iterable<number>, fps: {
|
|
43
|
+
num: number;
|
|
44
|
+
den: number;
|
|
45
|
+
}): Map<string, Set<number>>;
|
|
42
46
|
export declare function extractDeferredVideoFramesForChunk(input: {
|
|
43
47
|
planDir: string;
|
|
44
48
|
planVideos: PlanVideosJson;
|
|
@@ -50,4 +54,5 @@ export declare function extractDeferredVideoFramesForChunk(input: {
|
|
|
50
54
|
outputRoot: string;
|
|
51
55
|
cfg: Pick<EngineConfig, "ffmpegProcessTimeout">;
|
|
52
56
|
remoteSources?: PlanV2RemoteSources;
|
|
57
|
+
shownBoxes?: ReadonlyMap<string, ShownBox>;
|
|
53
58
|
}): Promise<ExtractedFrames[]>;
|
|
@@ -31,9 +31,9 @@ export interface TrackTable {
|
|
|
31
31
|
}
|
|
32
32
|
/** Each track's sample tables, expanded to one entry per sample (and per chunk). */
|
|
33
33
|
export declare function readTracks(moov: Buffer): TrackTable[];
|
|
34
|
-
export declare function buildStitchedHeader(layouts: readonly Mp4Layout[]): Buffer;
|
|
34
|
+
export declare function buildStitchedHeader(layouts: readonly Mp4Layout[], audio?: Mp4Layout): Buffer;
|
|
35
35
|
export declare function fileRange(path: string): ReadRange;
|
|
36
|
-
/** Local join: the
|
|
37
|
-
*
|
|
38
|
-
export declare function writeStitchedMp4(chunkPaths: readonly string[], outputPath: string): Promise<void>;
|
|
36
|
+
/** Local join: the header, then `audioPath`'s and each chunk's payload. Like the ffmpeg join, it
|
|
37
|
+
* writes in a work folder beside `outputPath` and renames only a finished file there. */
|
|
38
|
+
export declare function writeStitchedMp4(chunkPaths: readonly string[], outputPath: string, audioPath?: string): Promise<void>;
|
|
39
39
|
export {};
|
|
@@ -211,6 +211,8 @@ export interface BuildLocalExecutionPlanOptions {
|
|
|
211
211
|
readonly chunksCarryAudio?: boolean;
|
|
212
212
|
/** With `chunksCarryAudio`, also write each chunk's audio packets to their own file. Plan v2 only. */
|
|
213
213
|
readonly sliceChunkAudio?: boolean;
|
|
214
|
+
/** Leave the audio to {@link buildPlanAudio}; chunks render video only. mp4 without `cfr` only. */
|
|
215
|
+
readonly deferAudio?: boolean;
|
|
214
216
|
}
|
|
215
217
|
export interface RemoteProjectFile {
|
|
216
218
|
readonly url: string;
|
|
@@ -401,7 +403,16 @@ export declare function resolveDistributedEngineConfig(config: DistributedRender
|
|
|
401
403
|
* Build the shared local execution representation used by both transport
|
|
402
404
|
* protocols. See the module docstring for the directory layout.
|
|
403
405
|
*/
|
|
406
|
+
/** Whether the plan leaves its audio to {@link buildPlanAudio}. */
|
|
407
|
+
export declare function plansSeparateAudio(config: Pick<DistributedRenderConfig, "format" | "cfr">, options: Pick<BuildLocalExecutionPlanOptions, "deferAudio">): boolean;
|
|
404
408
|
export declare function buildLocalExecutionPlan(projectDir: string, config: DistributedRenderConfig, executionPlanDir: string, options?: Readonly<BuildLocalExecutionPlanOptions>): Promise<LocalExecutionPlan>;
|
|
409
|
+
/**
|
|
410
|
+
* The audio of a plan made with `deferAudio`: the mix and true-peak hold that plan would have
|
|
411
|
+
* made, written to `outputPath` (on `workRoot`'s filesystem) at exactly the video's length.
|
|
412
|
+
*/
|
|
413
|
+
export declare function buildPlanAudio(projectDir: string, config: DistributedRenderConfig, workRoot: string, outputPath: string, options?: Pick<BuildLocalExecutionPlanOptions, "remoteFiles">): Promise<{
|
|
414
|
+
hasAudio: boolean;
|
|
415
|
+
}>;
|
|
405
416
|
/**
|
|
406
417
|
* Legacy v1 transport wrapper around {@link buildLocalExecutionPlan}.
|
|
407
418
|
*
|
|
@@ -91,6 +91,7 @@ export interface PlanV2WithPublisherOptions {
|
|
|
91
91
|
/** See {@link BuildLocalExecutionPlanOptions.remoteFiles}; needs a publisher with `referenceStoredBlob`. */
|
|
92
92
|
readonly remoteFiles?: BuildLocalExecutionPlanOptions["remoteFiles"];
|
|
93
93
|
readonly chunksCarryAudio?: boolean;
|
|
94
|
+
readonly deferAudio?: boolean;
|
|
94
95
|
}
|
|
95
96
|
export interface PlanV2MaterializationResult {
|
|
96
97
|
readonly planDir: string;
|
|
@@ -22,9 +22,6 @@ export declare function listSiteFiles(projectDir: string): string[];
|
|
|
22
22
|
* same-bytes rename still yields a fresh id. We trim to 16 chars because the
|
|
23
23
|
* full 64 isn't useful in an object key for legibility. Pass `files` when the
|
|
24
24
|
* caller already listed them, so the hashed set is the shipped set.
|
|
25
|
-
*
|
|
26
|
-
* Reads are synchronous: project trees are typically tens of MB at most
|
|
27
|
-
* (HTML/CSS/JS plus a few composition assets), so the simpler shape wins over
|
|
28
|
-
* a streaming pipeline.
|
|
25
|
+
* Files are read in fixed-size chunks, so no file is ever held whole in memory.
|
|
29
26
|
*/
|
|
30
27
|
export declare function hashProjectDir(projectDir: string, files?: readonly string[]): string;
|