@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/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,
|
|
@@ -10374,7 +10426,8 @@ import { dirname as dirname9, join as join25 } from "node:path";
|
|
|
10374
10426
|
import {
|
|
10375
10427
|
DEFAULT_CONFIG,
|
|
10376
10428
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
10377
|
-
|
|
10429
|
+
encodeAac,
|
|
10430
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
10378
10431
|
getFfprobeBinary,
|
|
10379
10432
|
ManagedChildProcess,
|
|
10380
10433
|
runFfmpeg as runFfmpeg5,
|
|
@@ -10383,7 +10436,12 @@ import {
|
|
|
10383
10436
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
10384
10437
|
|
|
10385
10438
|
// src/services/render/audioTruePeak.ts
|
|
10386
|
-
import {
|
|
10439
|
+
import {
|
|
10440
|
+
aacSegmentCount,
|
|
10441
|
+
describeFfmpegFailure,
|
|
10442
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
10443
|
+
runFfmpeg as runFfmpeg4
|
|
10444
|
+
} from "@hyperframes/engine";
|
|
10387
10445
|
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
10388
10446
|
function checkedDbfs(value) {
|
|
10389
10447
|
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
@@ -10421,12 +10479,13 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
10421
10479
|
};
|
|
10422
10480
|
}
|
|
10423
10481
|
async function measureTruePeakDbfs(audioPath, options) {
|
|
10424
|
-
const { timeoutMs, signal
|
|
10482
|
+
const { timeoutMs, signal } = options;
|
|
10425
10483
|
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
10484
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
10426
10485
|
const block = 4800;
|
|
10427
|
-
const count =
|
|
10428
|
-
if (
|
|
10429
|
-
const cut = (k) => k * Math.floor(
|
|
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;
|
|
10430
10489
|
const peers = new AbortController();
|
|
10431
10490
|
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
10432
10491
|
const scans = await Promise.all(
|
|
@@ -10455,24 +10514,8 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
|
10455
10514
|
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
10456
10515
|
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
10457
10516
|
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
10458
|
-
function
|
|
10459
|
-
return
|
|
10460
|
-
"-i",
|
|
10461
|
-
inputPath,
|
|
10462
|
-
"-map",
|
|
10463
|
-
"0:a:0",
|
|
10464
|
-
"-vn",
|
|
10465
|
-
"-af",
|
|
10466
|
-
`volume=${attenuationDb.toFixed(3)}dB`,
|
|
10467
|
-
"-t",
|
|
10468
|
-
formatSeconds(targetDurationSeconds),
|
|
10469
|
-
"-c:a",
|
|
10470
|
-
"aac",
|
|
10471
|
-
"-b:a",
|
|
10472
|
-
"192k",
|
|
10473
|
-
"-y",
|
|
10474
|
-
outputPath
|
|
10475
|
-
];
|
|
10517
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
10518
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
10476
10519
|
}
|
|
10477
10520
|
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
10478
10521
|
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
@@ -10487,53 +10530,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
|
|
|
10487
10530
|
if (delta > 0) {
|
|
10488
10531
|
return {
|
|
10489
10532
|
operation: "pad",
|
|
10490
|
-
steps: [
|
|
10491
|
-
{
|
|
10492
|
-
kind: "normalize",
|
|
10493
|
-
args: [
|
|
10494
|
-
"-i",
|
|
10495
|
-
audioPath,
|
|
10496
|
-
"-af",
|
|
10497
|
-
`apad,atrim=0:${targetSec}`,
|
|
10498
|
-
"-t",
|
|
10499
|
-
targetSec,
|
|
10500
|
-
"-c:a",
|
|
10501
|
-
"aac",
|
|
10502
|
-
"-b:a",
|
|
10503
|
-
"192k",
|
|
10504
|
-
"-y",
|
|
10505
|
-
outputPath
|
|
10506
|
-
]
|
|
10507
|
-
}
|
|
10508
|
-
],
|
|
10533
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
10509
10534
|
cleanupPaths: []
|
|
10510
10535
|
};
|
|
10511
10536
|
}
|
|
10512
10537
|
return {
|
|
10513
10538
|
operation: "trim",
|
|
10514
|
-
steps: [
|
|
10515
|
-
{
|
|
10516
|
-
kind: "trim",
|
|
10517
|
-
args: [
|
|
10518
|
-
"-i",
|
|
10519
|
-
audioPath,
|
|
10520
|
-
"-af",
|
|
10521
|
-
`atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
|
|
10522
|
-
// `atrim` limits decoded samples but does not cap muxer timestamps
|
|
10523
|
-
// introduced by encoder delay/flush. The output duration contract
|
|
10524
|
-
// is enforced at the container boundary as well; without `-t`, a
|
|
10525
|
-
// long primed source can retain its original tail after re-encode.
|
|
10526
|
-
"-t",
|
|
10527
|
-
targetSec,
|
|
10528
|
-
"-c:a",
|
|
10529
|
-
"aac",
|
|
10530
|
-
"-b:a",
|
|
10531
|
-
"192k",
|
|
10532
|
-
"-y",
|
|
10533
|
-
outputPath
|
|
10534
|
-
]
|
|
10535
|
-
}
|
|
10536
|
-
],
|
|
10539
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
10537
10540
|
cleanupPaths: []
|
|
10538
10541
|
};
|
|
10539
10542
|
}
|
|
@@ -10550,7 +10553,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10550
10553
|
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
10551
10554
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
10552
10555
|
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
10553
|
-
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 }));
|
|
10554
10558
|
const [videoResult, audioResult] = await Promise.allSettled([
|
|
10555
10559
|
probeVideo(input.videoPath),
|
|
10556
10560
|
probeAudio(input.audioPath, input.signal)
|
|
@@ -10597,7 +10601,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10597
10601
|
);
|
|
10598
10602
|
try {
|
|
10599
10603
|
for (const step of plan2.steps) {
|
|
10600
|
-
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);
|
|
10601
10605
|
if (!ffmpegResult.success) {
|
|
10602
10606
|
return {
|
|
10603
10607
|
success: false,
|
|
@@ -10629,7 +10633,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
10629
10633
|
targetDurationSeconds,
|
|
10630
10634
|
signal: input.signal,
|
|
10631
10635
|
probeTruePeak,
|
|
10632
|
-
|
|
10636
|
+
encode
|
|
10633
10637
|
});
|
|
10634
10638
|
if (!correction.success) {
|
|
10635
10639
|
return {
|
|
@@ -10659,12 +10663,8 @@ async function constrainAacTruePeak(input) {
|
|
|
10659
10663
|
audioPath: input.audioPath,
|
|
10660
10664
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
10661
10665
|
signal: input.signal,
|
|
10662
|
-
probeTruePeak:
|
|
10663
|
-
|
|
10664
|
-
const segments = { durationSeconds: input.targetDurationSeconds, sampleRate };
|
|
10665
|
-
return measureTruePeakDbfs(audioPath, { timeoutMs, signal, segments });
|
|
10666
|
-
},
|
|
10667
|
-
runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
|
|
10666
|
+
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
10667
|
+
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
10668
10668
|
});
|
|
10669
10669
|
}
|
|
10670
10670
|
async function enforceAacTruePeak(input) {
|
|
@@ -10691,13 +10691,11 @@ async function enforceAacTruePeak(input) {
|
|
|
10691
10691
|
}
|
|
10692
10692
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
10693
10693
|
attenuationDb += correctionTargetDbfs - truePeakDbfs;
|
|
10694
|
-
const result = await input.
|
|
10695
|
-
|
|
10696
|
-
|
|
10697
|
-
|
|
10698
|
-
|
|
10699
|
-
attenuationDb
|
|
10700
|
-
)
|
|
10694
|
+
const result = await input.encode(
|
|
10695
|
+
input.audioPath,
|
|
10696
|
+
correctedPath,
|
|
10697
|
+
input.targetDurationSeconds,
|
|
10698
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
10701
10699
|
);
|
|
10702
10700
|
if (!result.success) {
|
|
10703
10701
|
return {
|
|
@@ -10792,7 +10790,7 @@ function parseFrameRate(rate) {
|
|
|
10792
10790
|
return { fpsNum, fpsDen };
|
|
10793
10791
|
}
|
|
10794
10792
|
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
10795
|
-
const metadata = await
|
|
10793
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
10796
10794
|
return {
|
|
10797
10795
|
durationSeconds: metadata.durationSeconds,
|
|
10798
10796
|
sampleRate: metadata.sampleRate,
|
|
@@ -10801,7 +10799,18 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
10801
10799
|
};
|
|
10802
10800
|
}
|
|
10803
10801
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
10804
|
-
|
|
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) {
|
|
10805
10814
|
if (result.success) return { success: true };
|
|
10806
10815
|
return {
|
|
10807
10816
|
success: false,
|
|
@@ -10989,7 +10998,7 @@ function assertOutputScaleSupported(deviceScaleFactor, route) {
|
|
|
10989
10998
|
const reason = UNSUPPORTED_REASON2[route];
|
|
10990
10999
|
if (reason === null) return;
|
|
10991
11000
|
throw new Error(
|
|
10992
|
-
`[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.`
|
|
10993
11002
|
);
|
|
10994
11003
|
}
|
|
10995
11004
|
|
|
@@ -11206,7 +11215,7 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11206
11215
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11207
11216
|
}
|
|
11208
11217
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11209
|
-
if (platform2 === "win32") return join26(systemTempDir, "hf-render-");
|
|
11218
|
+
if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
|
|
11210
11219
|
return join26(dirname10(outputPath), `work-${jobId}-`);
|
|
11211
11220
|
}
|
|
11212
11221
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
@@ -11700,6 +11709,9 @@ function resolveDeShortBand(args) {
|
|
|
11700
11709
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
11701
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;
|
|
11702
11711
|
}
|
|
11712
|
+
function isProbeDeGated(session) {
|
|
11713
|
+
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
11714
|
+
}
|
|
11703
11715
|
function resolveInversionRetryPlan(args) {
|
|
11704
11716
|
if (args.deWorkerInversion !== "inverted") return null;
|
|
11705
11717
|
const workerCount = args.isMemoryExhaustion ? 1 : args.preInversionWorkerCount;
|
|
@@ -11781,7 +11793,8 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
|
|
|
11781
11793
|
}
|
|
11782
11794
|
function resolveParallelCaptureMode(args) {
|
|
11783
11795
|
const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
|
|
11784
|
-
|
|
11796
|
+
const headlessShell = args.headlessShellPath !== void 0;
|
|
11797
|
+
return headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
|
|
11785
11798
|
}
|
|
11786
11799
|
function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
|
|
11787
11800
|
const raw = env.HF_CAPTURE_PARALLEL_STREAM?.trim().toLowerCase();
|
|
@@ -12389,6 +12402,7 @@ async function executeRenderPipeline(input) {
|
|
|
12389
12402
|
);
|
|
12390
12403
|
const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
|
|
12391
12404
|
const hasHdrContent = Boolean(effectiveHdr && nativeHdrIds.size > 0);
|
|
12405
|
+
if (hasHdrContent) assertOutputScaleSupported(deviceScaleFactor, "hdr_layered");
|
|
12392
12406
|
const deSingleMinFramesRaw = process.env.HF_DE_SINGLE_MIN_FRAMES;
|
|
12393
12407
|
const deSingleMinFramesNum = deSingleMinFramesRaw === void 0 || deSingleMinFramesRaw.trim() === "" ? 900 : Number(deSingleMinFramesRaw);
|
|
12394
12408
|
const deSingleMinFrames = Number.isFinite(deSingleMinFramesNum) ? deSingleMinFramesNum : 900;
|
|
@@ -12429,7 +12443,7 @@ async function executeRenderPipeline(input) {
|
|
|
12429
12443
|
singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
|
|
12430
12444
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12431
12445
|
supersampling: deviceScaleFactor !== 1,
|
|
12432
|
-
probeDeGated: probeSession
|
|
12446
|
+
probeDeGated: isProbeDeGated(probeSession),
|
|
12433
12447
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
|
|
12434
12448
|
process.env.HF_DE_PARALLEL_STREAM === "true"
|
|
12435
12449
|
};
|
|
@@ -12438,14 +12452,14 @@ async function executeRenderPipeline(input) {
|
|
|
12438
12452
|
...deInversionArgs,
|
|
12439
12453
|
minFrames: Math.min(deSingleMinFrames, deShortBandMinFrames)
|
|
12440
12454
|
});
|
|
12441
|
-
|
|
12455
|
+
let deShortBand = resolveDeShortBand({
|
|
12442
12456
|
invertAtBaseFloor,
|
|
12443
12457
|
invertAtBandFloor,
|
|
12444
12458
|
bandEnabled: deShortBandEnabled,
|
|
12445
12459
|
bandOpen: deShortBandOpen,
|
|
12446
12460
|
elementCountSource: compositionElementCountSource
|
|
12447
12461
|
});
|
|
12448
|
-
|
|
12462
|
+
let deInversionEligible = deShortBandRoute && deShortBand === "applied" ? invertAtBandFloor : invertAtBaseFloor;
|
|
12449
12463
|
const deInversionEffectiveMinFrames = deShortBandRoute && deShortBand === "applied" ? Math.min(deSingleMinFrames, deShortBandMinFrames) : deSingleMinFrames;
|
|
12450
12464
|
const deParallelRouterEnabled = isDeParallelRouterEnabled(process.env);
|
|
12451
12465
|
const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
|
|
@@ -12454,7 +12468,7 @@ async function executeRenderPipeline(input) {
|
|
|
12454
12468
|
const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
|
|
12455
12469
|
const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
|
|
12456
12470
|
const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
|
|
12457
|
-
|
|
12471
|
+
let deParallelRouterEligible = shouldPreferParallelDrawElement({
|
|
12458
12472
|
workerCount: WOULD_RESOLVE_MULTI_WORKER,
|
|
12459
12473
|
requestedWorkers: job.config.workers,
|
|
12460
12474
|
useDrawElement: cfg.useDrawElement,
|
|
@@ -12465,7 +12479,7 @@ async function executeRenderPipeline(input) {
|
|
|
12465
12479
|
minFrames: deParallelMinFrames,
|
|
12466
12480
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12467
12481
|
supersampling: deviceScaleFactor !== 1,
|
|
12468
|
-
probeDeGated: probeSession
|
|
12482
|
+
probeDeGated: isProbeDeGated(probeSession),
|
|
12469
12483
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12470
12484
|
routerEnabled: deParallelRouterEnabled,
|
|
12471
12485
|
// Router pins 3 workers for the streaming path; don't pin when a
|
|
@@ -12481,6 +12495,35 @@ async function executeRenderPipeline(input) {
|
|
|
12481
12495
|
totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
|
|
12482
12496
|
minMemoryMb: deParallelMinMemoryMb
|
|
12483
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;
|
|
12484
12527
|
let workerCount;
|
|
12485
12528
|
const captureStageObservationData = (extra = {}) => ({
|
|
12486
12529
|
stagePhase: "capturing",
|
|
@@ -12668,7 +12711,7 @@ async function executeRenderPipeline(input) {
|
|
|
12668
12711
|
}
|
|
12669
12712
|
const parallelCaptureMode = resolveParallelCaptureMode({
|
|
12670
12713
|
platform: process.platform,
|
|
12671
|
-
|
|
12714
|
+
headlessShellPath: resolveHeadlessShellPath(cfg),
|
|
12672
12715
|
forceScreenshot: captureForceScreenshot,
|
|
12673
12716
|
deviceScaleFactor: captureOptions.deviceScaleFactor
|
|
12674
12717
|
});
|
|
@@ -13440,7 +13483,9 @@ async function executeRenderPipeline(input) {
|
|
|
13440
13483
|
outputFormat,
|
|
13441
13484
|
durationSeconds: job.duration,
|
|
13442
13485
|
fps: fpsToNumber9(job.config.fps),
|
|
13443
|
-
expectedFrames: captureTotalFrames
|
|
13486
|
+
expectedFrames: captureTotalFrames,
|
|
13487
|
+
width: outputWidth,
|
|
13488
|
+
height: outputHeight
|
|
13444
13489
|
})
|
|
13445
13490
|
);
|
|
13446
13491
|
const totalElapsed = Date.now() - pipelineStart;
|
|
@@ -13476,6 +13521,7 @@ async function executeRenderPipeline(input) {
|
|
|
13476
13521
|
drawElement: {
|
|
13477
13522
|
compileGate: deCompileGate,
|
|
13478
13523
|
clampReason: deClampReason,
|
|
13524
|
+
probeGateReason: deProbeGateReason,
|
|
13479
13525
|
workerInversion: deWorkerInversion,
|
|
13480
13526
|
preInversionWorkers: deWorkerInversion ? preRoutingWorkerCount : void 0,
|
|
13481
13527
|
compositionElementCount,
|
|
@@ -13673,9 +13719,9 @@ import {
|
|
|
13673
13719
|
import { join as join29 } from "node:path";
|
|
13674
13720
|
import {
|
|
13675
13721
|
closeCaptureSession as closeCaptureSession9,
|
|
13676
|
-
createCaptureSession as
|
|
13722
|
+
createCaptureSession as createCaptureSession9,
|
|
13677
13723
|
createFrameLookupTable as createFrameLookupTable3,
|
|
13678
|
-
initializeSession as
|
|
13724
|
+
initializeSession as initializeSession9
|
|
13679
13725
|
} from "@hyperframes/engine";
|
|
13680
13726
|
|
|
13681
13727
|
// src/services/distributed/chunkFrames.ts
|
|
@@ -14073,6 +14119,29 @@ function buildChunkCaptureOptions(input) {
|
|
|
14073
14119
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
14074
14120
|
};
|
|
14075
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
|
+
}
|
|
14076
14145
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
14077
14146
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
14078
14147
|
const remoteSources = input.remoteSources ?? {};
|
|
@@ -14089,24 +14158,7 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14089
14158
|
});
|
|
14090
14159
|
}
|
|
14091
14160
|
if (deferred.length === 0) return [];
|
|
14092
|
-
const
|
|
14093
|
-
input.planVideos.videos,
|
|
14094
|
-
deferred.map((video) => ({
|
|
14095
|
-
...video,
|
|
14096
|
-
outputDir: input.outputRoot,
|
|
14097
|
-
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
14098
|
-
})),
|
|
14099
|
-
resolveRenderFpsConfig(input.fps).value
|
|
14100
|
-
);
|
|
14101
|
-
const shown = /* @__PURE__ */ new Map();
|
|
14102
|
-
for (const frame of input.frames) {
|
|
14103
|
-
const globalTime = frame * input.fps.den / input.fps.num;
|
|
14104
|
-
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
14105
|
-
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
14106
|
-
indexes.add(payload.frameIndex);
|
|
14107
|
-
shown.set(videoId, indexes);
|
|
14108
|
-
}
|
|
14109
|
-
}
|
|
14161
|
+
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
14110
14162
|
return Promise.all(
|
|
14111
14163
|
deferred.map(async (video) => {
|
|
14112
14164
|
const outputDir = join28(input.outputRoot, video.videoId);
|
|
@@ -14132,7 +14184,8 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14132
14184
|
fps: input.fps,
|
|
14133
14185
|
outputDir: input.outputRoot,
|
|
14134
14186
|
format: range.format,
|
|
14135
|
-
frameRanges: contiguousRuns(indexes)
|
|
14187
|
+
frameRanges: contiguousRuns(indexes),
|
|
14188
|
+
shownBox: input.shownBoxes?.get(video.videoId)
|
|
14136
14189
|
},
|
|
14137
14190
|
void 0,
|
|
14138
14191
|
input.cfg
|
|
@@ -14179,7 +14232,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14179
14232
|
});
|
|
14180
14233
|
let session = null;
|
|
14181
14234
|
try {
|
|
14182
|
-
session = await
|
|
14235
|
+
session = await createCaptureSession9(
|
|
14183
14236
|
fileServer.url,
|
|
14184
14237
|
join29(workDir, "frame-cost"),
|
|
14185
14238
|
buildChunkCaptureOptions({
|
|
@@ -14191,7 +14244,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14191
14244
|
createChunkVideoFrameInjectorFactory(lookup)(),
|
|
14192
14245
|
createCaptureCalibrationConfig(input.cfg)
|
|
14193
14246
|
);
|
|
14194
|
-
await
|
|
14247
|
+
await initializeSession9(session);
|
|
14195
14248
|
const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
|
|
14196
14249
|
return (estimate.p95Ms ?? 0) / 1e3;
|
|
14197
14250
|
} finally {
|
|
@@ -14785,6 +14838,20 @@ function withDuration(buf, target, duration) {
|
|
|
14785
14838
|
function timescaleOf(buf, header) {
|
|
14786
14839
|
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
14787
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
|
+
}
|
|
14788
14855
|
function buildStbl(stsd, track) {
|
|
14789
14856
|
const stts = runs(track.durations);
|
|
14790
14857
|
const stsc = [];
|
|
@@ -14826,24 +14893,48 @@ function planTracksOf(tracks, plan2, index) {
|
|
|
14826
14893
|
}
|
|
14827
14894
|
return matched;
|
|
14828
14895
|
}
|
|
14829
|
-
function
|
|
14830
|
-
const
|
|
14831
|
-
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;
|
|
14832
14919
|
const payloadBases = [];
|
|
14833
14920
|
let payloadBytes = 0;
|
|
14834
|
-
for (const layout of
|
|
14921
|
+
for (const layout of sourceLayouts) {
|
|
14835
14922
|
payloadBases.push(payloadBytes);
|
|
14836
14923
|
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
14837
14924
|
}
|
|
14838
14925
|
const template = layouts[0].moov;
|
|
14839
14926
|
const root = boxAt(template, 0, 0, template.length);
|
|
14840
|
-
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;
|
|
14841
14931
|
const buildMoov = (dataStart2) => {
|
|
14842
14932
|
const traks = plan2.map((track, t) => {
|
|
14843
|
-
const parts =
|
|
14933
|
+
const parts = sources.flatMap((tracks, chunk) => {
|
|
14844
14934
|
const part = tracks[t];
|
|
14845
14935
|
return part ? [{ ...part, chunk }] : [];
|
|
14846
14936
|
});
|
|
14937
|
+
const moov = t < chunkTrackCount ? template : audio.moov;
|
|
14847
14938
|
const merged = {
|
|
14848
14939
|
durations: parts.flatMap((part) => part.durations),
|
|
14849
14940
|
sizes: parts.flatMap((part) => part.sizes),
|
|
@@ -14852,9 +14943,9 @@ function buildStitchedHeader(layouts) {
|
|
|
14852
14943
|
chunkOffsets: parts.flatMap(
|
|
14853
14944
|
(part) => part.chunkOffsets.map((offset) => {
|
|
14854
14945
|
const i = part.chunk;
|
|
14855
|
-
const { payloadStart, payloadEnd } =
|
|
14946
|
+
const { payloadStart, payloadEnd } = sourceLayouts[i];
|
|
14856
14947
|
if (offset < payloadStart || offset >= payloadEnd) {
|
|
14857
|
-
throw new Error(`[mp4Stitch]
|
|
14948
|
+
throw new Error(`[mp4Stitch] source ${i} has samples outside its mdat`);
|
|
14858
14949
|
}
|
|
14859
14950
|
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
14860
14951
|
})
|
|
@@ -14870,20 +14961,25 @@ function buildStitchedHeader(layouts) {
|
|
|
14870
14961
|
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
14871
14962
|
elst.writeUInt32BE(65536, 20);
|
|
14872
14963
|
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
14873
|
-
const
|
|
14874
|
-
|
|
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]),
|
|
14875
14970
|
edts: () => Buffer.alloc(0),
|
|
14876
|
-
mdia: (mdia) => rebuild(
|
|
14877
|
-
mdhd: (mdhd) => withDuration(
|
|
14878
|
-
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) })
|
|
14879
14974
|
})
|
|
14880
14975
|
});
|
|
14881
14976
|
return { trak, shown };
|
|
14882
14977
|
});
|
|
14883
14978
|
let next = 0;
|
|
14979
|
+
const movieHeader = withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown)));
|
|
14884
14980
|
return rebuild(template, root, {
|
|
14885
|
-
mvhd: (
|
|
14886
|
-
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))
|
|
14887
14983
|
});
|
|
14888
14984
|
};
|
|
14889
14985
|
const ftyp = layouts[0].ftyp;
|
|
@@ -14916,15 +15012,16 @@ async function copyPayload(path, layout, index, out) {
|
|
|
14916
15012
|
);
|
|
14917
15013
|
}
|
|
14918
15014
|
}
|
|
14919
|
-
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
14920
|
-
const
|
|
14921
|
-
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);
|
|
14922
15019
|
mkdirSync11(dirname12(outputPath), { recursive: true });
|
|
14923
15020
|
const workDir = mkdtempSync4(`${outputPath}.stitch-`);
|
|
14924
15021
|
const out = createWriteStream(join31(workDir, "out.mp4"));
|
|
14925
15022
|
try {
|
|
14926
15023
|
out.write(header);
|
|
14927
|
-
for (const [i, path] of
|
|
15024
|
+
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
14928
15025
|
out.end();
|
|
14929
15026
|
await finished(out);
|
|
14930
15027
|
renameSync5(join31(workDir, "out.mp4"), outputPath);
|
|
@@ -15495,7 +15592,8 @@ function buildLockedRenderConfig(input) {
|
|
|
15495
15592
|
chunkCount: input.chunkCount,
|
|
15496
15593
|
runtimeEnv: input.runtimeEnv,
|
|
15497
15594
|
variables: config.variables,
|
|
15498
|
-
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
|
|
15595
|
+
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
15596
|
+
...input.separateAudio ? { separateAudio: true } : {}
|
|
15499
15597
|
};
|
|
15500
15598
|
}
|
|
15501
15599
|
function carriesChunkAudio(config, options) {
|
|
@@ -15590,8 +15688,10 @@ function createVideoSourceShipper(input) {
|
|
|
15590
15688
|
return result;
|
|
15591
15689
|
};
|
|
15592
15690
|
}
|
|
15593
|
-
|
|
15594
|
-
|
|
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) {
|
|
15595
15695
|
rejectUnsupportedDistributedFormat(config);
|
|
15596
15696
|
validateNoGpuEncode({
|
|
15597
15697
|
useGpu: false,
|
|
@@ -15727,6 +15827,100 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15727
15827
|
});
|
|
15728
15828
|
}
|
|
15729
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;
|
|
15730
15924
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
15731
15925
|
let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
|
|
15732
15926
|
const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
|
|
@@ -15737,24 +15931,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15737
15931
|
observedAt: "pre-extract",
|
|
15738
15932
|
log
|
|
15739
15933
|
});
|
|
15740
|
-
stageStarted = Date.now();
|
|
15741
|
-
const extractResult = await
|
|
15742
|
-
projectDir,
|
|
15743
|
-
compiledDir,
|
|
15744
|
-
job,
|
|
15745
|
-
cfg,
|
|
15746
|
-
composition,
|
|
15747
|
-
abortSignal,
|
|
15748
|
-
assertNotAborted,
|
|
15749
|
-
materializeSymlinks: true,
|
|
15750
|
-
deferRangeExtraction: options.deferVideoExtraction,
|
|
15751
|
-
urlByAbsentLocalPath
|
|
15752
|
-
});
|
|
15934
|
+
let stageStarted = Date.now();
|
|
15935
|
+
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
15753
15936
|
logPlanStage(log, "extract", stageStarted);
|
|
15754
|
-
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
15755
|
-
if (extractResult.extractionResult) {
|
|
15756
|
-
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
15757
|
-
}
|
|
15758
15937
|
const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
|
|
15759
15938
|
const videoFramesDst = join34(planDir, "video-frames");
|
|
15760
15939
|
if (existsSync21(videoFramesDst)) rmSync11(videoFramesDst, { recursive: true, force: true });
|
|
@@ -15810,23 +15989,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15810
15989
|
"utf-8"
|
|
15811
15990
|
);
|
|
15812
15991
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
15813
|
-
const
|
|
15814
|
-
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");
|
|
15815
15996
|
stageStarted = Date.now();
|
|
15816
15997
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
15817
|
-
|
|
15818
|
-
|
|
15819
|
-
|
|
15820
|
-
|
|
15821
|
-
|
|
15822
|
-
|
|
15823
|
-
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
15824
|
-
audioGain: cfg.audioGain,
|
|
15825
|
-
audios: composition.audios,
|
|
15826
|
-
masterFxChain: composition.masterFxChain,
|
|
15827
|
-
abortSignal,
|
|
15828
|
-
assertNotAborted
|
|
15829
|
-
}).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)),
|
|
15830
16004
|
(async () => {
|
|
15831
16005
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
15832
16006
|
await prehashPlanFiles(dir, digests);
|
|
@@ -15876,22 +16050,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15876
16050
|
);
|
|
15877
16051
|
const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
|
|
15878
16052
|
const audioResult = audioOutcome.value;
|
|
15879
|
-
if (audioResult
|
|
16053
|
+
if (audioResult?.audioError) {
|
|
15880
16054
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
15881
16055
|
}
|
|
15882
16056
|
const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
15883
|
-
if (audioResult
|
|
16057
|
+
if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
|
|
15884
16058
|
renameSync6(audioResult.audioOutputPath, planAudioPath);
|
|
15885
16059
|
}
|
|
15886
|
-
if (
|
|
16060
|
+
if (chunksCarryAudio && existsSync21(planAudioPath)) {
|
|
15887
16061
|
stageStarted = Date.now();
|
|
15888
|
-
|
|
15889
|
-
audioPath: planAudioPath,
|
|
15890
|
-
targetDurationSeconds: audioSeconds,
|
|
15891
|
-
signal: abortSignal,
|
|
15892
|
-
timeoutMs: cfg.ffmpegProcessTimeout
|
|
15893
|
-
});
|
|
15894
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
16062
|
+
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
15895
16063
|
logPlanStage(log, "audio-peak", stageStarted);
|
|
15896
16064
|
if (options.sliceChunkAudio) {
|
|
15897
16065
|
stageStarted = Date.now();
|
|
@@ -15917,7 +16085,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15917
16085
|
effectiveChunkSize,
|
|
15918
16086
|
chunkCount,
|
|
15919
16087
|
runtimeEnv,
|
|
15920
|
-
chunksCarryAudio
|
|
16088
|
+
chunksCarryAudio,
|
|
16089
|
+
separateAudio
|
|
15921
16090
|
});
|
|
15922
16091
|
const compositionJson = {
|
|
15923
16092
|
durationSeconds: job.duration ?? 0,
|
|
@@ -15962,7 +16131,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
15962
16131
|
fontSnapshotSha,
|
|
15963
16132
|
durationSeconds: job.duration ?? 0,
|
|
15964
16133
|
totalFrames,
|
|
15965
|
-
hasAudio: audioResult
|
|
16134
|
+
hasAudio: audioResult?.hasAudio ?? false,
|
|
15966
16135
|
digests
|
|
15967
16136
|
});
|
|
15968
16137
|
logPlanStage(log, "freeze", stageStarted);
|
|
@@ -16478,8 +16647,8 @@ function renderRequestFromDistributedConfig(input) {
|
|
|
16478
16647
|
|
|
16479
16648
|
// src/services/frameCapture.ts
|
|
16480
16649
|
import {
|
|
16481
|
-
createCaptureSession as
|
|
16482
|
-
initializeSession as
|
|
16650
|
+
createCaptureSession as createCaptureSession10,
|
|
16651
|
+
initializeSession as initializeSession10,
|
|
16483
16652
|
closeCaptureSession as closeCaptureSession10,
|
|
16484
16653
|
captureFrame as captureFrame2,
|
|
16485
16654
|
captureFrameToBuffer as captureFrameToBuffer4,
|
|
@@ -17980,7 +18149,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
17980
18149
|
)
|
|
17981
18150
|
),
|
|
17982
18151
|
chunksCarryAudio: options.chunksCarryAudio,
|
|
17983
|
-
sliceChunkAudio: true
|
|
18152
|
+
sliceChunkAudio: true,
|
|
18153
|
+
deferAudio: options.deferAudio
|
|
17984
18154
|
});
|
|
17985
18155
|
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
17986
18156
|
} catch (error) {
|
|
@@ -18316,7 +18486,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
18316
18486
|
return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
|
|
18317
18487
|
}
|
|
18318
18488
|
if (chunksAreStitchedMp4(planDir, cfr)) {
|
|
18319
|
-
|
|
18489
|
+
const separateAudio = stitchesSeparateAudio(planDir) && audioPath !== null;
|
|
18490
|
+
await writeStitchedMp4(chunkPaths, outputPath, separateAudio ? audioPath : void 0);
|
|
18320
18491
|
return {
|
|
18321
18492
|
outputPath,
|
|
18322
18493
|
durationMs: Date.now() - start,
|
|
@@ -18514,16 +18685,20 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
18514
18685
|
}
|
|
18515
18686
|
}
|
|
18516
18687
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
18517
|
-
|
|
18688
|
+
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
18689
|
+
if (!chunksCarryAudio && !separateAudio) return false;
|
|
18518
18690
|
if (cfr) {
|
|
18519
|
-
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");
|
|
18520
18692
|
}
|
|
18521
18693
|
return true;
|
|
18522
18694
|
}
|
|
18523
|
-
function
|
|
18695
|
+
function stitchesSeparateAudio(planDir) {
|
|
18696
|
+
return lockedAudio(planDir).separateAudio === true;
|
|
18697
|
+
}
|
|
18698
|
+
function lockedAudio(planDir) {
|
|
18524
18699
|
const encoderJsonPath = join41(planDir, "meta", "encoder.json");
|
|
18525
|
-
if (!existsSync27(encoderJsonPath)) return
|
|
18526
|
-
return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"))
|
|
18700
|
+
if (!existsSync27(encoderJsonPath)) return {};
|
|
18701
|
+
return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
|
|
18527
18702
|
}
|
|
18528
18703
|
function readCfrEncoder(planDir, format) {
|
|
18529
18704
|
if (format !== "mp4") {
|
|
@@ -18597,11 +18772,12 @@ import {
|
|
|
18597
18772
|
calculateOptimalWorkers as calculateOptimalWorkers2,
|
|
18598
18773
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
18599
18774
|
closeCaptureSession as closeCaptureSession11,
|
|
18600
|
-
createCaptureSession as
|
|
18775
|
+
createCaptureSession as createCaptureSession11,
|
|
18601
18776
|
createFrameLookupTable as createFrameLookupTable5,
|
|
18602
18777
|
deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
|
|
18603
18778
|
getEncoderPreset as getEncoderPreset6,
|
|
18604
|
-
initializeSession as
|
|
18779
|
+
initializeSession as initializeSession11,
|
|
18780
|
+
measureVideoShownBoxes,
|
|
18605
18781
|
probeBeginFrameLiveness as probeBeginFrameLiveness2,
|
|
18606
18782
|
readWebGlVendorInfoFromCanvas,
|
|
18607
18783
|
resolveConfig as resolveConfig7
|
|
@@ -18630,9 +18806,9 @@ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
|
|
|
18630
18806
|
return { startFrame, endFrame };
|
|
18631
18807
|
}
|
|
18632
18808
|
var distributedCaptureSessionDependencies = {
|
|
18633
|
-
createCaptureSession:
|
|
18809
|
+
createCaptureSession: createCaptureSession11,
|
|
18634
18810
|
assertSwiftShader,
|
|
18635
|
-
initializeSession:
|
|
18811
|
+
initializeSession: initializeSession11,
|
|
18636
18812
|
closeCaptureSession: closeCaptureSession11,
|
|
18637
18813
|
readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
|
|
18638
18814
|
};
|
|
@@ -18823,34 +18999,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18823
18999
|
const framesDir = join42(workDir, "captured-frames");
|
|
18824
19000
|
mkdirSync18(framesDir, { recursive: true });
|
|
18825
19001
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
18826
|
-
const
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
-
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
18831
|
-
outputRoot: videoFramesDir,
|
|
18832
|
-
cfg,
|
|
18833
|
-
remoteSources
|
|
18834
|
-
}) : [];
|
|
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();
|
|
18835
19006
|
const remoteVideoIds = new Set(
|
|
18836
19007
|
planVideos?.extracted.filter(
|
|
18837
19008
|
(video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
|
|
18838
19009
|
).map((video) => video.videoId)
|
|
18839
19010
|
);
|
|
18840
|
-
const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
|
|
18841
|
-
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
18842
|
-
planVideos.videos,
|
|
18843
|
-
[
|
|
18844
|
-
...rebuildExtractedFramesFromPlanDir(
|
|
18845
|
-
planDir,
|
|
18846
|
-
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
18847
|
-
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
18848
|
-
),
|
|
18849
|
-
...deferredVideoFrames
|
|
18850
|
-
],
|
|
18851
|
-
resolveRenderFpsConfig(job.config.fps).value
|
|
18852
|
-
) : null;
|
|
18853
|
-
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
18854
19011
|
const fileServer = await createFileServer({
|
|
18855
19012
|
projectDir: compiledDir,
|
|
18856
19013
|
compiledDir,
|
|
@@ -18866,8 +19023,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18866
19023
|
videoCount: planVideos?.videos.length ?? 0,
|
|
18867
19024
|
html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
|
|
18868
19025
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
18869
|
-
skipReadinessVideoIds:
|
|
18870
|
-
|
|
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
|
+
)
|
|
18871
19032
|
});
|
|
18872
19033
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
18873
19034
|
let session = null;
|
|
@@ -18880,6 +19041,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18880
19041
|
const capturePerfs = [];
|
|
18881
19042
|
const captureAttempts = [];
|
|
18882
19043
|
let forceScreenshotForChunk = encoder.forceScreenshot;
|
|
19044
|
+
let shownBoxes;
|
|
18883
19045
|
try {
|
|
18884
19046
|
if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
|
|
18885
19047
|
const bootStarted = Date.now();
|
|
@@ -18890,6 +19052,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18890
19052
|
cfg
|
|
18891
19053
|
);
|
|
18892
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
|
+
);
|
|
18893
19060
|
const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
|
|
18894
19061
|
const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
|
|
18895
19062
|
if (browserSelectedScreenshot) {
|
|
@@ -18931,6 +19098,29 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
18931
19098
|
session = null;
|
|
18932
19099
|
}
|
|
18933
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);
|
|
18934
19124
|
const captureStarted = Date.now();
|
|
18935
19125
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
18936
19126
|
forceScreenshot: forceScreenshotForChunk,
|
|
@@ -19121,6 +19311,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19121
19311
|
encodeStageMs,
|
|
19122
19312
|
workers: chunkWorkerCount,
|
|
19123
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 ?? []),
|
|
19124
19316
|
sha256,
|
|
19125
19317
|
outputKind,
|
|
19126
19318
|
producerVersion: plan2.producerVersion,
|
|
@@ -19188,6 +19380,9 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
19188
19380
|
rmSync17(workRoot, { recursive: true, force: true });
|
|
19189
19381
|
}
|
|
19190
19382
|
}
|
|
19383
|
+
|
|
19384
|
+
// src/services/distributed/projectHash.ts
|
|
19385
|
+
var READ_CHUNK_BYTES = 256 * 1024;
|
|
19191
19386
|
export {
|
|
19192
19387
|
CURRENT_PLAN_PROTOCOL,
|
|
19193
19388
|
DEFAULT_CONFIG2 as DEFAULT_CONFIG,
|
|
@@ -19220,7 +19415,7 @@ export {
|
|
|
19220
19415
|
captureFrameToBuffer4 as captureFrameToBuffer,
|
|
19221
19416
|
closeCaptureSession10 as closeCaptureSession,
|
|
19222
19417
|
collectRenderMedia,
|
|
19223
|
-
|
|
19418
|
+
createCaptureSession10 as createCaptureSession,
|
|
19224
19419
|
createConsoleLogger,
|
|
19225
19420
|
createFileServer,
|
|
19226
19421
|
createOwnedRenderDir,
|
|
@@ -19238,7 +19433,7 @@ export {
|
|
|
19238
19433
|
getCompositionDuration2 as getCompositionDuration,
|
|
19239
19434
|
getDistributedRenderCapabilities,
|
|
19240
19435
|
getPlanV2ExecutionPlanHash,
|
|
19241
|
-
|
|
19436
|
+
initializeSession10 as initializeSession,
|
|
19242
19437
|
injectDeterministicFontFaces2 as injectDeterministicFontFaces,
|
|
19243
19438
|
isTransientBrowserError3 as isTransientBrowserError,
|
|
19244
19439
|
listAbandonedRenderDirs,
|