@hyperframes/producer 0.8.180 → 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 +486 -237
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +17 -17
- package/dist/index.js +508 -255
- package/dist/public-server.js +224 -120
- 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 +8 -0
- 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/index.js
CHANGED
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
extractMediaMetadata as extractMediaMetadata4,
|
|
29
29
|
resolveConfig as resolveConfig2,
|
|
30
30
|
closeCaptureSession as closeCaptureSession8,
|
|
31
|
+
createCaptureSession as createCaptureSession8,
|
|
32
|
+
initializeSession as initializeSession8,
|
|
31
33
|
createVideoFrameInjector,
|
|
32
34
|
getEncoderPreset as getEncoderPreset3,
|
|
33
35
|
distributeFrames as distributeFrames2,
|
|
@@ -1402,7 +1404,7 @@ function pidScope() {
|
|
|
1402
1404
|
return {
|
|
1403
1405
|
host: hostname(),
|
|
1404
1406
|
// Without /proc (macOS, Windows) the boot time, to the minute, stands in for the boot id.
|
|
1405
|
-
boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? String(Math.round(Date.now() / 6e4 - uptime() / 60)),
|
|
1407
|
+
boot: attempt(() => readFileSync2("/proc/sys/kernel/random/boot_id", "utf-8").trim()) ?? attempt(() => String(Math.round(Date.now() / 6e4 - uptime() / 60))),
|
|
1406
1408
|
pidns: attempt(() => readlinkSync("/proc/self/ns/pid"))
|
|
1407
1409
|
};
|
|
1408
1410
|
}
|
|
@@ -1420,9 +1422,14 @@ function alive(pid) {
|
|
|
1420
1422
|
return error.code === "EPERM";
|
|
1421
1423
|
}
|
|
1422
1424
|
}
|
|
1423
|
-
function
|
|
1425
|
+
function knownPidScope() {
|
|
1424
1426
|
const scope = pidScope();
|
|
1425
|
-
|
|
1427
|
+
const unknown = scope.boot === void 0 || process.platform === "linux" && scope.pidns === void 0;
|
|
1428
|
+
return unknown ? void 0 : scope;
|
|
1429
|
+
}
|
|
1430
|
+
function inThisPidScope(owner) {
|
|
1431
|
+
const scope = knownPidScope();
|
|
1432
|
+
return scope !== void 0 && owner.host === scope.host && owner.boot === scope.boot && owner.pidns === scope.pidns;
|
|
1426
1433
|
}
|
|
1427
1434
|
function ownerState(dir) {
|
|
1428
1435
|
let owner;
|
|
@@ -1487,7 +1494,9 @@ async function defaultArtifactDurationProbe(path) {
|
|
|
1487
1494
|
const meta = await extractMediaMetadata(path);
|
|
1488
1495
|
return {
|
|
1489
1496
|
durationSeconds: meta.durationSeconds,
|
|
1490
|
-
frames: meta.frames
|
|
1497
|
+
frames: meta.frames,
|
|
1498
|
+
width: meta.width,
|
|
1499
|
+
height: meta.height
|
|
1491
1500
|
};
|
|
1492
1501
|
}
|
|
1493
1502
|
function buildArtifactExpectation(input) {
|
|
@@ -1496,7 +1505,9 @@ function buildArtifactExpectation(input) {
|
|
|
1496
1505
|
const expectation = {
|
|
1497
1506
|
expectedDurationSeconds: input.durationSeconds,
|
|
1498
1507
|
fps: input.fps,
|
|
1499
|
-
expectedFrames: input.expectedFrames
|
|
1508
|
+
expectedFrames: input.expectedFrames,
|
|
1509
|
+
expectedWidth: input.width,
|
|
1510
|
+
expectedHeight: input.height
|
|
1500
1511
|
};
|
|
1501
1512
|
if (input.outputFormat !== "hls") return expectation;
|
|
1502
1513
|
return {
|
|
@@ -1564,6 +1575,16 @@ function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPa
|
|
|
1564
1575
|
`Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
|
|
1565
1576
|
);
|
|
1566
1577
|
}
|
|
1578
|
+
function assertSizeMatches(expected, probed, stagingPath) {
|
|
1579
|
+
const { expectedWidth: width, expectedHeight: height } = expected;
|
|
1580
|
+
if (width === void 0 || height === void 0) return;
|
|
1581
|
+
if (probed.width === void 0 || probed.height === void 0) return;
|
|
1582
|
+
const fits = (want, got) => got === want || got === want + want % 2;
|
|
1583
|
+
if (fits(width, probed.width) && fits(height, probed.height)) return;
|
|
1584
|
+
throw new Error(
|
|
1585
|
+
`Render artifact has the wrong size: expected ${width}x${height}, probed ${probed.width}x${probed.height}. Artifact: ${stagingPath}`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1567
1588
|
var ArtifactTransaction = class {
|
|
1568
1589
|
constructor(destinationPath, kind, fileSystem = defaultFileSystem, durationProbe = defaultArtifactDurationProbe) {
|
|
1569
1590
|
this.kind = kind;
|
|
@@ -1634,6 +1655,7 @@ var ArtifactTransaction = class {
|
|
|
1634
1655
|
if (expected.expectedFrames !== void 0) {
|
|
1635
1656
|
assertFrameCountWithinTolerance(expected.expectedFrames, probed.frames, probePath);
|
|
1636
1657
|
}
|
|
1658
|
+
assertSizeMatches(expected, probed, probePath);
|
|
1637
1659
|
}
|
|
1638
1660
|
async runDurationProbe(probePath) {
|
|
1639
1661
|
try {
|
|
@@ -3182,7 +3204,9 @@ function aggregateDrawElement(perfs, de) {
|
|
|
3182
3204
|
if (perfs.length === 0) return void 0;
|
|
3183
3205
|
const modes = [...new Set(perfs.map((p) => p.captureMode).filter(Boolean))].sort();
|
|
3184
3206
|
const gateReasons = [
|
|
3185
|
-
...new Set(
|
|
3207
|
+
...new Set(
|
|
3208
|
+
[de.probeGateReason, ...perfs.map((p) => p.deGateReason)].filter((r) => !!r)
|
|
3209
|
+
)
|
|
3186
3210
|
].sort();
|
|
3187
3211
|
const gpuRenderers = [
|
|
3188
3212
|
...new Set(perfs.map((p) => p.gpuRenderer).filter((r) => !!r))
|
|
@@ -8682,6 +8706,7 @@ async function runAudioStage(input) {
|
|
|
8682
8706
|
audios,
|
|
8683
8707
|
masterFxChain,
|
|
8684
8708
|
urlByAbsentLocalPath,
|
|
8709
|
+
keepEncoderInputAt,
|
|
8685
8710
|
abortSignal,
|
|
8686
8711
|
assertNotAborted,
|
|
8687
8712
|
log
|
|
@@ -8701,7 +8726,13 @@ async function runAudioStage(input) {
|
|
|
8701
8726
|
audioOutputPath,
|
|
8702
8727
|
duration,
|
|
8703
8728
|
abortSignal,
|
|
8704
|
-
{
|
|
8729
|
+
{
|
|
8730
|
+
ffmpegProcessTimeout,
|
|
8731
|
+
audioGain,
|
|
8732
|
+
masterFxChain,
|
|
8733
|
+
urlByAbsentLocalPath,
|
|
8734
|
+
keepEncoderInputAt
|
|
8735
|
+
},
|
|
8705
8736
|
compiledDir
|
|
8706
8737
|
);
|
|
8707
8738
|
} catch (err) {
|
|
@@ -8815,13 +8846,29 @@ function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, free
|
|
|
8815
8846
|
}
|
|
8816
8847
|
return { available: false, estimatedBytes, freeBytes };
|
|
8817
8848
|
}
|
|
8818
|
-
function
|
|
8849
|
+
function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
|
|
8850
|
+
const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
|
|
8851
|
+
if (workerCount > 1 && oneWorker.enabled) {
|
|
8852
|
+
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.`;
|
|
8853
|
+
}
|
|
8854
|
+
switch (oneWorker.reason) {
|
|
8855
|
+
case "format_excluded":
|
|
8856
|
+
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
8857
|
+
case "disabled_by_config":
|
|
8858
|
+
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
8859
|
+
case "duration_cap":
|
|
8860
|
+
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.";
|
|
8861
|
+
default:
|
|
8862
|
+
return "Free up disk space.";
|
|
8863
|
+
}
|
|
8864
|
+
}
|
|
8865
|
+
function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
|
|
8819
8866
|
const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
|
|
8820
8867
|
return new Error(
|
|
8821
|
-
`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).
|
|
8868
|
+
`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}`
|
|
8822
8869
|
);
|
|
8823
8870
|
}
|
|
8824
|
-
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes) {
|
|
8871
|
+
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy, freeDiskBytes) {
|
|
8825
8872
|
const headroom = inspectDiskCaptureHeadroom(
|
|
8826
8873
|
framesDir,
|
|
8827
8874
|
totalFrames,
|
|
@@ -8833,7 +8880,8 @@ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeD
|
|
|
8833
8880
|
headroom.estimatedBytes,
|
|
8834
8881
|
headroom.freeBytes,
|
|
8835
8882
|
framesDir,
|
|
8836
|
-
"estimate"
|
|
8883
|
+
"estimate",
|
|
8884
|
+
remedy
|
|
8837
8885
|
);
|
|
8838
8886
|
}
|
|
8839
8887
|
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
@@ -8853,7 +8901,7 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
8853
8901
|
return total;
|
|
8854
8902
|
}
|
|
8855
8903
|
function createDiskCaptureProjection(input) {
|
|
8856
|
-
const { framesDir, totalFrames, measureBytes, freeDiskBytes = freeDiskBytesAt } = input;
|
|
8904
|
+
const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
|
|
8857
8905
|
let checked = false;
|
|
8858
8906
|
return (capturedFrames) => {
|
|
8859
8907
|
if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
|
|
@@ -8865,7 +8913,7 @@ function createDiskCaptureProjection(input) {
|
|
|
8865
8913
|
if (remaining <= 0 || freeBytes === null) return;
|
|
8866
8914
|
const needBytes = Math.ceil(measured / capturedFrames * remaining);
|
|
8867
8915
|
if (needBytes > freeBytes * 0.9) {
|
|
8868
|
-
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured");
|
|
8916
|
+
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
|
|
8869
8917
|
}
|
|
8870
8918
|
};
|
|
8871
8919
|
}
|
|
@@ -8907,11 +8955,13 @@ async function runCaptureStage(input) {
|
|
|
8907
8955
|
}
|
|
8908
8956
|
}
|
|
8909
8957
|
const captureOptions = buildCaptureOptions();
|
|
8910
|
-
|
|
8958
|
+
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
8959
|
+
assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy);
|
|
8911
8960
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
8912
8961
|
framesDir,
|
|
8913
8962
|
totalFrames,
|
|
8914
|
-
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir)
|
|
8963
|
+
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
|
|
8964
|
+
remedy
|
|
8915
8965
|
});
|
|
8916
8966
|
if (workerCount > 1) {
|
|
8917
8967
|
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
@@ -10122,8 +10172,10 @@ function fpsToFfmpegArg2(fps) {
|
|
|
10122
10172
|
}
|
|
10123
10173
|
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
10124
10174
|
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
10175
|
+
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
10125
10176
|
function framesInput(input, fpsArg) {
|
|
10126
10177
|
return [
|
|
10178
|
+
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
10127
10179
|
"-framerate",
|
|
10128
10180
|
fpsArg,
|
|
10129
10181
|
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
@@ -10373,36 +10425,97 @@ import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSyn
|
|
|
10373
10425
|
import { dirname as dirname9, join as join25 } from "node:path";
|
|
10374
10426
|
import {
|
|
10375
10427
|
DEFAULT_CONFIG,
|
|
10376
|
-
describeFfmpegFailure,
|
|
10377
|
-
|
|
10428
|
+
describeFfmpegFailure as describeFfmpegFailure2,
|
|
10429
|
+
encodeAac,
|
|
10430
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
10378
10431
|
getFfprobeBinary,
|
|
10379
10432
|
ManagedChildProcess,
|
|
10380
|
-
runFfmpeg as
|
|
10433
|
+
runFfmpeg as runFfmpeg5,
|
|
10381
10434
|
trackChildProcess
|
|
10382
10435
|
} from "@hyperframes/engine";
|
|
10383
10436
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
10437
|
+
|
|
10438
|
+
// src/services/render/audioTruePeak.ts
|
|
10439
|
+
import {
|
|
10440
|
+
aacSegmentCount,
|
|
10441
|
+
describeFfmpegFailure,
|
|
10442
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
10443
|
+
runFfmpeg as runFfmpeg4
|
|
10444
|
+
} from "@hyperframes/engine";
|
|
10445
|
+
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
10446
|
+
function checkedDbfs(value) {
|
|
10447
|
+
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
10448
|
+
throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
|
|
10449
|
+
}
|
|
10450
|
+
return value;
|
|
10451
|
+
}
|
|
10452
|
+
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
10453
|
+
let firstBlock;
|
|
10454
|
+
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
10455
|
+
let partialLine = "";
|
|
10456
|
+
const readLines = (chunk) => {
|
|
10457
|
+
const lines = (partialLine + chunk).split(/\r|\n/);
|
|
10458
|
+
partialLine = lines.pop() ?? "";
|
|
10459
|
+
for (const line of lines) {
|
|
10460
|
+
const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
|
|
10461
|
+
if (!ftpk) continue;
|
|
10462
|
+
const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
|
|
10463
|
+
if (firstBlock === void 0) firstBlock = block;
|
|
10464
|
+
else laterBlocks = Math.max(laterBlocks, block);
|
|
10465
|
+
}
|
|
10466
|
+
};
|
|
10467
|
+
const result = await runFfmpeg4(
|
|
10468
|
+
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
10469
|
+
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
10470
|
+
);
|
|
10471
|
+
if (!result.success) {
|
|
10472
|
+
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
10473
|
+
}
|
|
10474
|
+
const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
10475
|
+
return {
|
|
10476
|
+
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
10477
|
+
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
10478
|
+
laterBlocks: checkedDbfs(laterBlocks)
|
|
10479
|
+
};
|
|
10480
|
+
}
|
|
10481
|
+
async function measureTruePeakDbfs(audioPath, options) {
|
|
10482
|
+
const { timeoutMs, signal } = options;
|
|
10483
|
+
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
10484
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
10485
|
+
const block = 4800;
|
|
10486
|
+
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
10487
|
+
if (count === 1) return wholeFile();
|
|
10488
|
+
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
10489
|
+
const peers = new AbortController();
|
|
10490
|
+
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
10491
|
+
const scans = await Promise.all(
|
|
10492
|
+
Array.from({ length: count }, (_, k) => {
|
|
10493
|
+
const start = k === 0 ? 0 : cut(k) - block;
|
|
10494
|
+
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
10495
|
+
return scanTruePeak(
|
|
10496
|
+
audioPath,
|
|
10497
|
+
`atrim=start_sample=${start}${end},`,
|
|
10498
|
+
timeoutMs,
|
|
10499
|
+
segmentSignal
|
|
10500
|
+
);
|
|
10501
|
+
})
|
|
10502
|
+
).catch((error) => {
|
|
10503
|
+
peers.abort();
|
|
10504
|
+
throw error;
|
|
10505
|
+
});
|
|
10506
|
+
const last = scans[count - 1];
|
|
10507
|
+
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
10508
|
+
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
10509
|
+
return wholeFile();
|
|
10510
|
+
}
|
|
10511
|
+
|
|
10512
|
+
// src/services/render/audioPadTrim.ts
|
|
10384
10513
|
var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
10385
10514
|
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
10386
10515
|
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
10387
10516
|
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
10388
|
-
function
|
|
10389
|
-
return
|
|
10390
|
-
"-i",
|
|
10391
|
-
inputPath,
|
|
10392
|
-
"-map",
|
|
10393
|
-
"0:a:0",
|
|
10394
|
-
"-vn",
|
|
10395
|
-
"-af",
|
|
10396
|
-
`volume=${attenuationDb.toFixed(3)}dB`,
|
|
10397
|
-
"-t",
|
|
10398
|
-
formatSeconds(targetDurationSeconds),
|
|
10399
|
-
"-c:a",
|
|
10400
|
-
"aac",
|
|
10401
|
-
"-b:a",
|
|
10402
|
-
"192k",
|
|
10403
|
-
"-y",
|
|
10404
|
-
outputPath
|
|
10405
|
-
];
|
|
10517
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
10518
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
10406
10519
|
}
|
|
10407
10520
|
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
10408
10521
|
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
@@ -10417,53 +10530,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
|
|
|
10417
10530
|
if (delta > 0) {
|
|
10418
10531
|
return {
|
|
10419
10532
|
operation: "pad",
|
|
10420
|
-
steps: [
|
|
10421
|
-
{
|
|
10422
|
-
kind: "normalize",
|
|
10423
|
-
args: [
|
|
10424
|
-
"-i",
|
|
10425
|
-
audioPath,
|
|
10426
|
-
"-af",
|
|
10427
|
-
`apad,atrim=0:${targetSec}`,
|
|
10428
|
-
"-t",
|
|
10429
|
-
targetSec,
|
|
10430
|
-
"-c:a",
|
|
10431
|
-
"aac",
|
|
10432
|
-
"-b:a",
|
|
10433
|
-
"192k",
|
|
10434
|
-
"-y",
|
|
10435
|
-
outputPath
|
|
10436
|
-
]
|
|
10437
|
-
}
|
|
10438
|
-
],
|
|
10533
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
10439
10534
|
cleanupPaths: []
|
|
10440
10535
|
};
|
|
10441
10536
|
}
|
|
10442
10537
|
return {
|
|
10443
10538
|
operation: "trim",
|
|
10444
|
-
steps: [
|
|
10445
|
-
{
|
|
10446
|
-
kind: "trim",
|
|
10447
|
-
args: [
|
|
10448
|
-
"-i",
|
|
10449
|
-
audioPath,
|
|
10450
|
-
"-af",
|
|
10451
|
-
`atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
|
|
10452
|
-
// `atrim` limits decoded samples but does not cap muxer timestamps
|
|
10453
|
-
// introduced by encoder delay/flush. The output duration contract
|
|
10454
|
-
// is enforced at the container boundary as well; without `-t`, a
|
|
10455
|
-
// long primed source can retain its original tail after re-encode.
|
|
10456
|
-
"-t",
|
|
10457
|
-
targetSec,
|
|
10458
|
-
"-c:a",
|
|
10459
|
-
"aac",
|
|
10460
|
-
"-b:a",
|
|
10461
|
-
"192k",
|
|
10462
|
-
"-y",
|
|
10463
|
-
outputPath
|
|
10464
|
-
]
|
|
10465
|
-
}
|
|
10466
|
-
],
|
|
10539
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
10467
10540
|
cleanupPaths: []
|
|
10468
10541
|
};
|
|
10469
10542
|
}
|
|
@@ -10480,7 +10553,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10480
10553
|
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
10481
10554
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
10482
10555
|
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
10483
|
-
const
|
|
10556
|
+
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
10557
|
+
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
10484
10558
|
const [videoResult, audioResult] = await Promise.allSettled([
|
|
10485
10559
|
probeVideo(input.videoPath),
|
|
10486
10560
|
probeAudio(input.audioPath, input.signal)
|
|
@@ -10527,7 +10601,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10527
10601
|
);
|
|
10528
10602
|
try {
|
|
10529
10603
|
for (const step of plan2.steps) {
|
|
10530
|
-
const ffmpegResult = await runner(step.args);
|
|
10604
|
+
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
10531
10605
|
if (!ffmpegResult.success) {
|
|
10532
10606
|
return {
|
|
10533
10607
|
success: false,
|
|
@@ -10559,7 +10633,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10559
10633
|
targetDurationSeconds,
|
|
10560
10634
|
signal: input.signal,
|
|
10561
10635
|
probeTruePeak,
|
|
10562
|
-
|
|
10636
|
+
encode
|
|
10563
10637
|
});
|
|
10564
10638
|
if (!correction.success) {
|
|
10565
10639
|
return {
|
|
@@ -10589,8 +10663,8 @@ async function constrainAacTruePeak(input) {
|
|
|
10589
10663
|
audioPath: input.audioPath,
|
|
10590
10664
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
10591
10665
|
signal: input.signal,
|
|
10592
|
-
probeTruePeak: (audioPath, signal) =>
|
|
10593
|
-
|
|
10666
|
+
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
10667
|
+
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
10594
10668
|
});
|
|
10595
10669
|
}
|
|
10596
10670
|
async function enforceAacTruePeak(input) {
|
|
@@ -10617,13 +10691,11 @@ async function enforceAacTruePeak(input) {
|
|
|
10617
10691
|
}
|
|
10618
10692
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
10619
10693
|
attenuationDb += correctionTargetDbfs - truePeakDbfs;
|
|
10620
|
-
const result = await input.
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
attenuationDb
|
|
10626
|
-
)
|
|
10694
|
+
const result = await input.encode(
|
|
10695
|
+
input.audioPath,
|
|
10696
|
+
correctedPath,
|
|
10697
|
+
input.targetDurationSeconds,
|
|
10698
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
10627
10699
|
);
|
|
10628
10700
|
if (!result.success) {
|
|
10629
10701
|
return {
|
|
@@ -10718,7 +10790,7 @@ function parseFrameRate(rate) {
|
|
|
10718
10790
|
return { fpsNum, fpsDen };
|
|
10719
10791
|
}
|
|
10720
10792
|
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
10721
|
-
const metadata = await
|
|
10793
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
10722
10794
|
return {
|
|
10723
10795
|
durationSeconds: metadata.durationSeconds,
|
|
10724
10796
|
sampleRate: metadata.sampleRate,
|
|
@@ -10726,28 +10798,23 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
10726
10798
|
audioCodec: metadata.audioCodec
|
|
10727
10799
|
};
|
|
10728
10800
|
}
|
|
10729
|
-
async function defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal) {
|
|
10730
|
-
const result = await runFfmpeg4(
|
|
10731
|
-
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"],
|
|
10732
|
-
{ signal, timeout: timeoutMs }
|
|
10733
|
-
);
|
|
10734
|
-
if (!result.success) {
|
|
10735
|
-
throw new Error(`[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
10736
|
-
}
|
|
10737
|
-
const matches = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
10738
|
-
const token = matches.at(-1)?.[1]?.toLowerCase();
|
|
10739
|
-
const value = token === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
10740
|
-
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
10741
|
-
throw new Error("[audioPadTrim] FFmpeg did not report AAC true peak");
|
|
10742
|
-
}
|
|
10743
|
-
return value;
|
|
10744
|
-
}
|
|
10745
10801
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
10746
|
-
|
|
10802
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
10803
|
+
}
|
|
10804
|
+
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
10805
|
+
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
10806
|
+
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
10807
|
+
return toStepResult(
|
|
10808
|
+
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
10809
|
+
timeoutMs
|
|
10810
|
+
);
|
|
10811
|
+
};
|
|
10812
|
+
}
|
|
10813
|
+
function toStepResult(result, timeoutMs) {
|
|
10747
10814
|
if (result.success) return { success: true };
|
|
10748
10815
|
return {
|
|
10749
10816
|
success: false,
|
|
10750
|
-
error: `[audioPadTrim] ${
|
|
10817
|
+
error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
|
|
10751
10818
|
failureReason: result.failureReason
|
|
10752
10819
|
};
|
|
10753
10820
|
}
|
|
@@ -10931,7 +10998,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
|
|
|
10931
10998
|
const reason = UNSUPPORTED_REASON2[route];
|
|
10932
10999
|
if (reason === null) return;
|
|
10933
11000
|
throw new Error(
|
|
10934
|
-
`[Render] --resolution other than the composition's size is not supported on the "${route}" capture route: ${reason}. Render at the composition's size, or
|
|
11001
|
+
`[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.`
|
|
10935
11002
|
);
|
|
10936
11003
|
}
|
|
10937
11004
|
|
|
@@ -11148,7 +11215,7 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11148
11215
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11149
11216
|
}
|
|
11150
11217
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11151
|
-
if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
|
|
11218
|
+
if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
|
|
11152
11219
|
return join26(dirname10(outputPath), `work-${jobId}-`);
|
|
11153
11220
|
}
|
|
11154
11221
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
@@ -11642,6 +11709,9 @@ function resolveDeShortBand(args) {
|
|
|
11642
11709
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
11643
11710
|
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;
|
|
11644
11711
|
}
|
|
11712
|
+
function isProbeDeGated(session) {
|
|
11713
|
+
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
11714
|
+
}
|
|
11645
11715
|
function resolveInversionRetryPlan(args) {
|
|
11646
11716
|
if (args.deWorkerInversion !== "inverted") return null;
|
|
11647
11717
|
const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
|
|
@@ -11723,7 +11793,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
|
|
|
11723
11793
|
}
|
|
11724
11794
|
function resolveParallelCaptureMode(args) {
|
|
11725
11795
|
const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
|
|
11726
|
-
|
|
11796
|
+
const headlessShell = args.headlessShellPath !== void 0;
|
|
11797
|
+
return headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
|
|
11727
11798
|
}
|
|
11728
11799
|
function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
|
|
11729
11800
|
const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
|
|
@@ -12331,6 +12402,7 @@ async function executeRenderPipeline(input) {
|
|
|
12331
12402
|
);
|
|
12332
12403
|
const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
|
|
12333
12404
|
const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
|
|
12405
|
+
if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
|
|
12334
12406
|
const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
|
|
12335
12407
|
const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
|
|
12336
12408
|
const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
|
|
@@ -12371,7 +12443,7 @@ async function executeRenderPipeline(input) {
|
|
|
12371
12443
|
singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
|
|
12372
12444
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12373
12445
|
supersampling: deviceScaleFactor !== 1,
|
|
12374
|
-
probeDeGated: probeSession
|
|
12446
|
+
probeDeGated: isProbeDeGated(probeSession),
|
|
12375
12447
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
|
|
12376
12448
|
process.env.HF_DE_PARALLEL_STREAM === "true"
|
|
12377
12449
|
};
|
|
@@ -12380,14 +12452,14 @@ async function executeRenderPipeline(input) {
|
|
|
12380
12452
|
...deInversionArgs,
|
|
12381
12453
|
minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
|
|
12382
12454
|
});
|
|
12383
|
-
|
|
12455
|
+
let deShortBand = resolveDeShortBand({
|
|
12384
12456
|
invertAtBaseFloor,
|
|
12385
12457
|
invertAtBandFloor,
|
|
12386
12458
|
bandEnabled: deShortBandEnabled,
|
|
12387
12459
|
bandOpen: deShortBandOpen,
|
|
12388
12460
|
elementCountSource: compositionElementCountSource
|
|
12389
12461
|
});
|
|
12390
|
-
|
|
12462
|
+
let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
|
|
12391
12463
|
const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
|
|
12392
12464
|
const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
|
|
12393
12465
|
const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
|
|
@@ -12396,7 +12468,7 @@ async function executeRenderPipeline(input) {
|
|
|
12396
12468
|
const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
|
|
12397
12469
|
const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
|
|
12398
12470
|
const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
|
|
12399
|
-
|
|
12471
|
+
let deParallelRouterEligible = shouldPreferParallelDrawElement({
|
|
12400
12472
|
workerCount: WOULD_RESOLVE_MULTI_WORKER,
|
|
12401
12473
|
requestedWorkers: job.config.workers,
|
|
12402
12474
|
useDrawElement: cfg.useDrawElement,
|
|
@@ -12407,7 +12479,7 @@ async function executeRenderPipeline(input) {
|
|
|
12407
12479
|
minFrames: deParallelMinFrames,
|
|
12408
12480
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12409
12481
|
supersampling: deviceScaleFactor !== 1,
|
|
12410
|
-
probeDeGated: probeSession
|
|
12482
|
+
probeDeGated: isProbeDeGated(probeSession),
|
|
12411
12483
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12412
12484
|
routerEnabled: deParallelRouterEnabled,
|
|
12413
12485
|
// Router pins 3 workers for the streaming path; don't pin when a
|
|
@@ -12423,6 +12495,35 @@ async function executeRenderPipeline(input) {
|
|
|
12423
12495
|
totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
|
|
12424
12496
|
minMemoryMb: deParallelMinMemoryMb
|
|
12425
12497
|
});
|
|
12498
|
+
if (probeSession === null && (deInversionEligible || deParallelRouterEligible)) {
|
|
12499
|
+
try {
|
|
12500
|
+
probeSession = await createCaptureSession8(
|
|
12501
|
+
activeFileServer.url,
|
|
12502
|
+
framesDir,
|
|
12503
|
+
buildCaptureOptions(),
|
|
12504
|
+
null,
|
|
12505
|
+
{ ...cfg, forceScreenshot: captureForceScreenshot }
|
|
12506
|
+
);
|
|
12507
|
+
await initializeSession8(probeSession);
|
|
12508
|
+
} catch (error) {
|
|
12509
|
+
assertNotAborted();
|
|
12510
|
+
if (probeSession) {
|
|
12511
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
12512
|
+
await closeCaptureSession8(probeSession).catch(() => {
|
|
12513
|
+
});
|
|
12514
|
+
probeSession = null;
|
|
12515
|
+
}
|
|
12516
|
+
log.warn("[Render] Fast capture gate check failed; routing without it.", {
|
|
12517
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12518
|
+
});
|
|
12519
|
+
}
|
|
12520
|
+
if (isProbeDeGated(probeSession)) {
|
|
12521
|
+
deInversionEligible = false;
|
|
12522
|
+
deParallelRouterEligible = false;
|
|
12523
|
+
deShortBand = void 0;
|
|
12524
|
+
}
|
|
12525
|
+
}
|
|
12526
|
+
const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
|
|
12426
12527
|
let workerCount;
|
|
12427
12528
|
const captureStageObservationData = (extra = {}) => ({
|
|
12428
12529
|
stagePhase: "capturing",
|
|
@@ -12610,7 +12711,7 @@ async function executeRenderPipeline(input) {
|
|
|
12610
12711
|
}
|
|
12611
12712
|
const parallelCaptureMode = resolveParallelCaptureMode({
|
|
12612
12713
|
platform: process.platform,
|
|
12613
|
-
|
|
12714
|
+
headlessShellPath: resolveHeadlessShellPath(cfg),
|
|
12614
12715
|
forceScreenshot: captureForceScreenshot,
|
|
12615
12716
|
deviceScaleFactor: captureOptions.deviceScaleFactor
|
|
12616
12717
|
});
|
|
@@ -13382,7 +13483,9 @@ async function executeRenderPipeline(input) {
|
|
|
13382
13483
|
outputFormat,
|
|
13383
13484
|
durationSeconds: job.duration,
|
|
13384
13485
|
fps: fpsToNumber9(job.config.fps),
|
|
13385
|
-
expectedFrames: captureTotalFrames
|
|
13486
|
+
expectedFrames: captureTotalFrames,
|
|
13487
|
+
width: outputWidth,
|
|
13488
|
+
height: outputHeight
|
|
13386
13489
|
})
|
|
13387
13490
|
);
|
|
13388
13491
|
const totalElapsed = Date.now() - pipelineStart;
|
|
@@ -13418,6 +13521,7 @@ async function executeRenderPipeline(input) {
|
|
|
13418
13521
|
drawElement: {
|
|
13419
13522
|
compileGate: deCompileGate,
|
|
13420
13523
|
clampReason: deClampReason,
|
|
13524
|
+
probeGateReason: deProbeGateReason,
|
|
13421
13525
|
workerInversion: deWorkerInversion,
|
|
13422
13526
|
preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
|
|
13423
13527
|
compositionElementCount,
|
|
@@ -13615,9 +13719,9 @@ import {
|
|
|
13615
13719
|
import { join as join29 } from "node:path";
|
|
13616
13720
|
import {
|
|
13617
13721
|
closeCaptureSession as closeCaptureSession9,
|
|
13618
|
-
createCaptureSession as
|
|
13722
|
+
createCaptureSession as createCaptureSession9,
|
|
13619
13723
|
createFrameLookupTable as createFrameLookupTable3,
|
|
13620
|
-
initializeSession as
|
|
13724
|
+
initializeSession as initializeSession9
|
|
13621
13725
|
} from "@hyperframes/engine";
|
|
13622
13726
|
|
|
13623
13727
|
// src/services/distributed/chunkFrames.ts
|
|
@@ -14015,6 +14119,29 @@ function buildChunkCaptureOptions(input) {
|
|
|
14015
14119
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
14016
14120
|
};
|
|
14017
14121
|
}
|
|
14122
|
+
function shownDeferredFrameIndexes(planVideos, frames, fps) {
|
|
14123
|
+
const shown = /* @__PURE__ */ new Map();
|
|
14124
|
+
const deferred = planVideos.extracted.filter((video) => video.deferredRange);
|
|
14125
|
+
if (deferred.length === 0) return shown;
|
|
14126
|
+
const table = createFrameLookupTable2(
|
|
14127
|
+
planVideos.videos,
|
|
14128
|
+
deferred.map((video) => ({
|
|
14129
|
+
...video,
|
|
14130
|
+
outputDir: "",
|
|
14131
|
+
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
14132
|
+
})),
|
|
14133
|
+
resolveRenderFpsConfig(fps).value
|
|
14134
|
+
);
|
|
14135
|
+
for (const frame of frames) {
|
|
14136
|
+
const globalTime = frame * fps.den / fps.num;
|
|
14137
|
+
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
14138
|
+
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
14139
|
+
indexes.add(payload.frameIndex);
|
|
14140
|
+
shown.set(videoId, indexes);
|
|
14141
|
+
}
|
|
14142
|
+
}
|
|
14143
|
+
return shown;
|
|
14144
|
+
}
|
|
14018
14145
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
14019
14146
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
14020
14147
|
const remoteSources = input.remoteSources ?? {};
|
|
@@ -14031,24 +14158,7 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14031
14158
|
});
|
|
14032
14159
|
}
|
|
14033
14160
|
if (deferred.length === 0) return [];
|
|
14034
|
-
const
|
|
14035
|
-
input.planVideos.videos,
|
|
14036
|
-
deferred.map((video) => ({
|
|
14037
|
-
...video,
|
|
14038
|
-
outputDir: input.outputRoot,
|
|
14039
|
-
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
14040
|
-
})),
|
|
14041
|
-
resolveRenderFpsConfig(input.fps).value
|
|
14042
|
-
);
|
|
14043
|
-
const shown = /* @__PURE__ */ new Map();
|
|
14044
|
-
for (const frame of input.frames) {
|
|
14045
|
-
const globalTime = frame * input.fps.den / input.fps.num;
|
|
14046
|
-
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
14047
|
-
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
14048
|
-
indexes.add(payload.frameIndex);
|
|
14049
|
-
shown.set(videoId, indexes);
|
|
14050
|
-
}
|
|
14051
|
-
}
|
|
14161
|
+
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
14052
14162
|
return Promise.all(
|
|
14053
14163
|
deferred.map(async (video) => {
|
|
14054
14164
|
const outputDir = join28(input.outputRoot, video.videoId);
|
|
@@ -14074,7 +14184,8 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14074
14184
|
fps: input.fps,
|
|
14075
14185
|
outputDir: input.outputRoot,
|
|
14076
14186
|
format: range.format,
|
|
14077
|
-
frameRanges: contiguousRuns(indexes)
|
|
14187
|
+
frameRanges: contiguousRuns(indexes),
|
|
14188
|
+
shownBox: input.shownBoxes?.get(video.videoId)
|
|
14078
14189
|
},
|
|
14079
14190
|
void 0,
|
|
14080
14191
|
input.cfg
|
|
@@ -14121,7 +14232,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14121
14232
|
});
|
|
14122
14233
|
let session = null;
|
|
14123
14234
|
try {
|
|
14124
|
-
session = await
|
|
14235
|
+
session = await createCaptureSession9(
|
|
14125
14236
|
fileServer.url,
|
|
14126
14237
|
join29(workDir, "frame-cost"),
|
|
14127
14238
|
buildChunkCaptureOptions({
|
|
@@ -14133,7 +14244,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14133
14244
|
createChunkVideoFrameInjectorFactory(lookup)(),
|
|
14134
14245
|
createCaptureCalibrationConfig(input.cfg)
|
|
14135
14246
|
);
|
|
14136
|
-
await
|
|
14247
|
+
await initializeSession9(session);
|
|
14137
14248
|
const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
|
|
14138
14249
|
return (estimate.p95Ms ?? 0) / 1e3;
|
|
14139
14250
|
} finally {
|
|
@@ -14529,7 +14640,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
14529
14640
|
import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
|
|
14530
14641
|
import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
|
|
14531
14642
|
import { join as join32 } from "node:path";
|
|
14532
|
-
import { appendRenderProvenanceArgs, describeFfmpegFailure as
|
|
14643
|
+
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
14533
14644
|
|
|
14534
14645
|
// src/services/distributed/mp4Stitch.ts
|
|
14535
14646
|
import {
|
|
@@ -14727,6 +14838,20 @@ function withDuration(buf, target, duration) {
|
|
|
14727
14838
|
function timescaleOf(buf, header) {
|
|
14728
14839
|
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
14729
14840
|
}
|
|
14841
|
+
var TRACK_ID_AT = { tkhd: [12, 20], mvhd: [96, 108] };
|
|
14842
|
+
function idOf(buf, target) {
|
|
14843
|
+
return buf.readUInt32BE(
|
|
14844
|
+
target.bodyStart + TRACK_ID_AT[target.type][buf[target.bodyStart] === 1 ? 1 : 0]
|
|
14845
|
+
);
|
|
14846
|
+
}
|
|
14847
|
+
function withId(boxBytes, type, id) {
|
|
14848
|
+
const header = boxAt(boxBytes, 0, 0, boxBytes.length);
|
|
14849
|
+
boxBytes.writeUInt32BE(
|
|
14850
|
+
id,
|
|
14851
|
+
header.bodyStart + TRACK_ID_AT[type][boxBytes[header.bodyStart] === 1 ? 1 : 0]
|
|
14852
|
+
);
|
|
14853
|
+
return boxBytes;
|
|
14854
|
+
}
|
|
14730
14855
|
function buildStbl(stsd, track) {
|
|
14731
14856
|
const stts = runs(track.durations);
|
|
14732
14857
|
const stsc = [];
|
|
@@ -14768,24 +14893,48 @@ function planTracksOf(tracks, plan2, index) {
|
|
|
14768
14893
|
}
|
|
14769
14894
|
return matched;
|
|
14770
14895
|
}
|
|
14771
|
-
function
|
|
14772
|
-
const
|
|
14773
|
-
const
|
|
14896
|
+
function sourceTracks(layouts, audio) {
|
|
14897
|
+
const chunkPlan = readTracks(layouts[0].moov);
|
|
14898
|
+
const audioTracks = audio ? readTracks(audio.moov) : [];
|
|
14899
|
+
if (audio && (audioTracks.length !== 1 || audioTracks[0].handler !== "soun")) {
|
|
14900
|
+
throw new Error("[mp4Stitch] the separate audio file must hold one audio track");
|
|
14901
|
+
}
|
|
14902
|
+
const plan2 = [...chunkPlan, ...audioTracks];
|
|
14903
|
+
const chunks = layouts.map((layout, i) => {
|
|
14904
|
+
const tracks = readTracks(layout.moov);
|
|
14905
|
+
if (audio && tracks.some((track) => track.handler === "soun")) {
|
|
14906
|
+
throw new Error(`[mp4Stitch] chunk ${i} carries its own audio`);
|
|
14907
|
+
}
|
|
14908
|
+
return planTracksOf(tracks, plan2, i);
|
|
14909
|
+
});
|
|
14910
|
+
return {
|
|
14911
|
+
plan: plan2,
|
|
14912
|
+
chunkTrackCount: chunkPlan.length,
|
|
14913
|
+
sources: audio ? [[...chunkPlan.map(() => null), audioTracks[0]], ...chunks] : chunks
|
|
14914
|
+
};
|
|
14915
|
+
}
|
|
14916
|
+
function buildStitchedHeader(layouts, audio) {
|
|
14917
|
+
const { plan: plan2, chunkTrackCount, sources } = sourceTracks(layouts, audio);
|
|
14918
|
+
const sourceLayouts = audio ? [audio, ...layouts] : layouts;
|
|
14774
14919
|
const payloadBases = [];
|
|
14775
14920
|
let payloadBytes = 0;
|
|
14776
|
-
for (const layout of
|
|
14921
|
+
for (const layout of sourceLayouts) {
|
|
14777
14922
|
payloadBases.push(payloadBytes);
|
|
14778
14923
|
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
14779
14924
|
}
|
|
14780
14925
|
const template = layouts[0].moov;
|
|
14781
14926
|
const root = boxAt(template, 0, 0, template.length);
|
|
14782
|
-
const
|
|
14927
|
+
const mvhd = child(template, root, "mvhd");
|
|
14928
|
+
const movieTimescale = timescaleOf(template, mvhd);
|
|
14929
|
+
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child(template, track.trak, "tkhd")));
|
|
14930
|
+
const audioTrackId = Math.max(...chunkTrackIds) + 1;
|
|
14783
14931
|
const buildMoov = (dataStart2) => {
|
|
14784
14932
|
const traks = plan2.map((track, t) => {
|
|
14785
|
-
const parts =
|
|
14933
|
+
const parts = sources.flatMap((tracks, chunk) => {
|
|
14786
14934
|
const part = tracks[t];
|
|
14787
14935
|
return part ? [{ ...part, chunk }] : [];
|
|
14788
14936
|
});
|
|
14937
|
+
const moov = t < chunkTrackCount ? template : audio.moov;
|
|
14789
14938
|
const merged = {
|
|
14790
14939
|
durations: parts.flatMap((part) => part.durations),
|
|
14791
14940
|
sizes: parts.flatMap((part) => part.sizes),
|
|
@@ -14794,9 +14943,9 @@ function buildStitchedHeader(layouts) {
|
|
|
14794
14943
|
chunkOffsets: parts.flatMap(
|
|
14795
14944
|
(part) => part.chunkOffsets.map((offset) => {
|
|
14796
14945
|
const i = part.chunk;
|
|
14797
|
-
const { payloadStart, payloadEnd } =
|
|
14946
|
+
const { payloadStart, payloadEnd } = sourceLayouts[i];
|
|
14798
14947
|
if (offset < payloadStart || offset >= payloadEnd) {
|
|
14799
|
-
throw new Error(`[mp4Stitch]
|
|
14948
|
+
throw new Error(`[mp4Stitch] source ${i} has samples outside its mdat`);
|
|
14800
14949
|
}
|
|
14801
14950
|
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
14802
14951
|
})
|
|
@@ -14812,20 +14961,25 @@ function buildStitchedHeader(layouts) {
|
|
|
14812
14961
|
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
14813
14962
|
elst.writeUInt32BE(65536, 20);
|
|
14814
14963
|
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
14815
|
-
const
|
|
14816
|
-
|
|
14964
|
+
const tkhdOf = (tkhd) => {
|
|
14965
|
+
const timed = withDuration(moov, tkhd, shown);
|
|
14966
|
+
return t < chunkTrackCount ? timed : withId(timed, "tkhd", audioTrackId);
|
|
14967
|
+
};
|
|
14968
|
+
const trak = rebuild(moov, track.trak, {
|
|
14969
|
+
tkhd: (tkhd) => Buffer.concat([tkhdOf(tkhd), edts]),
|
|
14817
14970
|
edts: () => Buffer.alloc(0),
|
|
14818
|
-
mdia: (mdia) => rebuild(
|
|
14819
|
-
mdhd: (mdhd) => withDuration(
|
|
14820
|
-
minf: (minf) => rebuild(
|
|
14971
|
+
mdia: (mdia) => rebuild(moov, mdia, {
|
|
14972
|
+
mdhd: (mdhd) => withDuration(moov, mdhd, mediaDuration),
|
|
14973
|
+
minf: (minf) => rebuild(moov, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
14821
14974
|
})
|
|
14822
14975
|
});
|
|
14823
14976
|
return { trak, shown };
|
|
14824
14977
|
});
|
|
14825
14978
|
let next = 0;
|
|
14979
|
+
const movieHeader = withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown)));
|
|
14826
14980
|
return rebuild(template, root, {
|
|
14827
|
-
mvhd: (
|
|
14828
|
-
trak: () => traks[next
|
|
14981
|
+
mvhd: () => audio ? withId(movieHeader, "mvhd", audioTrackId + 1) : movieHeader,
|
|
14982
|
+
trak: () => ++next < chunkTrackCount ? traks[next - 1].trak : Buffer.concat(traks.slice(next - 1).map((t) => t.trak))
|
|
14829
14983
|
});
|
|
14830
14984
|
};
|
|
14831
14985
|
const ftyp = layouts[0].ftyp;
|
|
@@ -14858,15 +15012,16 @@ async function copyPayload(path, layout, index, out) {
|
|
|
14858
15012
|
);
|
|
14859
15013
|
}
|
|
14860
15014
|
}
|
|
14861
|
-
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
14862
|
-
const
|
|
14863
|
-
const
|
|
15015
|
+
async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
15016
|
+
const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
|
|
15017
|
+
const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
|
|
15018
|
+
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
14864
15019
|
mkdirSync11(dirname12(outputPath), { recursive: true });
|
|
14865
15020
|
const workDir = mkdtempSync4(`${outputPath}.stitch-`);
|
|
14866
15021
|
const out = createWriteStream(join31(workDir, "out.mp4"));
|
|
14867
15022
|
try {
|
|
14868
15023
|
out.write(header);
|
|
14869
|
-
for (const [i, path] of
|
|
15024
|
+
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
14870
15025
|
out.end();
|
|
14871
15026
|
await finished(out);
|
|
14872
15027
|
renameSync5(join31(workDir, "out.mp4"), outputPath);
|
|
@@ -15019,10 +15174,10 @@ async function muxChunkWithAudio(input) {
|
|
|
15019
15174
|
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
15020
15175
|
appendRenderProvenanceArgs(args, input.outputPath);
|
|
15021
15176
|
args.push("-y", input.outputPath);
|
|
15022
|
-
const result = await
|
|
15177
|
+
const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
15023
15178
|
if (!result.success) {
|
|
15024
15179
|
throw new Error(
|
|
15025
|
-
`[chunkAudio] chunk mux failed: ${
|
|
15180
|
+
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
|
|
15026
15181
|
);
|
|
15027
15182
|
}
|
|
15028
15183
|
const slice = input.chunks[input.chunkIndex];
|
|
@@ -15437,7 +15592,8 @@ function buildLockedRenderConfig(input) {
|
|
|
15437
15592
|
chunkCount: input.chunkCount,
|
|
15438
15593
|
runtimeEnv: input.runtimeEnv,
|
|
15439
15594
|
variables: config.variables,
|
|
15440
|
-
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
|
|
15595
|
+
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
15596
|
+
...input.separateAudio ? { separateAudio: true } : {}
|
|
15441
15597
|
};
|
|
15442
15598
|
}
|
|
15443
15599
|
function carriesChunkAudio(config, options) {
|
|
@@ -15532,8 +15688,10 @@ function createVideoSourceShipper(input) {
|
|
|
15532
15688
|
return result;
|
|
15533
15689
|
};
|
|
15534
15690
|
}
|
|
15535
|
-
|
|
15536
|
-
|
|
15691
|
+
function plansSeparateAudio(config, options) {
|
|
15692
|
+
return options.deferAudio === true && config.format === "mp4" && config.cfr !== true;
|
|
15693
|
+
}
|
|
15694
|
+
async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
15537
15695
|
rejectUnsupportedDistributedFormat(config);
|
|
15538
15696
|
validateNoGpuEncode({
|
|
15539
15697
|
useGpu: false,
|
|
@@ -15669,6 +15827,100 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15669
15827
|
});
|
|
15670
15828
|
}
|
|
15671
15829
|
}
|
|
15830
|
+
return {
|
|
15831
|
+
projectDir,
|
|
15832
|
+
log,
|
|
15833
|
+
sizeLimitBytes,
|
|
15834
|
+
abortSignal,
|
|
15835
|
+
assertNotAborted,
|
|
15836
|
+
cfg,
|
|
15837
|
+
job,
|
|
15838
|
+
workDir,
|
|
15839
|
+
compiledDir,
|
|
15840
|
+
finalCompiledDir,
|
|
15841
|
+
remoteFiles,
|
|
15842
|
+
urlByAbsentLocalPath,
|
|
15843
|
+
localPathByUrl,
|
|
15844
|
+
compileResult,
|
|
15845
|
+
compiled,
|
|
15846
|
+
composition,
|
|
15847
|
+
deviceScaleFactor,
|
|
15848
|
+
forceScreenshot,
|
|
15849
|
+
width,
|
|
15850
|
+
height,
|
|
15851
|
+
probeResult,
|
|
15852
|
+
totalFrames
|
|
15853
|
+
};
|
|
15854
|
+
}
|
|
15855
|
+
function mixPlanAudio(c, compiledDir, seconds, pcmPath) {
|
|
15856
|
+
return runAudioStage({
|
|
15857
|
+
projectDir: c.projectDir,
|
|
15858
|
+
workDir: c.workDir,
|
|
15859
|
+
compiledDir,
|
|
15860
|
+
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
15861
|
+
duration: seconds,
|
|
15862
|
+
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
15863
|
+
audioGain: c.cfg.audioGain,
|
|
15864
|
+
audios: c.composition.audios,
|
|
15865
|
+
masterFxChain: c.composition.masterFxChain,
|
|
15866
|
+
...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
|
|
15867
|
+
abortSignal: c.abortSignal,
|
|
15868
|
+
assertNotAborted: c.assertNotAborted
|
|
15869
|
+
});
|
|
15870
|
+
}
|
|
15871
|
+
async function extractPlanVideos(c, deferRangeExtraction) {
|
|
15872
|
+
const extractResult = await runExtractVideosStage({
|
|
15873
|
+
projectDir: c.projectDir,
|
|
15874
|
+
compiledDir: c.compiledDir,
|
|
15875
|
+
job: c.job,
|
|
15876
|
+
cfg: c.cfg,
|
|
15877
|
+
composition: c.composition,
|
|
15878
|
+
abortSignal: c.abortSignal,
|
|
15879
|
+
assertNotAborted: c.assertNotAborted,
|
|
15880
|
+
materializeSymlinks: true,
|
|
15881
|
+
deferRangeExtraction,
|
|
15882
|
+
urlByAbsentLocalPath: c.urlByAbsentLocalPath
|
|
15883
|
+
});
|
|
15884
|
+
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
15885
|
+
if (extractResult.extractionResult) {
|
|
15886
|
+
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
15887
|
+
}
|
|
15888
|
+
return extractResult;
|
|
15889
|
+
}
|
|
15890
|
+
async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
|
|
15891
|
+
const peak = await constrainAacTruePeak({
|
|
15892
|
+
audioPath,
|
|
15893
|
+
...existsSync21(pcmPath) ? { sourcePath: pcmPath } : {},
|
|
15894
|
+
targetDurationSeconds: seconds,
|
|
15895
|
+
signal: c.abortSignal,
|
|
15896
|
+
timeoutMs: c.cfg.ffmpegProcessTimeout
|
|
15897
|
+
}).finally(() => rmSync11(pcmPath, { force: true }));
|
|
15898
|
+
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
15899
|
+
}
|
|
15900
|
+
async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
|
|
15901
|
+
const planDir = executionPlanDir;
|
|
15902
|
+
const c = await compilePlanComposition(projectDir, config, planDir, options);
|
|
15903
|
+
const {
|
|
15904
|
+
log,
|
|
15905
|
+
sizeLimitBytes,
|
|
15906
|
+
cfg,
|
|
15907
|
+
job,
|
|
15908
|
+
workDir,
|
|
15909
|
+
compiledDir,
|
|
15910
|
+
finalCompiledDir,
|
|
15911
|
+
remoteFiles,
|
|
15912
|
+
urlByAbsentLocalPath,
|
|
15913
|
+
localPathByUrl,
|
|
15914
|
+
compileResult,
|
|
15915
|
+
compiled,
|
|
15916
|
+
composition,
|
|
15917
|
+
deviceScaleFactor,
|
|
15918
|
+
forceScreenshot,
|
|
15919
|
+
width,
|
|
15920
|
+
height,
|
|
15921
|
+
probeResult,
|
|
15922
|
+
totalFrames
|
|
15923
|
+
} = c;
|
|
15672
15924
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
15673
15925
|
let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
|
|
15674
15926
|
const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
|
|
@@ -15679,24 +15931,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15679
15931
|
observedAt: "pre-extract",
|
|
15680
15932
|
log
|
|
15681
15933
|
});
|
|
15682
|
-
stageStarted = Date.now();
|
|
15683
|
-
const extractResult = await
|
|
15684
|
-
projectDir,
|
|
15685
|
-
compiledDir,
|
|
15686
|
-
job,
|
|
15687
|
-
cfg,
|
|
15688
|
-
composition,
|
|
15689
|
-
abortSignal,
|
|
15690
|
-
assertNotAborted,
|
|
15691
|
-
materializeSymlinks: true,
|
|
15692
|
-
deferRangeExtraction: options.deferVideoExtraction,
|
|
15693
|
-
urlByAbsentLocalPath
|
|
15694
|
-
});
|
|
15934
|
+
let stageStarted = Date.now();
|
|
15935
|
+
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
15695
15936
|
logPlanStage(log, "extract", stageStarted);
|
|
15696
|
-
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
15697
|
-
if (extractResult.extractionResult) {
|
|
15698
|
-
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
15699
|
-
}
|
|
15700
15937
|
const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
|
|
15701
15938
|
const videoFramesDst = join34(planDir, "video-frames");
|
|
15702
15939
|
if (existsSync21(videoFramesDst)) rmSync11(videoFramesDst, { recursive: true, force: true });
|
|
@@ -15752,23 +15989,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15752
15989
|
"utf-8"
|
|
15753
15990
|
);
|
|
15754
15991
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
15755
|
-
const
|
|
15756
|
-
const
|
|
15992
|
+
const separateAudio = plansSeparateAudio(config, options);
|
|
15993
|
+
const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
|
|
15994
|
+
const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
|
|
15995
|
+
const planMixPcmPath = join34(workDir, "plan-mix.wav");
|
|
15757
15996
|
stageStarted = Date.now();
|
|
15758
15997
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
15766
|
-
audioGain: cfg.audioGain,
|
|
15767
|
-
audios: composition.audios,
|
|
15768
|
-
masterFxChain: composition.masterFxChain,
|
|
15769
|
-
abortSignal,
|
|
15770
|
-
assertNotAborted
|
|
15771
|
-
}).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
15998
|
+
separateAudio ? null : mixPlanAudio(
|
|
15999
|
+
c,
|
|
16000
|
+
finalCompiledDir,
|
|
16001
|
+
audioSeconds,
|
|
16002
|
+
chunksCarryAudio ? planMixPcmPath : void 0
|
|
16003
|
+
).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
15772
16004
|
(async () => {
|
|
15773
16005
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
15774
16006
|
await prehashPlanFiles(dir, digests);
|
|
@@ -15818,22 +16050,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15818
16050
|
);
|
|
15819
16051
|
const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
|
|
15820
16052
|
const audioResult = audioOutcome.value;
|
|
15821
|
-
if (audioResult
|
|
16053
|
+
if (audioResult?.audioError) {
|
|
15822
16054
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
15823
16055
|
}
|
|
15824
16056
|
const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
15825
|
-
if (audioResult
|
|
16057
|
+
if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
|
|
15826
16058
|
renameSync6(audioResult.audioOutputPath, planAudioPath);
|
|
15827
16059
|
}
|
|
15828
|
-
if (
|
|
16060
|
+
if (chunksCarryAudio && existsSync21(planAudioPath)) {
|
|
15829
16061
|
stageStarted = Date.now();
|
|
15830
|
-
|
|
15831
|
-
audioPath: planAudioPath,
|
|
15832
|
-
targetDurationSeconds: audioSeconds,
|
|
15833
|
-
signal: abortSignal,
|
|
15834
|
-
timeoutMs: cfg.ffmpegProcessTimeout
|
|
15835
|
-
});
|
|
15836
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
16062
|
+
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
15837
16063
|
logPlanStage(log, "audio-peak", stageStarted);
|
|
15838
16064
|
if (options.sliceChunkAudio) {
|
|
15839
16065
|
stageStarted = Date.now();
|
|
@@ -15859,7 +16085,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15859
16085
|
effectiveChunkSize,
|
|
15860
16086
|
chunkCount,
|
|
15861
16087
|
runtimeEnv,
|
|
15862
|
-
chunksCarryAudio
|
|
16088
|
+
chunksCarryAudio,
|
|
16089
|
+
separateAudio
|
|
15863
16090
|
});
|
|
15864
16091
|
const compositionJson = {
|
|
15865
16092
|
durationSeconds: job.duration ?? 0,
|
|
@@ -15904,7 +16131,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15904
16131
|
fontSnapshotSha,
|
|
15905
16132
|
durationSeconds: job.duration ?? 0,
|
|
15906
16133
|
totalFrames,
|
|
15907
|
-
hasAudio: audioResult
|
|
16134
|
+
hasAudio: audioResult?.hasAudio ?? false,
|
|
15908
16135
|
digests
|
|
15909
16136
|
});
|
|
15910
16137
|
logPlanStage(log, "freeze", stageStarted);
|
|
@@ -16420,8 +16647,8 @@ function renderRequestFromDistributedConfig(input) {
|
|
|
16420
16647
|
|
|
16421
16648
|
// src/services/frameCapture.ts
|
|
16422
16649
|
import {
|
|
16423
|
-
createCaptureSession as
|
|
16424
|
-
initializeSession as
|
|
16650
|
+
createCaptureSession as createCaptureSession10,
|
|
16651
|
+
initializeSession as initializeSession10,
|
|
16425
16652
|
closeCaptureSession as closeCaptureSession10,
|
|
16426
16653
|
captureFrame as captureFrame2,
|
|
16427
16654
|
captureFrameToBuffer as captureFrameToBuffer4,
|
|
@@ -17922,7 +18149,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
17922
18149
|
)
|
|
17923
18150
|
),
|
|
17924
18151
|
chunksCarryAudio: options.chunksCarryAudio,
|
|
17925
|
-
sliceChunkAudio: true
|
|
18152
|
+
sliceChunkAudio: true,
|
|
18153
|
+
deferAudio: options.deferAudio
|
|
17926
18154
|
});
|
|
17927
18155
|
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
17928
18156
|
} catch (error) {
|
|
@@ -18233,12 +18461,12 @@ import {
|
|
|
18233
18461
|
import { dirname as dirname17, join as join41 } from "node:path";
|
|
18234
18462
|
import {
|
|
18235
18463
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
18236
|
-
describeFfmpegFailure as
|
|
18464
|
+
describeFfmpegFailure as describeFfmpegFailure4,
|
|
18237
18465
|
getEncoderPreset as getEncoderPreset5,
|
|
18238
18466
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
18239
18467
|
muxVideoWithAudio as muxVideoWithAudio2,
|
|
18240
18468
|
resolveConfig as resolveConfig6,
|
|
18241
|
-
runFfmpeg as
|
|
18469
|
+
runFfmpeg as runFfmpeg7
|
|
18242
18470
|
} from "@hyperframes/engine";
|
|
18243
18471
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
18244
18472
|
function cfrRateControlArgs(locked) {
|
|
@@ -18258,7 +18486,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
18258
18486
|
return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
|
|
18259
18487
|
}
|
|
18260
18488
|
if (chunksAreStitchedMp4(planDir, cfr)) {
|
|
18261
|
-
|
|
18489
|
+
const separateAudio = stitchesSeparateAudio(planDir) && audioPath !== null;
|
|
18490
|
+
await writeStitchedMp4(chunkPaths, outputPath, separateAudio ? audioPath : void 0);
|
|
18262
18491
|
return {
|
|
18263
18492
|
outputPath,
|
|
18264
18493
|
durationMs: Date.now() - start,
|
|
@@ -18447,25 +18676,29 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
18447
18676
|
};
|
|
18448
18677
|
}
|
|
18449
18678
|
async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
18450
|
-
const result = await
|
|
18679
|
+
const result = await runFfmpeg7(args, { signal, timeout });
|
|
18451
18680
|
if (!result.success) {
|
|
18452
18681
|
throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
|
|
18453
|
-
error:
|
|
18682
|
+
error: describeFfmpegFailure4(result, timeout),
|
|
18454
18683
|
failureReason: result.failureReason
|
|
18455
18684
|
});
|
|
18456
18685
|
}
|
|
18457
18686
|
}
|
|
18458
18687
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
18459
|
-
|
|
18688
|
+
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
18689
|
+
if (!chunksCarryAudio && !separateAudio) return false;
|
|
18460
18690
|
if (cfr) {
|
|
18461
|
-
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks
|
|
18691
|
+
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
|
|
18462
18692
|
}
|
|
18463
18693
|
return true;
|
|
18464
18694
|
}
|
|
18465
|
-
function
|
|
18695
|
+
function stitchesSeparateAudio(planDir) {
|
|
18696
|
+
return lockedAudio(planDir).separateAudio === true;
|
|
18697
|
+
}
|
|
18698
|
+
function lockedAudio(planDir) {
|
|
18466
18699
|
const encoderJsonPath = join41(planDir, "meta", "encoder.json");
|
|
18467
|
-
if (!existsSync27(encoderJsonPath)) return
|
|
18468
|
-
return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"))
|
|
18700
|
+
if (!existsSync27(encoderJsonPath)) return {};
|
|
18701
|
+
return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
|
|
18469
18702
|
}
|
|
18470
18703
|
function readCfrEncoder(planDir, format) {
|
|
18471
18704
|
if (format !== "mp4") {
|
|
@@ -18539,11 +18772,12 @@ import {
|
|
|
18539
18772
|
calculateOptimalWorkers as calculateOptimalWorkers2,
|
|
18540
18773
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
18541
18774
|
closeCaptureSession as closeCaptureSession11,
|
|
18542
|
-
createCaptureSession as
|
|
18775
|
+
createCaptureSession as createCaptureSession11,
|
|
18543
18776
|
createFrameLookupTable as createFrameLookupTable5,
|
|
18544
18777
|
deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
|
|
18545
18778
|
getEncoderPreset as getEncoderPreset6,
|
|
18546
|
-
initializeSession as
|
|
18779
|
+
initializeSession as initializeSession11,
|
|
18780
|
+
measureVideoShownBoxes,
|
|
18547
18781
|
probeBeginFrameLiveness as probeBeginFrameLiveness2,
|
|
18548
18782
|
readWebGlVendorInfoFromCanvas,
|
|
18549
18783
|
resolveConfig as resolveConfig7
|
|
@@ -18572,9 +18806,9 @@ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
|
|
|
18572
18806
|
return { startFrame, endFrame };
|
|
18573
18807
|
}
|
|
18574
18808
|
var distributedCaptureSessionDependencies = {
|
|
18575
|
-
createCaptureSession:
|
|
18809
|
+
createCaptureSession: createCaptureSession11,
|
|
18576
18810
|
assertSwiftShader,
|
|
18577
|
-
initializeSession:
|
|
18811
|
+
initializeSession: initializeSession11,
|
|
18578
18812
|
closeCaptureSession: closeCaptureSession11,
|
|
18579
18813
|
readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
|
|
18580
18814
|
};
|
|
@@ -18765,34 +18999,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18765
18999
|
const framesDir = join42(workDir, "captured-frames");
|
|
18766
19000
|
mkdirSync18(framesDir, { recursive: true });
|
|
18767
19001
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
18768
|
-
const
|
|
18769
|
-
|
|
18770
|
-
|
|
18771
|
-
|
|
18772
|
-
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
18773
|
-
outputRoot: videoFramesDir,
|
|
18774
|
-
cfg,
|
|
18775
|
-
remoteSources
|
|
18776
|
-
}) : [];
|
|
19002
|
+
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
19003
|
+
const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
19004
|
+
const deferredVideos = planVideos?.extracted.filter((video) => video.deferredRange) ?? [];
|
|
19005
|
+
const shownFrames = planVideos ? shownDeferredFrameIndexes(planVideos, chunkFrameNumbers, chunkFps) : /* @__PURE__ */ new Map();
|
|
18777
19006
|
const remoteVideoIds = new Set(
|
|
18778
19007
|
planVideos?.extracted.filter(
|
|
18779
19008
|
(video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
|
|
18780
19009
|
).map((video) => video.videoId)
|
|
18781
19010
|
);
|
|
18782
|
-
const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
|
|
18783
|
-
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
18784
|
-
planVideos.videos,
|
|
18785
|
-
[
|
|
18786
|
-
...rebuildExtractedFramesFromPlanDir(
|
|
18787
|
-
planDir,
|
|
18788
|
-
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
18789
|
-
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
18790
|
-
),
|
|
18791
|
-
...deferredVideoFrames
|
|
18792
|
-
],
|
|
18793
|
-
resolveRenderFpsConfig(job.config.fps).value
|
|
18794
|
-
) : null;
|
|
18795
|
-
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
18796
19011
|
const fileServer = await createFileServer({
|
|
18797
19012
|
projectDir: compiledDir,
|
|
18798
19013
|
compiledDir,
|
|
@@ -18808,8 +19023,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18808
19023
|
videoCount: planVideos?.videos.length ?? 0,
|
|
18809
19024
|
html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
|
|
18810
19025
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
18811
|
-
skipReadinessVideoIds:
|
|
18812
|
-
|
|
19026
|
+
skipReadinessVideoIds: deferredVideos.filter(
|
|
19027
|
+
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
19028
|
+
).map((video) => video.videoId),
|
|
19029
|
+
videoMetadataHints: collectVideoMetadataHints(
|
|
19030
|
+
deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
|
|
19031
|
+
)
|
|
18813
19032
|
});
|
|
18814
19033
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
18815
19034
|
let session = null;
|
|
@@ -18822,6 +19041,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18822
19041
|
const capturePerfs = [];
|
|
18823
19042
|
const captureAttempts = [];
|
|
18824
19043
|
let forceScreenshotForChunk = encoder.forceScreenshot;
|
|
19044
|
+
let shownBoxes;
|
|
18825
19045
|
try {
|
|
18826
19046
|
if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
|
|
18827
19047
|
const bootStarted = Date.now();
|
|
@@ -18832,6 +19052,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18832
19052
|
cfg
|
|
18833
19053
|
);
|
|
18834
19054
|
sessionBootMs = Date.now() - bootStarted;
|
|
19055
|
+
shownBoxes = await measureVideoShownBoxes(
|
|
19056
|
+
session.page,
|
|
19057
|
+
[...shownFrames.keys()],
|
|
19058
|
+
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
19059
|
+
);
|
|
18835
19060
|
const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
|
|
18836
19061
|
const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
|
|
18837
19062
|
if (browserSelectedScreenshot) {
|
|
@@ -18873,6 +19098,29 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18873
19098
|
session = null;
|
|
18874
19099
|
}
|
|
18875
19100
|
}
|
|
19101
|
+
const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
|
|
19102
|
+
planDir,
|
|
19103
|
+
planVideos,
|
|
19104
|
+
frames: chunkFrameNumbers,
|
|
19105
|
+
fps: chunkFps,
|
|
19106
|
+
outputRoot: videoFramesDir,
|
|
19107
|
+
cfg,
|
|
19108
|
+
remoteSources,
|
|
19109
|
+
shownBoxes
|
|
19110
|
+
}) : [];
|
|
19111
|
+
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
19112
|
+
planVideos.videos,
|
|
19113
|
+
[
|
|
19114
|
+
...rebuildExtractedFramesFromPlanDir(
|
|
19115
|
+
planDir,
|
|
19116
|
+
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
19117
|
+
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
19118
|
+
),
|
|
19119
|
+
...deferredVideoFrames
|
|
19120
|
+
],
|
|
19121
|
+
resolveRenderFpsConfig(job.config.fps).value
|
|
19122
|
+
) : null;
|
|
19123
|
+
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
18876
19124
|
const captureStarted = Date.now();
|
|
18877
19125
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
18878
19126
|
forceScreenshot: forceScreenshotForChunk,
|
|
@@ -19063,6 +19311,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19063
19311
|
encodeStageMs,
|
|
19064
19312
|
workers: chunkWorkerCount,
|
|
19065
19313
|
captureMode,
|
|
19314
|
+
// Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
|
|
19315
|
+
videoShownBoxes: Object.fromEntries(shownBoxes ?? []),
|
|
19066
19316
|
sha256,
|
|
19067
19317
|
outputKind,
|
|
19068
19318
|
producerVersion: plan2.producerVersion,
|
|
@@ -19130,6 +19380,9 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
19130
19380
|
rmSync17(workRoot, { recursive: true, force: true });
|
|
19131
19381
|
}
|
|
19132
19382
|
}
|
|
19383
|
+
|
|
19384
|
+
// src/services/distributed/projectHash.ts
|
|
19385
|
+
var READ_CHUNK_BYTES = 256 * 1024;
|
|
19133
19386
|
export {
|
|
19134
19387
|
CURRENT_PLAN_PROTOCOL,
|
|
19135
19388
|
DEFAULT_CONFIG2 as DEFAULT_CONFIG,
|
|
@@ -19162,7 +19415,7 @@ export {
|
|
|
19162
19415
|
captureFrameToBuffer4 as captureFrameToBuffer,
|
|
19163
19416
|
closeCaptureSession10 as closeCaptureSession,
|
|
19164
19417
|
collectRenderMedia,
|
|
19165
|
-
|
|
19418
|
+
createCaptureSession10 as createCaptureSession,
|
|
19166
19419
|
createConsoleLogger,
|
|
19167
19420
|
createFileServer,
|
|
19168
19421
|
createOwnedRenderDir,
|
|
@@ -19180,7 +19433,7 @@ export {
|
|
|
19180
19433
|
getCompositionDuration2 as getCompositionDuration,
|
|
19181
19434
|
getDistributedRenderCapabilities,
|
|
19182
19435
|
getPlanV2ExecutionPlanHash,
|
|
19183
|
-
|
|
19436
|
+
initializeSession10 as initializeSession,
|
|
19184
19437
|
injectDeterministicFontFaces2 as injectDeterministicFontFaces,
|
|
19185
19438
|
isTransientBrowserError3 as isTransientBrowserError,
|
|
19186
19439
|
listAbandonedRenderDirs,
|