@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/distributed.js
CHANGED
|
@@ -89,6 +89,8 @@ import {
|
|
|
89
89
|
extractMediaMetadata as extractMediaMetadata4,
|
|
90
90
|
resolveConfig as resolveConfig2,
|
|
91
91
|
closeCaptureSession as closeCaptureSession8,
|
|
92
|
+
createCaptureSession as createCaptureSession8,
|
|
93
|
+
initializeSession as initializeSession8,
|
|
92
94
|
createVideoFrameInjector,
|
|
93
95
|
getEncoderPreset as getEncoderPreset3,
|
|
94
96
|
distributeFrames as distributeFrames2,
|
|
@@ -4955,6 +4957,7 @@ async function runAudioStage(input) {
|
|
|
4955
4957
|
audios,
|
|
4956
4958
|
masterFxChain,
|
|
4957
4959
|
urlByAbsentLocalPath,
|
|
4960
|
+
keepEncoderInputAt,
|
|
4958
4961
|
abortSignal,
|
|
4959
4962
|
assertNotAborted,
|
|
4960
4963
|
log
|
|
@@ -4974,7 +4977,13 @@ async function runAudioStage(input) {
|
|
|
4974
4977
|
audioOutputPath,
|
|
4975
4978
|
duration,
|
|
4976
4979
|
abortSignal,
|
|
4977
|
-
{
|
|
4980
|
+
{
|
|
4981
|
+
ffmpegProcessTimeout,
|
|
4982
|
+
audioGain,
|
|
4983
|
+
masterFxChain,
|
|
4984
|
+
urlByAbsentLocalPath,
|
|
4985
|
+
keepEncoderInputAt
|
|
4986
|
+
},
|
|
4978
4987
|
compiledDir
|
|
4979
4988
|
);
|
|
4980
4989
|
} catch (err) {
|
|
@@ -5088,13 +5097,29 @@ function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, free
|
|
|
5088
5097
|
}
|
|
5089
5098
|
return { available: false, estimatedBytes, freeBytes };
|
|
5090
5099
|
}
|
|
5091
|
-
function
|
|
5100
|
+
function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
|
|
5101
|
+
const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
|
|
5102
|
+
if (workerCount > 1 && oneWorker.enabled) {
|
|
5103
|
+
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.`;
|
|
5104
|
+
}
|
|
5105
|
+
switch (oneWorker.reason) {
|
|
5106
|
+
case "format_excluded":
|
|
5107
|
+
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
5108
|
+
case "disabled_by_config":
|
|
5109
|
+
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
5110
|
+
case "duration_cap":
|
|
5111
|
+
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.";
|
|
5112
|
+
default:
|
|
5113
|
+
return "Free up disk space.";
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
|
|
5092
5117
|
const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
|
|
5093
5118
|
return new Error(
|
|
5094
|
-
`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).
|
|
5119
|
+
`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}`
|
|
5095
5120
|
);
|
|
5096
5121
|
}
|
|
5097
|
-
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes) {
|
|
5122
|
+
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy, freeDiskBytes) {
|
|
5098
5123
|
const headroom = inspectDiskCaptureHeadroom(
|
|
5099
5124
|
framesDir,
|
|
5100
5125
|
totalFrames,
|
|
@@ -5106,7 +5131,8 @@ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeD
|
|
|
5106
5131
|
headroom.estimatedBytes,
|
|
5107
5132
|
headroom.freeBytes,
|
|
5108
5133
|
framesDir,
|
|
5109
|
-
"estimate"
|
|
5134
|
+
"estimate",
|
|
5135
|
+
remedy
|
|
5110
5136
|
);
|
|
5111
5137
|
}
|
|
5112
5138
|
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
@@ -5126,7 +5152,7 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
5126
5152
|
return total;
|
|
5127
5153
|
}
|
|
5128
5154
|
function createDiskCaptureProjection(input) {
|
|
5129
|
-
const { framesDir, totalFrames, measureBytes, freeDiskBytes = freeDiskBytesAt } = input;
|
|
5155
|
+
const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
|
|
5130
5156
|
let checked = false;
|
|
5131
5157
|
return (capturedFrames) => {
|
|
5132
5158
|
if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
|
|
@@ -5138,7 +5164,7 @@ function createDiskCaptureProjection(input) {
|
|
|
5138
5164
|
if (remaining <= 0 || freeBytes === null) return;
|
|
5139
5165
|
const needBytes = Math.ceil(measured / capturedFrames * remaining);
|
|
5140
5166
|
if (needBytes > freeBytes * 0.9) {
|
|
5141
|
-
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured");
|
|
5167
|
+
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
|
|
5142
5168
|
}
|
|
5143
5169
|
};
|
|
5144
5170
|
}
|
|
@@ -5180,11 +5206,13 @@ async function runCaptureStage(input) {
|
|
|
5180
5206
|
}
|
|
5181
5207
|
}
|
|
5182
5208
|
const captureOptions = buildCaptureOptions();
|
|
5183
|
-
|
|
5209
|
+
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
5210
|
+
assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy);
|
|
5184
5211
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
5185
5212
|
framesDir,
|
|
5186
5213
|
totalFrames,
|
|
5187
|
-
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir)
|
|
5214
|
+
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
|
|
5215
|
+
remedy
|
|
5188
5216
|
});
|
|
5189
5217
|
if (workerCount > 1) {
|
|
5190
5218
|
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
@@ -5459,8 +5487,10 @@ function fpsToFfmpegArg2(fps) {
|
|
|
5459
5487
|
}
|
|
5460
5488
|
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
5461
5489
|
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
5490
|
+
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
5462
5491
|
function framesInput(input, fpsArg) {
|
|
5463
5492
|
return [
|
|
5493
|
+
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
5464
5494
|
"-framerate",
|
|
5465
5495
|
fpsArg,
|
|
5466
5496
|
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
@@ -5709,36 +5739,97 @@ import { mkdtempSync as mkdtempSync2, renameSync as renameSync2, rmSync as rmSyn
|
|
|
5709
5739
|
import { dirname as dirname6, join as join13 } from "node:path";
|
|
5710
5740
|
import {
|
|
5711
5741
|
DEFAULT_CONFIG,
|
|
5712
|
-
describeFfmpegFailure,
|
|
5713
|
-
|
|
5742
|
+
describeFfmpegFailure as describeFfmpegFailure2,
|
|
5743
|
+
encodeAac,
|
|
5744
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
5714
5745
|
getFfprobeBinary,
|
|
5715
5746
|
ManagedChildProcess,
|
|
5716
|
-
runFfmpeg as
|
|
5747
|
+
runFfmpeg as runFfmpeg5,
|
|
5717
5748
|
trackChildProcess
|
|
5718
5749
|
} from "@hyperframes/engine";
|
|
5719
5750
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
5751
|
+
|
|
5752
|
+
// src/services/render/audioTruePeak.ts
|
|
5753
|
+
import {
|
|
5754
|
+
aacSegmentCount,
|
|
5755
|
+
describeFfmpegFailure,
|
|
5756
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
5757
|
+
runFfmpeg as runFfmpeg4
|
|
5758
|
+
} from "@hyperframes/engine";
|
|
5759
|
+
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
5760
|
+
function checkedDbfs(value) {
|
|
5761
|
+
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
5762
|
+
throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
|
|
5763
|
+
}
|
|
5764
|
+
return value;
|
|
5765
|
+
}
|
|
5766
|
+
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
5767
|
+
let firstBlock;
|
|
5768
|
+
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
5769
|
+
let partialLine = "";
|
|
5770
|
+
const readLines = (chunk) => {
|
|
5771
|
+
const lines = (partialLine + chunk).split(/\r|\n/);
|
|
5772
|
+
partialLine = lines.pop() ?? "";
|
|
5773
|
+
for (const line of lines) {
|
|
5774
|
+
const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
|
|
5775
|
+
if (!ftpk) continue;
|
|
5776
|
+
const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
|
|
5777
|
+
if (firstBlock === void 0) firstBlock = block;
|
|
5778
|
+
else laterBlocks = Math.max(laterBlocks, block);
|
|
5779
|
+
}
|
|
5780
|
+
};
|
|
5781
|
+
const result = await runFfmpeg4(
|
|
5782
|
+
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
5783
|
+
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
5784
|
+
);
|
|
5785
|
+
if (!result.success) {
|
|
5786
|
+
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
5787
|
+
}
|
|
5788
|
+
const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
5789
|
+
return {
|
|
5790
|
+
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
5791
|
+
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
5792
|
+
laterBlocks: checkedDbfs(laterBlocks)
|
|
5793
|
+
};
|
|
5794
|
+
}
|
|
5795
|
+
async function measureTruePeakDbfs(audioPath, options) {
|
|
5796
|
+
const { timeoutMs, signal } = options;
|
|
5797
|
+
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
5798
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
5799
|
+
const block = 4800;
|
|
5800
|
+
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
5801
|
+
if (count === 1) return wholeFile();
|
|
5802
|
+
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
5803
|
+
const peers = new AbortController();
|
|
5804
|
+
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
5805
|
+
const scans = await Promise.all(
|
|
5806
|
+
Array.from({ length: count }, (_, k) => {
|
|
5807
|
+
const start = k === 0 ? 0 : cut(k) - block;
|
|
5808
|
+
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
5809
|
+
return scanTruePeak(
|
|
5810
|
+
audioPath,
|
|
5811
|
+
`atrim=start_sample=${start}${end},`,
|
|
5812
|
+
timeoutMs,
|
|
5813
|
+
segmentSignal
|
|
5814
|
+
);
|
|
5815
|
+
})
|
|
5816
|
+
).catch((error) => {
|
|
5817
|
+
peers.abort();
|
|
5818
|
+
throw error;
|
|
5819
|
+
});
|
|
5820
|
+
const last = scans[count - 1];
|
|
5821
|
+
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
5822
|
+
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
5823
|
+
return wholeFile();
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
// src/services/render/audioPadTrim.ts
|
|
5720
5827
|
var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
5721
5828
|
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
5722
5829
|
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
5723
5830
|
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
5724
|
-
function
|
|
5725
|
-
return
|
|
5726
|
-
"-i",
|
|
5727
|
-
inputPath,
|
|
5728
|
-
"-map",
|
|
5729
|
-
"0:a:0",
|
|
5730
|
-
"-vn",
|
|
5731
|
-
"-af",
|
|
5732
|
-
`volume=${attenuationDb.toFixed(3)}dB`,
|
|
5733
|
-
"-t",
|
|
5734
|
-
formatSeconds(targetDurationSeconds),
|
|
5735
|
-
"-c:a",
|
|
5736
|
-
"aac",
|
|
5737
|
-
"-b:a",
|
|
5738
|
-
"192k",
|
|
5739
|
-
"-y",
|
|
5740
|
-
outputPath
|
|
5741
|
-
];
|
|
5831
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
5832
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
5742
5833
|
}
|
|
5743
5834
|
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
5744
5835
|
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
@@ -5753,53 +5844,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
|
|
|
5753
5844
|
if (delta > 0) {
|
|
5754
5845
|
return {
|
|
5755
5846
|
operation: "pad",
|
|
5756
|
-
steps: [
|
|
5757
|
-
{
|
|
5758
|
-
kind: "normalize",
|
|
5759
|
-
args: [
|
|
5760
|
-
"-i",
|
|
5761
|
-
audioPath,
|
|
5762
|
-
"-af",
|
|
5763
|
-
`apad,atrim=0:${targetSec}`,
|
|
5764
|
-
"-t",
|
|
5765
|
-
targetSec,
|
|
5766
|
-
"-c:a",
|
|
5767
|
-
"aac",
|
|
5768
|
-
"-b:a",
|
|
5769
|
-
"192k",
|
|
5770
|
-
"-y",
|
|
5771
|
-
outputPath
|
|
5772
|
-
]
|
|
5773
|
-
}
|
|
5774
|
-
],
|
|
5847
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
5775
5848
|
cleanupPaths: []
|
|
5776
5849
|
};
|
|
5777
5850
|
}
|
|
5778
5851
|
return {
|
|
5779
5852
|
operation: "trim",
|
|
5780
|
-
steps: [
|
|
5781
|
-
{
|
|
5782
|
-
kind: "trim",
|
|
5783
|
-
args: [
|
|
5784
|
-
"-i",
|
|
5785
|
-
audioPath,
|
|
5786
|
-
"-af",
|
|
5787
|
-
`atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
|
|
5788
|
-
// `atrim` limits decoded samples but does not cap muxer timestamps
|
|
5789
|
-
// introduced by encoder delay/flush. The output duration contract
|
|
5790
|
-
// is enforced at the container boundary as well; without `-t`, a
|
|
5791
|
-
// long primed source can retain its original tail after re-encode.
|
|
5792
|
-
"-t",
|
|
5793
|
-
targetSec,
|
|
5794
|
-
"-c:a",
|
|
5795
|
-
"aac",
|
|
5796
|
-
"-b:a",
|
|
5797
|
-
"192k",
|
|
5798
|
-
"-y",
|
|
5799
|
-
outputPath
|
|
5800
|
-
]
|
|
5801
|
-
}
|
|
5802
|
-
],
|
|
5853
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
5803
5854
|
cleanupPaths: []
|
|
5804
5855
|
};
|
|
5805
5856
|
}
|
|
@@ -5816,7 +5867,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5816
5867
|
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
5817
5868
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
5818
5869
|
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
5819
|
-
const
|
|
5870
|
+
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
5871
|
+
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
5820
5872
|
const [videoResult, audioResult] = await Promise.allSettled([
|
|
5821
5873
|
probeVideo(input.videoPath),
|
|
5822
5874
|
probeAudio(input.audioPath, input.signal)
|
|
@@ -5863,7 +5915,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5863
5915
|
);
|
|
5864
5916
|
try {
|
|
5865
5917
|
for (const step of plan2.steps) {
|
|
5866
|
-
const ffmpegResult = await runner(step.args);
|
|
5918
|
+
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
5867
5919
|
if (!ffmpegResult.success) {
|
|
5868
5920
|
return {
|
|
5869
5921
|
success: false,
|
|
@@ -5895,7 +5947,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5895
5947
|
targetDurationSeconds,
|
|
5896
5948
|
signal: input.signal,
|
|
5897
5949
|
probeTruePeak,
|
|
5898
|
-
|
|
5950
|
+
encode
|
|
5899
5951
|
});
|
|
5900
5952
|
if (!correction.success) {
|
|
5901
5953
|
return {
|
|
@@ -5925,8 +5977,8 @@ async function constrainAacTruePeak(input) {
|
|
|
5925
5977
|
audioPath: input.audioPath,
|
|
5926
5978
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
5927
5979
|
signal: input.signal,
|
|
5928
|
-
probeTruePeak: (audioPath, signal) =>
|
|
5929
|
-
|
|
5980
|
+
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
5981
|
+
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
5930
5982
|
});
|
|
5931
5983
|
}
|
|
5932
5984
|
async function enforceAacTruePeak(input) {
|
|
@@ -5953,13 +6005,11 @@ async function enforceAacTruePeak(input) {
|
|
|
5953
6005
|
}
|
|
5954
6006
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
5955
6007
|
attenuationDb += correctionTargetDbfs - truePeakDbfs;
|
|
5956
|
-
const result = await input.
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
attenuationDb
|
|
5962
|
-
)
|
|
6008
|
+
const result = await input.encode(
|
|
6009
|
+
input.audioPath,
|
|
6010
|
+
correctedPath,
|
|
6011
|
+
input.targetDurationSeconds,
|
|
6012
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
5963
6013
|
);
|
|
5964
6014
|
if (!result.success) {
|
|
5965
6015
|
return {
|
|
@@ -6054,7 +6104,7 @@ function parseFrameRate(rate) {
|
|
|
6054
6104
|
return { fpsNum, fpsDen };
|
|
6055
6105
|
}
|
|
6056
6106
|
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
6057
|
-
const metadata = await
|
|
6107
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
6058
6108
|
return {
|
|
6059
6109
|
durationSeconds: metadata.durationSeconds,
|
|
6060
6110
|
sampleRate: metadata.sampleRate,
|
|
@@ -6062,28 +6112,23 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
6062
6112
|
audioCodec: metadata.audioCodec
|
|
6063
6113
|
};
|
|
6064
6114
|
}
|
|
6065
|
-
async function defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal) {
|
|
6066
|
-
const result = await runFfmpeg4(
|
|
6067
|
-
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", "ebur128=peak=true", "-f", "null", "-"],
|
|
6068
|
-
{ signal, timeout: timeoutMs }
|
|
6069
|
-
);
|
|
6070
|
-
if (!result.success) {
|
|
6071
|
-
throw new Error(`[audioPadTrim] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
6072
|
-
}
|
|
6073
|
-
const matches = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
6074
|
-
const token = matches.at(-1)?.[1]?.toLowerCase();
|
|
6075
|
-
const value = token === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
6076
|
-
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
6077
|
-
throw new Error("[audioPadTrim] FFmpeg did not report AAC true peak");
|
|
6078
|
-
}
|
|
6079
|
-
return value;
|
|
6080
|
-
}
|
|
6081
6115
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
6082
|
-
|
|
6116
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
6117
|
+
}
|
|
6118
|
+
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
6119
|
+
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
6120
|
+
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
6121
|
+
return toStepResult(
|
|
6122
|
+
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
6123
|
+
timeoutMs
|
|
6124
|
+
);
|
|
6125
|
+
};
|
|
6126
|
+
}
|
|
6127
|
+
function toStepResult(result, timeoutMs) {
|
|
6083
6128
|
if (result.success) return { success: true };
|
|
6084
6129
|
return {
|
|
6085
6130
|
success: false,
|
|
6086
|
-
error: `[audioPadTrim] ${
|
|
6131
|
+
error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
|
|
6087
6132
|
failureReason: result.failureReason
|
|
6088
6133
|
};
|
|
6089
6134
|
}
|
|
@@ -6455,15 +6500,33 @@ function createRenderJob(config) {
|
|
|
6455
6500
|
warnings: []
|
|
6456
6501
|
};
|
|
6457
6502
|
}
|
|
6503
|
+
function explainStreamingEncodeGate(cfg, outputFormat, workerCount, durationSeconds, forceParallelStream = false) {
|
|
6504
|
+
if (!cfg.enableStreamingEncode) return { enabled: false, reason: "disabled_by_config" };
|
|
6505
|
+
if (outputFormat === "png-sequence" || outputFormat === "gif") {
|
|
6506
|
+
return { enabled: false, reason: "format_excluded" };
|
|
6507
|
+
}
|
|
6508
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
6509
|
+
return { enabled: false, reason: "invalid_duration" };
|
|
6510
|
+
}
|
|
6511
|
+
const overCap = cfg.streamingEncodeDurationCapEnabled === true && durationSeconds > cfg.streamingEncodeMaxDurationSeconds;
|
|
6512
|
+
if (overCap && !cfg.lowMemoryMode) return { enabled: false, reason: "duration_cap" };
|
|
6513
|
+
if (forceParallelStream || process.env.HF_DE_PARALLEL_STREAM === "true") {
|
|
6514
|
+
return { enabled: true, reason: "parallel_forced" };
|
|
6515
|
+
}
|
|
6516
|
+
if (workerCount === 1) {
|
|
6517
|
+
return { enabled: true, reason: overCap ? "low_memory_mode" : "single_worker" };
|
|
6518
|
+
}
|
|
6519
|
+
return { enabled: false, reason: "multi_worker" };
|
|
6520
|
+
}
|
|
6458
6521
|
var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
|
|
6459
6522
|
|
|
6460
6523
|
// src/services/distributed/planFrameCost.ts
|
|
6461
6524
|
import { join as join17 } from "node:path";
|
|
6462
6525
|
import {
|
|
6463
6526
|
closeCaptureSession as closeCaptureSession9,
|
|
6464
|
-
createCaptureSession as
|
|
6527
|
+
createCaptureSession as createCaptureSession9,
|
|
6465
6528
|
createFrameLookupTable as createFrameLookupTable3,
|
|
6466
|
-
initializeSession as
|
|
6529
|
+
initializeSession as initializeSession9
|
|
6467
6530
|
} from "@hyperframes/engine";
|
|
6468
6531
|
|
|
6469
6532
|
// src/services/distributed/chunkFrames.ts
|
|
@@ -6861,6 +6924,29 @@ function buildChunkCaptureOptions(input) {
|
|
|
6861
6924
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
6862
6925
|
};
|
|
6863
6926
|
}
|
|
6927
|
+
function shownDeferredFrameIndexes(planVideos, frames, fps) {
|
|
6928
|
+
const shown = /* @__PURE__ */ new Map();
|
|
6929
|
+
const deferred = planVideos.extracted.filter((video) => video.deferredRange);
|
|
6930
|
+
if (deferred.length === 0) return shown;
|
|
6931
|
+
const table = createFrameLookupTable2(
|
|
6932
|
+
planVideos.videos,
|
|
6933
|
+
deferred.map((video) => ({
|
|
6934
|
+
...video,
|
|
6935
|
+
outputDir: "",
|
|
6936
|
+
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
6937
|
+
})),
|
|
6938
|
+
resolveRenderFpsConfig(fps).value
|
|
6939
|
+
);
|
|
6940
|
+
for (const frame of frames) {
|
|
6941
|
+
const globalTime = frame * fps.den / fps.num;
|
|
6942
|
+
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
6943
|
+
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
6944
|
+
indexes.add(payload.frameIndex);
|
|
6945
|
+
shown.set(videoId, indexes);
|
|
6946
|
+
}
|
|
6947
|
+
}
|
|
6948
|
+
return shown;
|
|
6949
|
+
}
|
|
6864
6950
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
6865
6951
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
6866
6952
|
const remoteSources = input.remoteSources ?? {};
|
|
@@ -6877,24 +6963,7 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
6877
6963
|
});
|
|
6878
6964
|
}
|
|
6879
6965
|
if (deferred.length === 0) return [];
|
|
6880
|
-
const
|
|
6881
|
-
input.planVideos.videos,
|
|
6882
|
-
deferred.map((video) => ({
|
|
6883
|
-
...video,
|
|
6884
|
-
outputDir: input.outputRoot,
|
|
6885
|
-
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
6886
|
-
})),
|
|
6887
|
-
resolveRenderFpsConfig(input.fps).value
|
|
6888
|
-
);
|
|
6889
|
-
const shown = /* @__PURE__ */ new Map();
|
|
6890
|
-
for (const frame of input.frames) {
|
|
6891
|
-
const globalTime = frame * input.fps.den / input.fps.num;
|
|
6892
|
-
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
6893
|
-
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
6894
|
-
indexes.add(payload.frameIndex);
|
|
6895
|
-
shown.set(videoId, indexes);
|
|
6896
|
-
}
|
|
6897
|
-
}
|
|
6966
|
+
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
6898
6967
|
return Promise.all(
|
|
6899
6968
|
deferred.map(async (video) => {
|
|
6900
6969
|
const outputDir = join16(input.outputRoot, video.videoId);
|
|
@@ -6920,7 +6989,8 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
6920
6989
|
fps: input.fps,
|
|
6921
6990
|
outputDir: input.outputRoot,
|
|
6922
6991
|
format: range.format,
|
|
6923
|
-
frameRanges: contiguousRuns(indexes)
|
|
6992
|
+
frameRanges: contiguousRuns(indexes),
|
|
6993
|
+
shownBox: input.shownBoxes?.get(video.videoId)
|
|
6924
6994
|
},
|
|
6925
6995
|
void 0,
|
|
6926
6996
|
input.cfg
|
|
@@ -6967,7 +7037,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
6967
7037
|
});
|
|
6968
7038
|
let session = null;
|
|
6969
7039
|
try {
|
|
6970
|
-
session = await
|
|
7040
|
+
session = await createCaptureSession9(
|
|
6971
7041
|
fileServer.url,
|
|
6972
7042
|
join17(workDir, "frame-cost"),
|
|
6973
7043
|
buildChunkCaptureOptions({
|
|
@@ -6979,7 +7049,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
6979
7049
|
createChunkVideoFrameInjectorFactory(lookup)(),
|
|
6980
7050
|
createCaptureCalibrationConfig(input.cfg)
|
|
6981
7051
|
);
|
|
6982
|
-
await
|
|
7052
|
+
await initializeSession9(session);
|
|
6983
7053
|
const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
|
|
6984
7054
|
return (estimate.p95Ms ?? 0) / 1e3;
|
|
6985
7055
|
} finally {
|
|
@@ -7375,7 +7445,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
7375
7445
|
import { closeSync as closeSync4, existsSync as existsSync14, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
7376
7446
|
import { writeFileSync as writeFileSync5, writeSync } from "node:fs";
|
|
7377
7447
|
import { join as join20 } from "node:path";
|
|
7378
|
-
import { appendRenderProvenanceArgs, describeFfmpegFailure as
|
|
7448
|
+
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
7379
7449
|
|
|
7380
7450
|
// src/services/distributed/mp4Stitch.ts
|
|
7381
7451
|
import {
|
|
@@ -7573,6 +7643,20 @@ function withDuration(buf, target, duration) {
|
|
|
7573
7643
|
function timescaleOf(buf, header) {
|
|
7574
7644
|
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
7575
7645
|
}
|
|
7646
|
+
var TRACK_ID_AT = { tkhd: [12, 20], mvhd: [96, 108] };
|
|
7647
|
+
function idOf(buf, target) {
|
|
7648
|
+
return buf.readUInt32BE(
|
|
7649
|
+
target.bodyStart + TRACK_ID_AT[target.type][buf[target.bodyStart] === 1 ? 1 : 0]
|
|
7650
|
+
);
|
|
7651
|
+
}
|
|
7652
|
+
function withId(boxBytes, type, id) {
|
|
7653
|
+
const header = boxAt(boxBytes, 0, 0, boxBytes.length);
|
|
7654
|
+
boxBytes.writeUInt32BE(
|
|
7655
|
+
id,
|
|
7656
|
+
header.bodyStart + TRACK_ID_AT[type][boxBytes[header.bodyStart] === 1 ? 1 : 0]
|
|
7657
|
+
);
|
|
7658
|
+
return boxBytes;
|
|
7659
|
+
}
|
|
7576
7660
|
function buildStbl(stsd, track) {
|
|
7577
7661
|
const stts = runs(track.durations);
|
|
7578
7662
|
const stsc = [];
|
|
@@ -7614,24 +7698,48 @@ function planTracksOf(tracks, plan2, index) {
|
|
|
7614
7698
|
}
|
|
7615
7699
|
return matched;
|
|
7616
7700
|
}
|
|
7617
|
-
function
|
|
7618
|
-
const
|
|
7619
|
-
const
|
|
7701
|
+
function sourceTracks(layouts, audio) {
|
|
7702
|
+
const chunkPlan = readTracks(layouts[0].moov);
|
|
7703
|
+
const audioTracks = audio ? readTracks(audio.moov) : [];
|
|
7704
|
+
if (audio && (audioTracks.length !== 1 || audioTracks[0].handler !== "soun")) {
|
|
7705
|
+
throw new Error("[mp4Stitch] the separate audio file must hold one audio track");
|
|
7706
|
+
}
|
|
7707
|
+
const plan2 = [...chunkPlan, ...audioTracks];
|
|
7708
|
+
const chunks = layouts.map((layout, i) => {
|
|
7709
|
+
const tracks = readTracks(layout.moov);
|
|
7710
|
+
if (audio && tracks.some((track) => track.handler === "soun")) {
|
|
7711
|
+
throw new Error(`[mp4Stitch] chunk ${i} carries its own audio`);
|
|
7712
|
+
}
|
|
7713
|
+
return planTracksOf(tracks, plan2, i);
|
|
7714
|
+
});
|
|
7715
|
+
return {
|
|
7716
|
+
plan: plan2,
|
|
7717
|
+
chunkTrackCount: chunkPlan.length,
|
|
7718
|
+
sources: audio ? [[...chunkPlan.map(() => null), audioTracks[0]], ...chunks] : chunks
|
|
7719
|
+
};
|
|
7720
|
+
}
|
|
7721
|
+
function buildStitchedHeader(layouts, audio) {
|
|
7722
|
+
const { plan: plan2, chunkTrackCount, sources } = sourceTracks(layouts, audio);
|
|
7723
|
+
const sourceLayouts = audio ? [audio, ...layouts] : layouts;
|
|
7620
7724
|
const payloadBases = [];
|
|
7621
7725
|
let payloadBytes = 0;
|
|
7622
|
-
for (const layout of
|
|
7726
|
+
for (const layout of sourceLayouts) {
|
|
7623
7727
|
payloadBases.push(payloadBytes);
|
|
7624
7728
|
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
7625
7729
|
}
|
|
7626
7730
|
const template = layouts[0].moov;
|
|
7627
7731
|
const root = boxAt(template, 0, 0, template.length);
|
|
7628
|
-
const
|
|
7732
|
+
const mvhd = child(template, root, "mvhd");
|
|
7733
|
+
const movieTimescale = timescaleOf(template, mvhd);
|
|
7734
|
+
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child(template, track.trak, "tkhd")));
|
|
7735
|
+
const audioTrackId = Math.max(...chunkTrackIds) + 1;
|
|
7629
7736
|
const buildMoov = (dataStart2) => {
|
|
7630
7737
|
const traks = plan2.map((track, t) => {
|
|
7631
|
-
const parts =
|
|
7738
|
+
const parts = sources.flatMap((tracks, chunk) => {
|
|
7632
7739
|
const part = tracks[t];
|
|
7633
7740
|
return part ? [{ ...part, chunk }] : [];
|
|
7634
7741
|
});
|
|
7742
|
+
const moov = t < chunkTrackCount ? template : audio.moov;
|
|
7635
7743
|
const merged = {
|
|
7636
7744
|
durations: parts.flatMap((part) => part.durations),
|
|
7637
7745
|
sizes: parts.flatMap((part) => part.sizes),
|
|
@@ -7640,9 +7748,9 @@ function buildStitchedHeader(layouts) {
|
|
|
7640
7748
|
chunkOffsets: parts.flatMap(
|
|
7641
7749
|
(part) => part.chunkOffsets.map((offset) => {
|
|
7642
7750
|
const i = part.chunk;
|
|
7643
|
-
const { payloadStart, payloadEnd } =
|
|
7751
|
+
const { payloadStart, payloadEnd } = sourceLayouts[i];
|
|
7644
7752
|
if (offset < payloadStart || offset >= payloadEnd) {
|
|
7645
|
-
throw new Error(`[mp4Stitch]
|
|
7753
|
+
throw new Error(`[mp4Stitch] source ${i} has samples outside its mdat`);
|
|
7646
7754
|
}
|
|
7647
7755
|
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
7648
7756
|
})
|
|
@@ -7658,20 +7766,25 @@ function buildStitchedHeader(layouts) {
|
|
|
7658
7766
|
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
7659
7767
|
elst.writeUInt32BE(65536, 20);
|
|
7660
7768
|
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
7661
|
-
const
|
|
7662
|
-
|
|
7769
|
+
const tkhdOf = (tkhd) => {
|
|
7770
|
+
const timed = withDuration(moov, tkhd, shown);
|
|
7771
|
+
return t < chunkTrackCount ? timed : withId(timed, "tkhd", audioTrackId);
|
|
7772
|
+
};
|
|
7773
|
+
const trak = rebuild(moov, track.trak, {
|
|
7774
|
+
tkhd: (tkhd) => Buffer.concat([tkhdOf(tkhd), edts]),
|
|
7663
7775
|
edts: () => Buffer.alloc(0),
|
|
7664
|
-
mdia: (mdia) => rebuild(
|
|
7665
|
-
mdhd: (mdhd) => withDuration(
|
|
7666
|
-
minf: (minf) => rebuild(
|
|
7776
|
+
mdia: (mdia) => rebuild(moov, mdia, {
|
|
7777
|
+
mdhd: (mdhd) => withDuration(moov, mdhd, mediaDuration),
|
|
7778
|
+
minf: (minf) => rebuild(moov, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
7667
7779
|
})
|
|
7668
7780
|
});
|
|
7669
7781
|
return { trak, shown };
|
|
7670
7782
|
});
|
|
7671
7783
|
let next = 0;
|
|
7784
|
+
const movieHeader = withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown)));
|
|
7672
7785
|
return rebuild(template, root, {
|
|
7673
|
-
mvhd: (
|
|
7674
|
-
trak: () => traks[next
|
|
7786
|
+
mvhd: () => audio ? withId(movieHeader, "mvhd", audioTrackId + 1) : movieHeader,
|
|
7787
|
+
trak: () => ++next < chunkTrackCount ? traks[next - 1].trak : Buffer.concat(traks.slice(next - 1).map((t) => t.trak))
|
|
7675
7788
|
});
|
|
7676
7789
|
};
|
|
7677
7790
|
const ftyp = layouts[0].ftyp;
|
|
@@ -7704,15 +7817,16 @@ async function copyPayload(path, layout, index, out) {
|
|
|
7704
7817
|
);
|
|
7705
7818
|
}
|
|
7706
7819
|
}
|
|
7707
|
-
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
7708
|
-
const
|
|
7709
|
-
const
|
|
7820
|
+
async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
7821
|
+
const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
|
|
7822
|
+
const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
|
|
7823
|
+
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
7710
7824
|
mkdirSync8(dirname9(outputPath), { recursive: true });
|
|
7711
7825
|
const workDir = mkdtempSync3(`${outputPath}.stitch-`);
|
|
7712
7826
|
const out = createWriteStream(join19(workDir, "out.mp4"));
|
|
7713
7827
|
try {
|
|
7714
7828
|
out.write(header);
|
|
7715
|
-
for (const [i, path] of
|
|
7829
|
+
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
7716
7830
|
out.end();
|
|
7717
7831
|
await finished(out);
|
|
7718
7832
|
renameSync3(join19(workDir, "out.mp4"), outputPath);
|
|
@@ -7865,10 +7979,10 @@ async function muxChunkWithAudio(input) {
|
|
|
7865
7979
|
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
7866
7980
|
appendRenderProvenanceArgs(args, input.outputPath);
|
|
7867
7981
|
args.push("-y", input.outputPath);
|
|
7868
|
-
const result = await
|
|
7982
|
+
const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
7869
7983
|
if (!result.success) {
|
|
7870
7984
|
throw new Error(
|
|
7871
|
-
`[chunkAudio] chunk mux failed: ${
|
|
7985
|
+
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
|
|
7872
7986
|
);
|
|
7873
7987
|
}
|
|
7874
7988
|
const slice = input.chunks[input.chunkIndex];
|
|
@@ -8287,7 +8401,8 @@ function buildLockedRenderConfig(input) {
|
|
|
8287
8401
|
chunkCount: input.chunkCount,
|
|
8288
8402
|
runtimeEnv: input.runtimeEnv,
|
|
8289
8403
|
variables: config.variables,
|
|
8290
|
-
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
|
|
8404
|
+
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
8405
|
+
...input.separateAudio ? { separateAudio: true } : {}
|
|
8291
8406
|
};
|
|
8292
8407
|
}
|
|
8293
8408
|
function carriesChunkAudio(config, options) {
|
|
@@ -8382,8 +8497,10 @@ function createVideoSourceShipper(input) {
|
|
|
8382
8497
|
return result;
|
|
8383
8498
|
};
|
|
8384
8499
|
}
|
|
8385
|
-
|
|
8386
|
-
|
|
8500
|
+
function plansSeparateAudio(config, options) {
|
|
8501
|
+
return options.deferAudio === true && config.format === "mp4" && config.cfr !== true;
|
|
8502
|
+
}
|
|
8503
|
+
async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
8387
8504
|
rejectUnsupportedDistributedFormat(config);
|
|
8388
8505
|
validateNoGpuEncode({
|
|
8389
8506
|
useGpu: false,
|
|
@@ -8519,6 +8636,100 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8519
8636
|
});
|
|
8520
8637
|
}
|
|
8521
8638
|
}
|
|
8639
|
+
return {
|
|
8640
|
+
projectDir,
|
|
8641
|
+
log,
|
|
8642
|
+
sizeLimitBytes,
|
|
8643
|
+
abortSignal,
|
|
8644
|
+
assertNotAborted,
|
|
8645
|
+
cfg,
|
|
8646
|
+
job,
|
|
8647
|
+
workDir,
|
|
8648
|
+
compiledDir,
|
|
8649
|
+
finalCompiledDir,
|
|
8650
|
+
remoteFiles,
|
|
8651
|
+
urlByAbsentLocalPath,
|
|
8652
|
+
localPathByUrl,
|
|
8653
|
+
compileResult,
|
|
8654
|
+
compiled,
|
|
8655
|
+
composition,
|
|
8656
|
+
deviceScaleFactor,
|
|
8657
|
+
forceScreenshot,
|
|
8658
|
+
width,
|
|
8659
|
+
height,
|
|
8660
|
+
probeResult,
|
|
8661
|
+
totalFrames
|
|
8662
|
+
};
|
|
8663
|
+
}
|
|
8664
|
+
function mixPlanAudio(c, compiledDir, seconds, pcmPath) {
|
|
8665
|
+
return runAudioStage({
|
|
8666
|
+
projectDir: c.projectDir,
|
|
8667
|
+
workDir: c.workDir,
|
|
8668
|
+
compiledDir,
|
|
8669
|
+
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
8670
|
+
duration: seconds,
|
|
8671
|
+
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
8672
|
+
audioGain: c.cfg.audioGain,
|
|
8673
|
+
audios: c.composition.audios,
|
|
8674
|
+
masterFxChain: c.composition.masterFxChain,
|
|
8675
|
+
...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
|
|
8676
|
+
abortSignal: c.abortSignal,
|
|
8677
|
+
assertNotAborted: c.assertNotAborted
|
|
8678
|
+
});
|
|
8679
|
+
}
|
|
8680
|
+
async function extractPlanVideos(c, deferRangeExtraction) {
|
|
8681
|
+
const extractResult = await runExtractVideosStage({
|
|
8682
|
+
projectDir: c.projectDir,
|
|
8683
|
+
compiledDir: c.compiledDir,
|
|
8684
|
+
job: c.job,
|
|
8685
|
+
cfg: c.cfg,
|
|
8686
|
+
composition: c.composition,
|
|
8687
|
+
abortSignal: c.abortSignal,
|
|
8688
|
+
assertNotAborted: c.assertNotAborted,
|
|
8689
|
+
materializeSymlinks: true,
|
|
8690
|
+
deferRangeExtraction,
|
|
8691
|
+
urlByAbsentLocalPath: c.urlByAbsentLocalPath
|
|
8692
|
+
});
|
|
8693
|
+
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
8694
|
+
if (extractResult.extractionResult) {
|
|
8695
|
+
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
8696
|
+
}
|
|
8697
|
+
return extractResult;
|
|
8698
|
+
}
|
|
8699
|
+
async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
|
|
8700
|
+
const peak = await constrainAacTruePeak({
|
|
8701
|
+
audioPath,
|
|
8702
|
+
...existsSync15(pcmPath) ? { sourcePath: pcmPath } : {},
|
|
8703
|
+
targetDurationSeconds: seconds,
|
|
8704
|
+
signal: c.abortSignal,
|
|
8705
|
+
timeoutMs: c.cfg.ffmpegProcessTimeout
|
|
8706
|
+
}).finally(() => rmSync8(pcmPath, { force: true }));
|
|
8707
|
+
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
8708
|
+
}
|
|
8709
|
+
async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
|
|
8710
|
+
const planDir = executionPlanDir;
|
|
8711
|
+
const c = await compilePlanComposition(projectDir, config, planDir, options);
|
|
8712
|
+
const {
|
|
8713
|
+
log,
|
|
8714
|
+
sizeLimitBytes,
|
|
8715
|
+
cfg,
|
|
8716
|
+
job,
|
|
8717
|
+
workDir,
|
|
8718
|
+
compiledDir,
|
|
8719
|
+
finalCompiledDir,
|
|
8720
|
+
remoteFiles,
|
|
8721
|
+
urlByAbsentLocalPath,
|
|
8722
|
+
localPathByUrl,
|
|
8723
|
+
compileResult,
|
|
8724
|
+
compiled,
|
|
8725
|
+
composition,
|
|
8726
|
+
deviceScaleFactor,
|
|
8727
|
+
forceScreenshot,
|
|
8728
|
+
width,
|
|
8729
|
+
height,
|
|
8730
|
+
probeResult,
|
|
8731
|
+
totalFrames
|
|
8732
|
+
} = c;
|
|
8522
8733
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
8523
8734
|
let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
|
|
8524
8735
|
const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
|
|
@@ -8529,24 +8740,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8529
8740
|
observedAt: "pre-extract",
|
|
8530
8741
|
log
|
|
8531
8742
|
});
|
|
8532
|
-
stageStarted = Date.now();
|
|
8533
|
-
const extractResult = await
|
|
8534
|
-
projectDir,
|
|
8535
|
-
compiledDir,
|
|
8536
|
-
job,
|
|
8537
|
-
cfg,
|
|
8538
|
-
composition,
|
|
8539
|
-
abortSignal,
|
|
8540
|
-
assertNotAborted,
|
|
8541
|
-
materializeSymlinks: true,
|
|
8542
|
-
deferRangeExtraction: options.deferVideoExtraction,
|
|
8543
|
-
urlByAbsentLocalPath
|
|
8544
|
-
});
|
|
8743
|
+
let stageStarted = Date.now();
|
|
8744
|
+
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
8545
8745
|
logPlanStage(log, "extract", stageStarted);
|
|
8546
|
-
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
8547
|
-
if (extractResult.extractionResult) {
|
|
8548
|
-
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
8549
|
-
}
|
|
8550
8746
|
const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
|
|
8551
8747
|
const videoFramesDst = join22(planDir, "video-frames");
|
|
8552
8748
|
if (existsSync15(videoFramesDst)) rmSync8(videoFramesDst, { recursive: true, force: true });
|
|
@@ -8602,23 +8798,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8602
8798
|
"utf-8"
|
|
8603
8799
|
);
|
|
8604
8800
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
8605
|
-
const
|
|
8606
|
-
const
|
|
8801
|
+
const separateAudio = plansSeparateAudio(config, options);
|
|
8802
|
+
const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
|
|
8803
|
+
const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
|
|
8804
|
+
const planMixPcmPath = join22(workDir, "plan-mix.wav");
|
|
8607
8805
|
stageStarted = Date.now();
|
|
8608
8806
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
8616
|
-
audioGain: cfg.audioGain,
|
|
8617
|
-
audios: composition.audios,
|
|
8618
|
-
masterFxChain: composition.masterFxChain,
|
|
8619
|
-
abortSignal,
|
|
8620
|
-
assertNotAborted
|
|
8621
|
-
}).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8807
|
+
separateAudio ? null : mixPlanAudio(
|
|
8808
|
+
c,
|
|
8809
|
+
finalCompiledDir,
|
|
8810
|
+
audioSeconds,
|
|
8811
|
+
chunksCarryAudio ? planMixPcmPath : void 0
|
|
8812
|
+
).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8622
8813
|
(async () => {
|
|
8623
8814
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
8624
8815
|
await prehashPlanFiles(dir, digests);
|
|
@@ -8668,22 +8859,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8668
8859
|
);
|
|
8669
8860
|
const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
|
|
8670
8861
|
const audioResult = audioOutcome.value;
|
|
8671
|
-
if (audioResult
|
|
8862
|
+
if (audioResult?.audioError) {
|
|
8672
8863
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
8673
8864
|
}
|
|
8674
8865
|
const planAudioPath = join22(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
8675
|
-
if (audioResult
|
|
8866
|
+
if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
|
|
8676
8867
|
renameSync4(audioResult.audioOutputPath, planAudioPath);
|
|
8677
8868
|
}
|
|
8678
|
-
if (
|
|
8869
|
+
if (chunksCarryAudio && existsSync15(planAudioPath)) {
|
|
8679
8870
|
stageStarted = Date.now();
|
|
8680
|
-
|
|
8681
|
-
audioPath: planAudioPath,
|
|
8682
|
-
targetDurationSeconds: audioSeconds,
|
|
8683
|
-
signal: abortSignal,
|
|
8684
|
-
timeoutMs: cfg.ffmpegProcessTimeout
|
|
8685
|
-
});
|
|
8686
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
8871
|
+
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
8687
8872
|
logPlanStage(log, "audio-peak", stageStarted);
|
|
8688
8873
|
if (options.sliceChunkAudio) {
|
|
8689
8874
|
stageStarted = Date.now();
|
|
@@ -8709,7 +8894,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8709
8894
|
effectiveChunkSize,
|
|
8710
8895
|
chunkCount,
|
|
8711
8896
|
runtimeEnv,
|
|
8712
|
-
chunksCarryAudio
|
|
8897
|
+
chunksCarryAudio,
|
|
8898
|
+
separateAudio
|
|
8713
8899
|
});
|
|
8714
8900
|
const compositionJson = {
|
|
8715
8901
|
durationSeconds: job.duration ?? 0,
|
|
@@ -8754,7 +8940,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8754
8940
|
fontSnapshotSha,
|
|
8755
8941
|
durationSeconds: job.duration ?? 0,
|
|
8756
8942
|
totalFrames,
|
|
8757
|
-
hasAudio: audioResult
|
|
8943
|
+
hasAudio: audioResult?.hasAudio ?? false,
|
|
8758
8944
|
digests
|
|
8759
8945
|
});
|
|
8760
8946
|
logPlanStage(log, "freeze", stageStarted);
|
|
@@ -8779,6 +8965,33 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8779
8965
|
producerVersion
|
|
8780
8966
|
};
|
|
8781
8967
|
}
|
|
8968
|
+
async function buildPlanAudio(projectDir, config, workRoot, outputPath, options = {}) {
|
|
8969
|
+
const c = await compilePlanComposition(projectDir, config, workRoot, {
|
|
8970
|
+
...options,
|
|
8971
|
+
executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER
|
|
8972
|
+
});
|
|
8973
|
+
try {
|
|
8974
|
+
const seconds = c.totalFrames / config.fps;
|
|
8975
|
+
const pcmPath = join22(c.workDir, "plan-mix.wav");
|
|
8976
|
+
let stageStarted = Date.now();
|
|
8977
|
+
await extractPlanVideos(c, true);
|
|
8978
|
+
logPlanStage(c.log, "extract", stageStarted);
|
|
8979
|
+
stageStarted = Date.now();
|
|
8980
|
+
const mixed = await mixPlanAudio(c, c.compiledDir, seconds, pcmPath);
|
|
8981
|
+
logPlanStage(c.log, "audio", stageStarted);
|
|
8982
|
+
if (mixed.audioError) {
|
|
8983
|
+
applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
|
|
8984
|
+
}
|
|
8985
|
+
if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
|
|
8986
|
+
renameSync4(mixed.audioOutputPath, outputPath);
|
|
8987
|
+
stageStarted = Date.now();
|
|
8988
|
+
await holdPlanAudioPeak(c, outputPath, pcmPath, seconds);
|
|
8989
|
+
logPlanStage(c.log, "audio-peak", stageStarted);
|
|
8990
|
+
return { hasAudio: true };
|
|
8991
|
+
} finally {
|
|
8992
|
+
rmSync8(c.workDir, { recursive: true, force: true });
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8782
8995
|
async function plan(projectDir, config, planDir, options = {}) {
|
|
8783
8996
|
const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir, options);
|
|
8784
8997
|
return {
|
|
@@ -9387,7 +9600,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
9387
9600
|
)
|
|
9388
9601
|
),
|
|
9389
9602
|
chunksCarryAudio: options.chunksCarryAudio,
|
|
9390
|
-
sliceChunkAudio: true
|
|
9603
|
+
sliceChunkAudio: true,
|
|
9604
|
+
deferAudio: options.deferAudio
|
|
9391
9605
|
});
|
|
9392
9606
|
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
9393
9607
|
} catch (error) {
|
|
@@ -9698,12 +9912,12 @@ import {
|
|
|
9698
9912
|
import { dirname as dirname12, join as join26 } from "node:path";
|
|
9699
9913
|
import {
|
|
9700
9914
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
9701
|
-
describeFfmpegFailure as
|
|
9915
|
+
describeFfmpegFailure as describeFfmpegFailure4,
|
|
9702
9916
|
getEncoderPreset as getEncoderPreset5,
|
|
9703
9917
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
|
|
9704
9918
|
muxVideoWithAudio as muxVideoWithAudio2,
|
|
9705
9919
|
resolveConfig as resolveConfig4,
|
|
9706
|
-
runFfmpeg as
|
|
9920
|
+
runFfmpeg as runFfmpeg7
|
|
9707
9921
|
} from "@hyperframes/engine";
|
|
9708
9922
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
9709
9923
|
function cfrRateControlArgs(locked) {
|
|
@@ -9723,7 +9937,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
9723
9937
|
return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
|
|
9724
9938
|
}
|
|
9725
9939
|
if (chunksAreStitchedMp4(planDir, cfr)) {
|
|
9726
|
-
|
|
9940
|
+
const separateAudio = stitchesSeparateAudio(planDir) && audioPath !== null;
|
|
9941
|
+
await writeStitchedMp4(chunkPaths, outputPath, separateAudio ? audioPath : void 0);
|
|
9727
9942
|
return {
|
|
9728
9943
|
outputPath,
|
|
9729
9944
|
durationMs: Date.now() - start,
|
|
@@ -9912,25 +10127,29 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
|
|
|
9912
10127
|
};
|
|
9913
10128
|
}
|
|
9914
10129
|
async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
9915
|
-
const result = await
|
|
10130
|
+
const result = await runFfmpeg7(args, { signal, timeout });
|
|
9916
10131
|
if (!result.success) {
|
|
9917
10132
|
throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
|
|
9918
|
-
error:
|
|
10133
|
+
error: describeFfmpegFailure4(result, timeout),
|
|
9919
10134
|
failureReason: result.failureReason
|
|
9920
10135
|
});
|
|
9921
10136
|
}
|
|
9922
10137
|
}
|
|
9923
10138
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
9924
|
-
|
|
10139
|
+
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
10140
|
+
if (!chunksCarryAudio && !separateAudio) return false;
|
|
9925
10141
|
if (cfr) {
|
|
9926
|
-
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks
|
|
10142
|
+
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
|
|
9927
10143
|
}
|
|
9928
10144
|
return true;
|
|
9929
10145
|
}
|
|
9930
|
-
function
|
|
10146
|
+
function stitchesSeparateAudio(planDir) {
|
|
10147
|
+
return lockedAudio(planDir).separateAudio === true;
|
|
10148
|
+
}
|
|
10149
|
+
function lockedAudio(planDir) {
|
|
9931
10150
|
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
9932
|
-
if (!existsSync18(encoderJsonPath)) return
|
|
9933
|
-
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"))
|
|
10151
|
+
if (!existsSync18(encoderJsonPath)) return {};
|
|
10152
|
+
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
9934
10153
|
}
|
|
9935
10154
|
function readCfrEncoder(planDir, format) {
|
|
9936
10155
|
if (format !== "mp4") {
|
|
@@ -10004,11 +10223,12 @@ import {
|
|
|
10004
10223
|
calculateOptimalWorkers as calculateOptimalWorkers2,
|
|
10005
10224
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
10006
10225
|
closeCaptureSession as closeCaptureSession10,
|
|
10007
|
-
createCaptureSession as
|
|
10226
|
+
createCaptureSession as createCaptureSession10,
|
|
10008
10227
|
createFrameLookupTable as createFrameLookupTable5,
|
|
10009
10228
|
deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
|
|
10010
10229
|
getEncoderPreset as getEncoderPreset6,
|
|
10011
|
-
initializeSession as
|
|
10230
|
+
initializeSession as initializeSession10,
|
|
10231
|
+
measureVideoShownBoxes,
|
|
10012
10232
|
probeBeginFrameLiveness as probeBeginFrameLiveness2,
|
|
10013
10233
|
readWebGlVendorInfoFromCanvas,
|
|
10014
10234
|
resolveConfig as resolveConfig5
|
|
@@ -10037,9 +10257,9 @@ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
|
|
|
10037
10257
|
return { startFrame, endFrame };
|
|
10038
10258
|
}
|
|
10039
10259
|
var distributedCaptureSessionDependencies = {
|
|
10040
|
-
createCaptureSession:
|
|
10260
|
+
createCaptureSession: createCaptureSession10,
|
|
10041
10261
|
assertSwiftShader,
|
|
10042
|
-
initializeSession:
|
|
10262
|
+
initializeSession: initializeSession10,
|
|
10043
10263
|
closeCaptureSession: closeCaptureSession10,
|
|
10044
10264
|
readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
|
|
10045
10265
|
};
|
|
@@ -10230,34 +10450,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10230
10450
|
const framesDir = join27(workDir, "captured-frames");
|
|
10231
10451
|
mkdirSync14(framesDir, { recursive: true });
|
|
10232
10452
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
10233
|
-
const
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
10238
|
-
outputRoot: videoFramesDir,
|
|
10239
|
-
cfg,
|
|
10240
|
-
remoteSources
|
|
10241
|
-
}) : [];
|
|
10453
|
+
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
10454
|
+
const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
10455
|
+
const deferredVideos = planVideos?.extracted.filter((video) => video.deferredRange) ?? [];
|
|
10456
|
+
const shownFrames = planVideos ? shownDeferredFrameIndexes(planVideos, chunkFrameNumbers, chunkFps) : /* @__PURE__ */ new Map();
|
|
10242
10457
|
const remoteVideoIds = new Set(
|
|
10243
10458
|
planVideos?.extracted.filter(
|
|
10244
10459
|
(video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
|
|
10245
10460
|
).map((video) => video.videoId)
|
|
10246
10461
|
);
|
|
10247
|
-
const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
|
|
10248
|
-
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
10249
|
-
planVideos.videos,
|
|
10250
|
-
[
|
|
10251
|
-
...rebuildExtractedFramesFromPlanDir(
|
|
10252
|
-
planDir,
|
|
10253
|
-
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
10254
|
-
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
10255
|
-
),
|
|
10256
|
-
...deferredVideoFrames
|
|
10257
|
-
],
|
|
10258
|
-
resolveRenderFpsConfig(job.config.fps).value
|
|
10259
|
-
) : null;
|
|
10260
|
-
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
10261
10462
|
const fileServer = await createFileServer({
|
|
10262
10463
|
projectDir: compiledDir,
|
|
10263
10464
|
compiledDir,
|
|
@@ -10273,8 +10474,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10273
10474
|
videoCount: planVideos?.videos.length ?? 0,
|
|
10274
10475
|
html: readFileSync12(join27(compiledDir, "index.html"), "utf-8"),
|
|
10275
10476
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
10276
|
-
skipReadinessVideoIds:
|
|
10277
|
-
|
|
10477
|
+
skipReadinessVideoIds: deferredVideos.filter(
|
|
10478
|
+
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
10479
|
+
).map((video) => video.videoId),
|
|
10480
|
+
videoMetadataHints: collectVideoMetadataHints(
|
|
10481
|
+
deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
|
|
10482
|
+
)
|
|
10278
10483
|
});
|
|
10279
10484
|
const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
|
|
10280
10485
|
let session = null;
|
|
@@ -10287,6 +10492,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10287
10492
|
const capturePerfs = [];
|
|
10288
10493
|
const captureAttempts = [];
|
|
10289
10494
|
let forceScreenshotForChunk = encoder.forceScreenshot;
|
|
10495
|
+
let shownBoxes;
|
|
10290
10496
|
try {
|
|
10291
10497
|
if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
|
|
10292
10498
|
const bootStarted = Date.now();
|
|
@@ -10297,6 +10503,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10297
10503
|
cfg
|
|
10298
10504
|
);
|
|
10299
10505
|
sessionBootMs = Date.now() - bootStarted;
|
|
10506
|
+
shownBoxes = await measureVideoShownBoxes(
|
|
10507
|
+
session.page,
|
|
10508
|
+
[...shownFrames.keys()],
|
|
10509
|
+
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
10510
|
+
);
|
|
10300
10511
|
const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
|
|
10301
10512
|
const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
|
|
10302
10513
|
if (browserSelectedScreenshot) {
|
|
@@ -10338,6 +10549,29 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10338
10549
|
session = null;
|
|
10339
10550
|
}
|
|
10340
10551
|
}
|
|
10552
|
+
const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
|
|
10553
|
+
planDir,
|
|
10554
|
+
planVideos,
|
|
10555
|
+
frames: chunkFrameNumbers,
|
|
10556
|
+
fps: chunkFps,
|
|
10557
|
+
outputRoot: videoFramesDir,
|
|
10558
|
+
cfg,
|
|
10559
|
+
remoteSources,
|
|
10560
|
+
shownBoxes
|
|
10561
|
+
}) : [];
|
|
10562
|
+
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
10563
|
+
planVideos.videos,
|
|
10564
|
+
[
|
|
10565
|
+
...rebuildExtractedFramesFromPlanDir(
|
|
10566
|
+
planDir,
|
|
10567
|
+
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
10568
|
+
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
10569
|
+
),
|
|
10570
|
+
...deferredVideoFrames
|
|
10571
|
+
],
|
|
10572
|
+
resolveRenderFpsConfig(job.config.fps).value
|
|
10573
|
+
) : null;
|
|
10574
|
+
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
10341
10575
|
const captureStarted = Date.now();
|
|
10342
10576
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
10343
10577
|
forceScreenshot: forceScreenshotForChunk,
|
|
@@ -10528,6 +10762,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10528
10762
|
encodeStageMs,
|
|
10529
10763
|
workers: chunkWorkerCount,
|
|
10530
10764
|
captureMode,
|
|
10765
|
+
// Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
|
|
10766
|
+
videoShownBoxes: Object.fromEntries(shownBoxes ?? []),
|
|
10531
10767
|
sha256,
|
|
10532
10768
|
outputKind,
|
|
10533
10769
|
producerVersion: plan2.producerVersion,
|
|
@@ -10839,10 +11075,11 @@ function validateIntDimension(field, value) {
|
|
|
10839
11075
|
}
|
|
10840
11076
|
|
|
10841
11077
|
// src/services/distributed/projectHash.ts
|
|
10842
|
-
import { readdirSync as readdirSync10,
|
|
11078
|
+
import { closeSync as closeSync5, openSync as openSync5, readdirSync as readdirSync10, readSync as readSync3, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
|
|
10843
11079
|
import { createHash as createHash6 } from "node:crypto";
|
|
10844
11080
|
import { join as join29 } from "node:path";
|
|
10845
11081
|
var SITE_ID_SCHEME = "site-v2\0";
|
|
11082
|
+
var READ_CHUNK_BYTES = 256 * 1024;
|
|
10846
11083
|
function listSiteFiles(projectDir) {
|
|
10847
11084
|
const files = [];
|
|
10848
11085
|
const ancestors = /* @__PURE__ */ new Set([realpathSync2(projectDir)]);
|
|
@@ -10875,10 +11112,19 @@ function listSiteFiles(projectDir) {
|
|
|
10875
11112
|
}
|
|
10876
11113
|
function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
|
|
10877
11114
|
const hash = createHash6("sha256").update(SITE_ID_SCHEME);
|
|
11115
|
+
const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
10878
11116
|
for (const rel of files) {
|
|
10879
11117
|
hash.update(rel);
|
|
10880
11118
|
hash.update("\0");
|
|
10881
|
-
|
|
11119
|
+
const fd = openSync5(join29(projectDir, rel), "r");
|
|
11120
|
+
try {
|
|
11121
|
+
let read;
|
|
11122
|
+
while ((read = readSync3(fd, chunk, 0, chunk.length, null)) > 0) {
|
|
11123
|
+
hash.update(chunk.subarray(0, read));
|
|
11124
|
+
}
|
|
11125
|
+
} finally {
|
|
11126
|
+
closeSync5(fd);
|
|
11127
|
+
}
|
|
10882
11128
|
}
|
|
10883
11129
|
return hash.digest("hex").slice(0, 16);
|
|
10884
11130
|
}
|
|
@@ -10926,6 +11172,7 @@ export {
|
|
|
10926
11172
|
assemble,
|
|
10927
11173
|
assembleV2,
|
|
10928
11174
|
buildChunkSlices,
|
|
11175
|
+
buildPlanAudio,
|
|
10929
11176
|
buildStitchedHeader,
|
|
10930
11177
|
chunksAreStitchedMp4,
|
|
10931
11178
|
createPlanV2FromExecutionPlan,
|
|
@@ -10943,6 +11190,7 @@ export {
|
|
|
10943
11190
|
plan,
|
|
10944
11191
|
planV2,
|
|
10945
11192
|
planV2WithPublisher,
|
|
11193
|
+
plansSeparateAudio,
|
|
10946
11194
|
publishPlanV2FromExecutionPlan,
|
|
10947
11195
|
publishPlanV2FromV1,
|
|
10948
11196
|
readMp4Layout,
|
|
@@ -10955,6 +11203,7 @@ export {
|
|
|
10955
11203
|
renderChunkV2,
|
|
10956
11204
|
resolveChunkPlan,
|
|
10957
11205
|
resolvePlanAudioPath,
|
|
11206
|
+
stitchesSeparateAudio,
|
|
10958
11207
|
validateDistributedRenderConfig,
|
|
10959
11208
|
validatePlanV2MaterializedTarget,
|
|
10960
11209
|
validateVariablesPayload
|