@hyperframes/producer 0.8.186 → 0.8.188
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/README.md +26 -30
- package/dist/distributed.js +1298 -1332
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +65 -64
- package/dist/index.js +1666 -1667
- package/dist/public-server.js +1589 -1567
- package/dist/server.d.ts +1 -0
- package/dist/services/distributed/assemble.d.ts +1 -1
- package/dist/services/distributed/chunkFrames.d.ts +1 -1
- package/dist/services/distributed/plan.d.ts +2 -1
- package/dist/services/render/segmentManifest.d.ts +1 -0
- package/dist/services/render/stages/assembleStage.d.ts +1 -0
- package/dist/services/render/stages/audioStage.d.ts +6 -4
- package/dist/services/render/stages/captureStage.d.ts +3 -8
- package/dist/services/render/stages/freezePlan.d.ts +2 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
mkdirSync as mkdirSync9,
|
|
6
6
|
readFileSync as readFileSync8,
|
|
7
7
|
readdirSync as readdirSync5,
|
|
8
|
-
rmSync as
|
|
8
|
+
rmSync as rmSync10,
|
|
9
9
|
statSync as statSync6,
|
|
10
10
|
writeFileSync as writeFileSync5,
|
|
11
11
|
copyFileSync as copyFileSync4,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
augmentProtocolTimeoutError,
|
|
56
56
|
augmentPageNavigationTimeoutError
|
|
57
57
|
} from "@hyperframes/engine";
|
|
58
|
-
import { join as
|
|
58
|
+
import { join as join25, dirname as dirname10, resolve as resolve7 } from "path";
|
|
59
59
|
import { totalmem as totalmem2 } from "node:os";
|
|
60
60
|
import { randomUUID } from "crypto";
|
|
61
61
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -4336,6 +4336,7 @@ function computeSegmentPlanHash(input) {
|
|
|
4336
4336
|
input.useGpu ? "gpu" : "cpu",
|
|
4337
4337
|
`${input.outputWidth}x${input.outputHeight}`,
|
|
4338
4338
|
input.motionBlur,
|
|
4339
|
+
input.pageMotionBlur ?? "",
|
|
4339
4340
|
input.captureQuality === void 0 ? "" : String(input.captureQuality)
|
|
4340
4341
|
].join(" ");
|
|
4341
4342
|
return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
|
|
@@ -7193,7 +7194,9 @@ async function discoverMediaFromBrowser(page) {
|
|
|
7193
7194
|
const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
|
|
7194
7195
|
const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
|
|
7195
7196
|
if (!id) return;
|
|
7196
|
-
const
|
|
7197
|
+
const ownSrc = htmlEl.getAttribute("src");
|
|
7198
|
+
const blankSrc = ownSrc !== null && ownSrc.trim() === "" && !htmlEl.querySelector("source[src]");
|
|
7199
|
+
const src = blankSrc ? "" : htmlEl.currentSrc || htmlEl.src || ownSrc || "";
|
|
7197
7200
|
const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
|
|
7198
7201
|
const endRaw = htmlEl.getAttribute("data-end");
|
|
7199
7202
|
const durationRaw = htmlEl.getAttribute("data-duration");
|
|
@@ -8696,771 +8699,1209 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
8696
8699
|
}
|
|
8697
8700
|
|
|
8698
8701
|
// src/services/render/stages/audioStage.ts
|
|
8699
|
-
import { join as
|
|
8702
|
+
import { join as join18 } from "node:path";
|
|
8703
|
+
import { rmSync as rmSync8 } from "node:fs";
|
|
8700
8704
|
import {
|
|
8701
8705
|
MIXED_AUDIO_FILENAME,
|
|
8702
8706
|
audioFailureDiagnostics,
|
|
8703
8707
|
processCompositionAudio
|
|
8704
8708
|
} from "@hyperframes/engine";
|
|
8705
|
-
|
|
8706
|
-
|
|
8709
|
+
|
|
8710
|
+
// src/services/render/audioPadTrim.ts
|
|
8711
|
+
import { spawn } from "node:child_process";
|
|
8712
|
+
import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSync7 } from "node:fs";
|
|
8713
|
+
import { dirname as dirname7, join as join17 } from "node:path";
|
|
8714
|
+
import {
|
|
8715
|
+
DEFAULT_CONFIG,
|
|
8716
|
+
describeFfmpegFailure as describeFfmpegFailure2,
|
|
8717
|
+
encodeAac,
|
|
8718
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
8719
|
+
getFfprobeBinary,
|
|
8720
|
+
ManagedChildProcess,
|
|
8721
|
+
runFfmpeg as runFfmpeg4,
|
|
8722
|
+
trackChildProcess
|
|
8723
|
+
} from "@hyperframes/engine";
|
|
8724
|
+
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
8725
|
+
|
|
8726
|
+
// src/services/render/audioTruePeak.ts
|
|
8727
|
+
import {
|
|
8728
|
+
aacSegmentCount,
|
|
8729
|
+
describeFfmpegFailure,
|
|
8730
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
8731
|
+
runFfmpeg as runFfmpeg3
|
|
8732
|
+
} from "@hyperframes/engine";
|
|
8733
|
+
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
8734
|
+
function checkedDbfs(value) {
|
|
8735
|
+
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
8736
|
+
throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
|
|
8737
|
+
}
|
|
8738
|
+
return value;
|
|
8707
8739
|
}
|
|
8708
|
-
async function
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
assertNotAborted,
|
|
8722
|
-
log
|
|
8723
|
-
} = input;
|
|
8724
|
-
const stage3Start = Date.now();
|
|
8725
|
-
const audioOutputPath = join17(workDir, MIXED_AUDIO_FILENAME);
|
|
8726
|
-
let hasAudio = false;
|
|
8727
|
-
let audioError;
|
|
8728
|
-
let audioFailures;
|
|
8729
|
-
if (audios.length > 0) {
|
|
8730
|
-
let audioResult;
|
|
8731
|
-
try {
|
|
8732
|
-
audioResult = await processCompositionAudio(
|
|
8733
|
-
audios,
|
|
8734
|
-
projectDir,
|
|
8735
|
-
join17(workDir, "audio-work"),
|
|
8736
|
-
audioOutputPath,
|
|
8737
|
-
duration,
|
|
8738
|
-
abortSignal,
|
|
8739
|
-
{
|
|
8740
|
-
ffmpegProcessTimeout,
|
|
8741
|
-
audioGain,
|
|
8742
|
-
masterFxChain,
|
|
8743
|
-
urlByAbsentLocalPath,
|
|
8744
|
-
keepEncoderInputAt
|
|
8745
|
-
},
|
|
8746
|
-
compiledDir
|
|
8747
|
-
);
|
|
8748
|
-
} catch (err) {
|
|
8749
|
-
assertNotAborted();
|
|
8750
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
8751
|
-
return {
|
|
8752
|
-
audioOutputPath,
|
|
8753
|
-
hasAudio: false,
|
|
8754
|
-
audioProcessMs: Date.now() - stage3Start,
|
|
8755
|
-
audioError: detail,
|
|
8756
|
-
// Synthesised rather than left undefined. The warning policy reads
|
|
8757
|
-
// owner, retryability, reason and stage off this list, so reporting the
|
|
8758
|
-
// FATAL failure — the one that took the whole mix down — with an empty
|
|
8759
|
-
// one gave it strictly less classification than a single dropped track
|
|
8760
|
-
// gets, which is the opposite of what this stage exists to do.
|
|
8761
|
-
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
|
8762
|
-
// and this is the FX render, which is none of them.
|
|
8763
|
-
audioFailures: [
|
|
8764
|
-
{
|
|
8765
|
-
stage: "internal",
|
|
8766
|
-
reason: "internal",
|
|
8767
|
-
owner: "system",
|
|
8768
|
-
retryable: false,
|
|
8769
|
-
detail: detail.slice(0, 2e3)
|
|
8770
|
-
}
|
|
8771
|
-
]
|
|
8772
|
-
};
|
|
8773
|
-
}
|
|
8774
|
-
assertNotAborted();
|
|
8775
|
-
const interrupted = audioResult.failures?.find(
|
|
8776
|
-
(failure) => failure.reason === "external_interruption"
|
|
8777
|
-
);
|
|
8778
|
-
if (interrupted) {
|
|
8779
|
-
throw encoderFailureError("Audio processing failed", {
|
|
8780
|
-
error: interrupted.detail,
|
|
8781
|
-
failureReason: "external_interruption",
|
|
8782
|
-
audioFailures: audioFailureDiagnostics(triggerFirst(audioResult.failures, interrupted))
|
|
8783
|
-
});
|
|
8784
|
-
}
|
|
8785
|
-
hasAudio = audioResult.success;
|
|
8786
|
-
audioFailures = audioResult.failures;
|
|
8787
|
-
if (!hasAudio) {
|
|
8788
|
-
audioError = audioResult.error ?? "audio mix failed for an unknown reason";
|
|
8789
|
-
for (const failure of audioResult.failures ?? []) {
|
|
8790
|
-
log?.warn("Audio track failed", {
|
|
8791
|
-
elementId: failure.elementId,
|
|
8792
|
-
stage: failure.stage,
|
|
8793
|
-
reason: failure.reason,
|
|
8794
|
-
owner: failure.owner,
|
|
8795
|
-
retryable: failure.retryable,
|
|
8796
|
-
detail: failure.detail
|
|
8797
|
-
});
|
|
8798
|
-
}
|
|
8740
|
+
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
8741
|
+
let firstBlock;
|
|
8742
|
+
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
8743
|
+
let partialLine = "";
|
|
8744
|
+
const readLines = (chunk) => {
|
|
8745
|
+
const lines = (partialLine + chunk).split(/\r|\n/);
|
|
8746
|
+
partialLine = lines.pop() ?? "";
|
|
8747
|
+
for (const line of lines) {
|
|
8748
|
+
const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
|
|
8749
|
+
if (!ftpk) continue;
|
|
8750
|
+
const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
|
|
8751
|
+
if (firstBlock === void 0) firstBlock = block;
|
|
8752
|
+
else laterBlocks = Math.max(laterBlocks, block);
|
|
8799
8753
|
}
|
|
8754
|
+
};
|
|
8755
|
+
const result = await runFfmpeg3(
|
|
8756
|
+
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
8757
|
+
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
8758
|
+
);
|
|
8759
|
+
if (!result.success) {
|
|
8760
|
+
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
8800
8761
|
}
|
|
8801
|
-
const
|
|
8762
|
+
const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
8802
8763
|
return {
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
audioError,
|
|
8807
|
-
audioFailures
|
|
8764
|
+
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
8765
|
+
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
8766
|
+
laterBlocks: checkedDbfs(laterBlocks)
|
|
8808
8767
|
};
|
|
8809
8768
|
}
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8769
|
+
async function measureTruePeakDbfs(audioPath, options) {
|
|
8770
|
+
const { timeoutMs, signal } = options;
|
|
8771
|
+
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
8772
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
8773
|
+
const block = 4800;
|
|
8774
|
+
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
8775
|
+
if (count === 1) return wholeFile();
|
|
8776
|
+
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
8777
|
+
const peers = new AbortController();
|
|
8778
|
+
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
8779
|
+
const scans = await Promise.all(
|
|
8780
|
+
Array.from({ length: count }, (_, k) => {
|
|
8781
|
+
const start = k === 0 ? 0 : cut(k) - block;
|
|
8782
|
+
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
8783
|
+
return scanTruePeak(
|
|
8784
|
+
audioPath,
|
|
8785
|
+
`atrim=start_sample=${start}${end},`,
|
|
8786
|
+
timeoutMs,
|
|
8787
|
+
segmentSignal
|
|
8788
|
+
);
|
|
8789
|
+
})
|
|
8790
|
+
).catch((error) => {
|
|
8791
|
+
peers.abort();
|
|
8792
|
+
throw error;
|
|
8793
|
+
});
|
|
8794
|
+
const last = scans[count - 1];
|
|
8795
|
+
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
8796
|
+
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
8797
|
+
return wholeFile();
|
|
8830
8798
|
}
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
var
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
|
|
8840
|
-
rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
|
|
8841
|
-
) : rawBytes;
|
|
8799
|
+
|
|
8800
|
+
// src/services/render/audioPadTrim.ts
|
|
8801
|
+
var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
8802
|
+
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
8803
|
+
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
8804
|
+
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
8805
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
8806
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
8842
8807
|
}
|
|
8843
|
-
function
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8808
|
+
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
8809
|
+
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
8810
|
+
const targetSec = formatSeconds(targetDurationSeconds);
|
|
8811
|
+
if (Math.abs(delta) < AUDIO_DURATION_TOLERANCE_SECONDS) {
|
|
8812
|
+
return {
|
|
8813
|
+
operation: "copy",
|
|
8814
|
+
steps: [{ kind: "copy", args: ["-i", audioPath, "-c:a", "copy", "-y", outputPath] }],
|
|
8815
|
+
cleanupPaths: []
|
|
8816
|
+
};
|
|
8849
8817
|
}
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8818
|
+
if (delta > 0) {
|
|
8819
|
+
return {
|
|
8820
|
+
operation: "pad",
|
|
8821
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
8822
|
+
cleanupPaths: []
|
|
8823
|
+
};
|
|
8856
8824
|
}
|
|
8857
|
-
return {
|
|
8825
|
+
return {
|
|
8826
|
+
operation: "trim",
|
|
8827
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
8828
|
+
cleanupPaths: []
|
|
8829
|
+
};
|
|
8858
8830
|
}
|
|
8859
|
-
function
|
|
8860
|
-
|
|
8861
|
-
if (workerCount > 1 && oneWorker.enabled) {
|
|
8862
|
-
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.`;
|
|
8863
|
-
}
|
|
8864
|
-
switch (oneWorker.reason) {
|
|
8865
|
-
case "format_excluded":
|
|
8866
|
-
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
8867
|
-
case "disabled_by_config":
|
|
8868
|
-
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
8869
|
-
case "duration_cap":
|
|
8870
|
-
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.";
|
|
8871
|
-
default:
|
|
8872
|
-
return "Free up disk space.";
|
|
8873
|
-
}
|
|
8831
|
+
function formatSeconds(sec) {
|
|
8832
|
+
return sec.toFixed(6);
|
|
8874
8833
|
}
|
|
8875
|
-
function
|
|
8876
|
-
const
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
);
|
|
8834
|
+
function sanitizeProbeFailure(reason, paths) {
|
|
8835
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
8836
|
+
const keepFfmpegsLastLineUncapped = Number.POSITIVE_INFINITY;
|
|
8837
|
+
return redactTelemetryString3(redactKnownPaths(message, paths), keepFfmpegsLastLineUncapped);
|
|
8880
8838
|
}
|
|
8881
|
-
function
|
|
8882
|
-
const
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
);
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8839
|
+
async function padOrTrimAudioToVideoFrameCount(input) {
|
|
8840
|
+
const probeVideo = input.probeVideoFrameInfo ?? ((videoPath) => defaultProbeVideoFrameInfo(videoPath, input.signal));
|
|
8841
|
+
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
8842
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
8843
|
+
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
8844
|
+
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
8845
|
+
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
8846
|
+
const [videoResult, audioResult] = await Promise.allSettled([
|
|
8847
|
+
probeVideo(input.videoPath),
|
|
8848
|
+
probeAudio(input.audioPath, input.signal)
|
|
8849
|
+
]);
|
|
8850
|
+
const probePaths = [input.videoPath, input.audioPath, input.outputPath];
|
|
8851
|
+
if (videoResult.status === "rejected") {
|
|
8852
|
+
return failResult(
|
|
8853
|
+
input.outputPath,
|
|
8854
|
+
0,
|
|
8855
|
+
audioResult.status === "fulfilled" ? audioResult.value.durationSeconds : 0,
|
|
8856
|
+
`audioPadTrim: failed to probe video: ${sanitizeProbeFailure(
|
|
8857
|
+
videoResult.reason,
|
|
8858
|
+
probePaths
|
|
8859
|
+
)}`
|
|
8860
|
+
);
|
|
8861
|
+
}
|
|
8862
|
+
if (audioResult.status === "rejected") {
|
|
8863
|
+
return failResult(
|
|
8864
|
+
input.outputPath,
|
|
8865
|
+
0,
|
|
8866
|
+
0,
|
|
8867
|
+
`audioPadTrim: failed to probe audio: ${sanitizeProbeFailure(
|
|
8868
|
+
audioResult.reason,
|
|
8869
|
+
probePaths
|
|
8870
|
+
)}`
|
|
8871
|
+
);
|
|
8872
|
+
}
|
|
8873
|
+
const videoInfo = videoResult.value;
|
|
8874
|
+
const audioInfo = audioResult.value;
|
|
8875
|
+
if (!Number.isFinite(videoInfo.frameCount) || videoInfo.frameCount <= 0 || !Number.isFinite(videoInfo.fpsNum) || videoInfo.fpsNum <= 0 || !Number.isFinite(videoInfo.fpsDen) || videoInfo.fpsDen <= 0) {
|
|
8876
|
+
return failResult(
|
|
8877
|
+
input.outputPath,
|
|
8878
|
+
0,
|
|
8879
|
+
audioInfo.durationSeconds,
|
|
8880
|
+
`audioPadTrim: invalid video frame info: ${JSON.stringify(videoInfo)}`
|
|
8881
|
+
);
|
|
8882
|
+
}
|
|
8883
|
+
const targetDurationSeconds = videoInfo.frameCount * videoInfo.fpsDen / videoInfo.fpsNum;
|
|
8884
|
+
const plan2 = buildPadTrimAudioPlan(
|
|
8885
|
+
input.audioPath,
|
|
8886
|
+
input.outputPath,
|
|
8887
|
+
audioInfo.durationSeconds,
|
|
8888
|
+
targetDurationSeconds
|
|
8895
8889
|
);
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8890
|
+
try {
|
|
8891
|
+
for (const step of plan2.steps) {
|
|
8892
|
+
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
8893
|
+
if (!ffmpegResult.success) {
|
|
8894
|
+
return {
|
|
8895
|
+
success: false,
|
|
8896
|
+
outputPath: input.outputPath,
|
|
8897
|
+
targetDurationSeconds,
|
|
8898
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
8899
|
+
operation: plan2.operation,
|
|
8900
|
+
error: ffmpegResult.error,
|
|
8901
|
+
failureReason: ffmpegResult.failureReason
|
|
8902
|
+
};
|
|
8903
|
+
}
|
|
8909
8904
|
}
|
|
8905
|
+
} catch (err) {
|
|
8906
|
+
return {
|
|
8907
|
+
success: false,
|
|
8908
|
+
outputPath: input.outputPath,
|
|
8909
|
+
targetDurationSeconds,
|
|
8910
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
8911
|
+
operation: plan2.operation,
|
|
8912
|
+
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
8913
|
+
};
|
|
8914
|
+
} finally {
|
|
8915
|
+
for (const path of plan2.cleanupPaths) rmSync7(path, { force: true });
|
|
8910
8916
|
}
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8917
|
+
let loweredDb = 0;
|
|
8918
|
+
if (probeTruePeak) {
|
|
8919
|
+
const correction = await enforceAacTruePeak({
|
|
8920
|
+
audioPath: input.outputPath,
|
|
8921
|
+
targetDurationSeconds,
|
|
8922
|
+
signal: input.signal,
|
|
8923
|
+
probeTruePeak,
|
|
8924
|
+
encode
|
|
8925
|
+
});
|
|
8926
|
+
if (!correction.success) {
|
|
8927
|
+
return {
|
|
8928
|
+
success: false,
|
|
8929
|
+
outputPath: input.outputPath,
|
|
8930
|
+
targetDurationSeconds,
|
|
8931
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
8932
|
+
operation: plan2.operation,
|
|
8933
|
+
error: correction.error,
|
|
8934
|
+
failureReason: correction.failureReason
|
|
8935
|
+
};
|
|
8927
8936
|
}
|
|
8937
|
+
loweredDb = correction.loweredDb ?? 0;
|
|
8938
|
+
}
|
|
8939
|
+
return {
|
|
8940
|
+
success: true,
|
|
8941
|
+
outputPath: input.outputPath,
|
|
8942
|
+
targetDurationSeconds,
|
|
8943
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
8944
|
+
operation: plan2.operation,
|
|
8945
|
+
...loweredDb > 0 ? { audioLoweredDb: loweredDb } : {}
|
|
8928
8946
|
};
|
|
8929
8947
|
}
|
|
8930
|
-
async function
|
|
8931
|
-
const
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
|
|
8953
|
-
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
8954
|
-
if (frameRange !== void 0) {
|
|
8955
|
-
if (!Number.isFinite(frameRange.startFrame) || !Number.isFinite(frameRange.endFrame) || frameRange.startFrame < 0 || frameRange.endFrame <= frameRange.startFrame) {
|
|
8956
|
-
throw new Error(
|
|
8957
|
-
`[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
|
|
8948
|
+
async function constrainAacTruePeak(input) {
|
|
8949
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
8950
|
+
return enforceAacTruePeak({
|
|
8951
|
+
audioPath: input.audioPath,
|
|
8952
|
+
targetDurationSeconds: input.targetDurationSeconds,
|
|
8953
|
+
signal: input.signal,
|
|
8954
|
+
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
8955
|
+
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
8956
|
+
});
|
|
8957
|
+
}
|
|
8958
|
+
async function enforceAacTruePeak(input) {
|
|
8959
|
+
let scratchDir;
|
|
8960
|
+
try {
|
|
8961
|
+
scratchDir = mkdtempSync3(join17(dirname7(input.audioPath), ".true-peak-"));
|
|
8962
|
+
const correctedPath = join17(scratchDir, "audio.m4a");
|
|
8963
|
+
const measurements = [];
|
|
8964
|
+
let attenuationDb = 0;
|
|
8965
|
+
let measuredPath = input.audioPath;
|
|
8966
|
+
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
8967
|
+
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
8968
|
+
measurements.push(
|
|
8969
|
+
`pass ${pass}: ${truePeakDbfs.toFixed(3)} dBFS at ${attenuationDb.toFixed(3)} dB attenuation`
|
|
8958
8970
|
);
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8971
|
+
if (Number.isNaN(truePeakDbfs) || truePeakDbfs === Number.POSITIVE_INFINITY) {
|
|
8972
|
+
return { success: false, error: "audioPadTrim: FFmpeg reported an invalid true peak" };
|
|
8973
|
+
}
|
|
8974
|
+
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
8975
|
+
if (measuredPath !== correctedPath) return { success: true };
|
|
8976
|
+
renameSync4(correctedPath, input.audioPath);
|
|
8977
|
+
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
8978
|
+
}
|
|
8979
|
+
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
8980
|
+
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
8981
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
|
|
8982
|
+
const result = await input.encode(
|
|
8983
|
+
input.audioPath,
|
|
8984
|
+
correctedPath,
|
|
8985
|
+
input.targetDurationSeconds,
|
|
8986
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
8964
8987
|
);
|
|
8988
|
+
if (!result.success) {
|
|
8989
|
+
return {
|
|
8990
|
+
success: false,
|
|
8991
|
+
error: sanitizeProbeFailure(
|
|
8992
|
+
result.error ?? "audioPadTrim: failed to constrain AAC true peak",
|
|
8993
|
+
[input.audioPath, correctedPath]
|
|
8994
|
+
),
|
|
8995
|
+
failureReason: result.failureReason
|
|
8996
|
+
};
|
|
8997
|
+
}
|
|
8998
|
+
measuredPath = correctedPath;
|
|
8965
8999
|
}
|
|
9000
|
+
return {
|
|
9001
|
+
success: false,
|
|
9002
|
+
error: `audioPadTrim: AAC true peak remained above ${AAC_DELIVERY_TRUE_PEAK_DBFS} dBFS after ${MAX_TRUE_PEAK_CORRECTION_PASSES} correction passes; ${measurements.join("; ")}`
|
|
9003
|
+
};
|
|
9004
|
+
} catch (err) {
|
|
9005
|
+
return {
|
|
9006
|
+
success: false,
|
|
9007
|
+
error: `audioPadTrim: failed to validate AAC true peak: ${sanitizeProbeFailure(
|
|
9008
|
+
err,
|
|
9009
|
+
scratchDir ? [input.audioPath, scratchDir] : [input.audioPath]
|
|
9010
|
+
)}`
|
|
9011
|
+
};
|
|
9012
|
+
} finally {
|
|
9013
|
+
if (scratchDir) rmSync7(scratchDir, { recursive: true, force: true });
|
|
8966
9014
|
}
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
+
}
|
|
9016
|
+
function failResult(outputPath, target, source, error) {
|
|
9017
|
+
return {
|
|
9018
|
+
success: false,
|
|
9019
|
+
outputPath,
|
|
9020
|
+
targetDurationSeconds: target,
|
|
9021
|
+
sourceDurationSeconds: source,
|
|
9022
|
+
operation: "copy",
|
|
9023
|
+
error
|
|
9024
|
+
};
|
|
9025
|
+
}
|
|
9026
|
+
async function defaultProbeVideoFrameInfo(videoPath, signal) {
|
|
9027
|
+
const fastInfo = await runFfprobeJson(
|
|
9028
|
+
[
|
|
9029
|
+
"-v",
|
|
9030
|
+
"error",
|
|
9031
|
+
"-select_streams",
|
|
9032
|
+
"v:0",
|
|
9033
|
+
"-show_entries",
|
|
9034
|
+
"stream=nb_frames,r_frame_rate",
|
|
9035
|
+
"-of",
|
|
9036
|
+
"json",
|
|
9037
|
+
"--",
|
|
9038
|
+
videoPath
|
|
9039
|
+
],
|
|
9040
|
+
signal
|
|
9041
|
+
);
|
|
9042
|
+
let stream = fastInfo.streams?.[0];
|
|
9043
|
+
const fastCount = Number(stream?.nb_frames);
|
|
9044
|
+
if (stream && Number.isFinite(fastCount) && fastCount > 0) {
|
|
9045
|
+
return { frameCount: fastCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
9046
|
+
}
|
|
9047
|
+
const slowInfo = await runFfprobeJson(
|
|
9048
|
+
[
|
|
9049
|
+
"-v",
|
|
9050
|
+
"error",
|
|
9051
|
+
"-select_streams",
|
|
9052
|
+
"v:0",
|
|
9053
|
+
"-count_packets",
|
|
9054
|
+
"-show_entries",
|
|
9055
|
+
"stream=nb_read_packets,r_frame_rate",
|
|
9056
|
+
"-of",
|
|
9057
|
+
"json",
|
|
9058
|
+
"--",
|
|
9059
|
+
videoPath
|
|
9060
|
+
],
|
|
9061
|
+
signal
|
|
9062
|
+
);
|
|
9063
|
+
stream = slowInfo.streams?.[0];
|
|
9064
|
+
if (!stream) throw new Error(`ffprobe found no video stream in ${videoPath}`);
|
|
9065
|
+
const slowCount = Number(stream.nb_read_packets);
|
|
9066
|
+
if (!Number.isFinite(slowCount) || slowCount <= 0) {
|
|
9067
|
+
throw new Error(`ffprobe returned no frame count: ${JSON.stringify(stream)}`);
|
|
9068
|
+
}
|
|
9069
|
+
return { frameCount: slowCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
9070
|
+
}
|
|
9071
|
+
function parseFrameRate(rate) {
|
|
9072
|
+
const [n, d] = rate.split("/");
|
|
9073
|
+
const fpsNum = Number(n);
|
|
9074
|
+
const fpsDen = d === void 0 ? 1 : Number(d);
|
|
9075
|
+
if (!Number.isFinite(fpsNum) || !Number.isFinite(fpsDen) || fpsNum <= 0 || fpsDen <= 0) {
|
|
9076
|
+
throw new Error(`Invalid r_frame_rate: ${JSON.stringify(rate)}`);
|
|
9077
|
+
}
|
|
9078
|
+
return { fpsNum, fpsDen };
|
|
9079
|
+
}
|
|
9080
|
+
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
9081
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
9082
|
+
return {
|
|
9083
|
+
durationSeconds: metadata.durationSeconds,
|
|
9084
|
+
sampleRate: metadata.sampleRate,
|
|
9085
|
+
channels: metadata.channels,
|
|
9086
|
+
audioCodec: metadata.audioCodec
|
|
9087
|
+
};
|
|
9088
|
+
}
|
|
9089
|
+
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
9090
|
+
return toStepResult(await runFfmpeg4(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
9091
|
+
}
|
|
9092
|
+
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
9093
|
+
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
9094
|
+
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
9095
|
+
return toStepResult(
|
|
9096
|
+
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
9097
|
+
timeoutMs
|
|
9098
|
+
);
|
|
9099
|
+
};
|
|
9100
|
+
}
|
|
9101
|
+
function toStepResult(result, timeoutMs) {
|
|
9102
|
+
if (result.success) return { success: true };
|
|
9103
|
+
return {
|
|
9104
|
+
success: false,
|
|
9105
|
+
error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
|
|
9106
|
+
failureReason: result.failureReason
|
|
9107
|
+
};
|
|
9108
|
+
}
|
|
9109
|
+
async function runFfprobeJson(args, signal) {
|
|
9110
|
+
if (!args.includes("--")) {
|
|
9111
|
+
throw new Error('[audioPadTrim] ffprobe args must terminate options with "--".');
|
|
9112
|
+
}
|
|
9113
|
+
const proc = spawn(getFfprobeBinary(), args, {
|
|
9114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9115
|
+
windowsHide: true
|
|
9116
|
+
});
|
|
9117
|
+
trackChildProcess(proc);
|
|
9118
|
+
let stdout = "";
|
|
9119
|
+
proc.stdout.on("data", (data) => {
|
|
9120
|
+
stdout += data.toString();
|
|
9121
|
+
});
|
|
9122
|
+
const managed = new ManagedChildProcess(proc, {
|
|
9123
|
+
signal,
|
|
9124
|
+
deadlineAtMs: Date.now() + 3e4
|
|
9125
|
+
});
|
|
9126
|
+
const outcome = await managed.wait();
|
|
9127
|
+
if (outcome.reason === "spawn_error") {
|
|
9128
|
+
if (outcome.error?.code === "ENOENT") {
|
|
9129
|
+
throw new Error("[audioPadTrim] ffprobe not found. Please install FFmpeg.");
|
|
9015
9130
|
}
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9131
|
+
throw outcome.error ?? new Error(outcome.stderr);
|
|
9132
|
+
}
|
|
9133
|
+
if (outcome.reason !== "exit" || outcome.exitCode !== 0) {
|
|
9134
|
+
const probed = args[args.length - 1];
|
|
9135
|
+
const scrubbed = redactKnownPaths(outcome.stderr, probed === void 0 ? [] : [probed]);
|
|
9136
|
+
throw new Error(`ffprobe ${outcome.reason}: ${redactTelemetryString3(scrubbed, 2e3)}`);
|
|
9137
|
+
}
|
|
9138
|
+
try {
|
|
9139
|
+
return JSON.parse(stdout);
|
|
9140
|
+
} catch (err) {
|
|
9141
|
+
throw new Error(`Failed to parse ffprobe output: ${err.message}`);
|
|
9142
|
+
}
|
|
9143
|
+
}
|
|
9144
|
+
|
|
9145
|
+
// src/services/render/stages/audioStage.ts
|
|
9146
|
+
function assertAudioNotInterrupted(failures) {
|
|
9147
|
+
const interrupted = failures?.find((failure) => failure.reason === "external_interruption");
|
|
9148
|
+
if (!interrupted) return;
|
|
9149
|
+
throw encoderFailureError("Audio processing failed", {
|
|
9150
|
+
error: interrupted.detail,
|
|
9151
|
+
failureReason: "external_interruption",
|
|
9152
|
+
audioFailures: audioFailureDiagnostics([
|
|
9153
|
+
interrupted,
|
|
9154
|
+
...(failures ?? []).filter((failure) => failure !== interrupted)
|
|
9155
|
+
])
|
|
9156
|
+
});
|
|
9157
|
+
}
|
|
9158
|
+
async function runAudioStage(input) {
|
|
9159
|
+
const stageStart = Date.now();
|
|
9160
|
+
const pcmSourcePath = join18(input.workDir, "audio-source.wav");
|
|
9161
|
+
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
9162
|
+
try {
|
|
9163
|
+
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
9164
|
+
let audioLoweredDb;
|
|
9165
|
+
if (result.hasAudio) {
|
|
9166
|
+
const peak = await constrainAacTruePeak({
|
|
9167
|
+
audioPath: result.audioOutputPath,
|
|
9168
|
+
sourcePath: pcmSourcePath,
|
|
9169
|
+
targetDurationSeconds: duration,
|
|
9170
|
+
signal: input.abortSignal,
|
|
9171
|
+
timeoutMs: input.ffmpegProcessTimeout
|
|
9172
|
+
});
|
|
9173
|
+
if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
|
|
9174
|
+
audioLoweredDb = peak.loweredDb;
|
|
9021
9175
|
}
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
captureOptions,
|
|
9026
|
-
captureCfg,
|
|
9027
|
-
checkDiskProjection
|
|
9028
|
-
);
|
|
9029
|
-
probeSession = null;
|
|
9030
|
-
lastBrowserConsole = sequential.lastBrowserConsole;
|
|
9031
|
-
captureBeyondViewport = sequential.captureBeyondViewport;
|
|
9176
|
+
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9177
|
+
} finally {
|
|
9178
|
+
rmSync8(pcmSourcePath, { force: true });
|
|
9032
9179
|
}
|
|
9033
|
-
return { workerCount, probeSession, lastBrowserConsole, captureBeyondViewport };
|
|
9034
9180
|
}
|
|
9035
|
-
async function
|
|
9181
|
+
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
9036
9182
|
const {
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9183
|
+
projectDir,
|
|
9184
|
+
workDir,
|
|
9185
|
+
compiledDir,
|
|
9186
|
+
ffmpegProcessTimeout,
|
|
9187
|
+
audioGain,
|
|
9188
|
+
audios,
|
|
9189
|
+
masterFxChain,
|
|
9190
|
+
urlByAbsentLocalPath,
|
|
9042
9191
|
abortSignal,
|
|
9043
9192
|
assertNotAborted,
|
|
9044
|
-
|
|
9193
|
+
log
|
|
9045
9194
|
} = input;
|
|
9046
|
-
|
|
9047
|
-
let
|
|
9048
|
-
let
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
let transientRetriesUsed = 0;
|
|
9053
|
-
let resumeFrom = 0;
|
|
9054
|
-
while (true) {
|
|
9055
|
-
const videoInjector = createRenderVideoFrameInjector();
|
|
9056
|
-
const session = probeSession ?? await createCaptureSession5(
|
|
9057
|
-
fileServer.url,
|
|
9058
|
-
framesDir,
|
|
9059
|
-
captureOptions,
|
|
9060
|
-
videoInjector,
|
|
9061
|
-
captureCfg
|
|
9062
|
-
);
|
|
9063
|
-
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
9195
|
+
const audioOutputPath = join18(workDir, MIXED_AUDIO_FILENAME);
|
|
9196
|
+
let hasAudio = false;
|
|
9197
|
+
let audioError;
|
|
9198
|
+
let audioFailures;
|
|
9199
|
+
if (audios.length > 0) {
|
|
9200
|
+
let audioResult;
|
|
9064
9201
|
try {
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9072
|
-
await captureSessionFrames(session, {
|
|
9073
|
-
input,
|
|
9074
|
-
rangeStart,
|
|
9075
|
-
rangeEnd,
|
|
9076
|
-
resumeFrom,
|
|
9077
|
-
checkDiskProjection
|
|
9078
|
-
});
|
|
9079
|
-
break;
|
|
9080
|
-
} catch (error) {
|
|
9081
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9082
|
-
const resume = planTransientResume(error, {
|
|
9202
|
+
audioResult = await processCompositionAudio(
|
|
9203
|
+
audios,
|
|
9204
|
+
projectDir,
|
|
9205
|
+
join18(workDir, "audio-work"),
|
|
9206
|
+
audioOutputPath,
|
|
9207
|
+
duration,
|
|
9083
9208
|
abortSignal,
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9209
|
+
{
|
|
9210
|
+
ffmpegProcessTimeout,
|
|
9211
|
+
audioGain,
|
|
9212
|
+
masterFxChain,
|
|
9213
|
+
urlByAbsentLocalPath,
|
|
9214
|
+
keepEncoderInputAt: pcmSourcePath
|
|
9215
|
+
},
|
|
9216
|
+
compiledDir
|
|
9217
|
+
);
|
|
9218
|
+
} catch (err) {
|
|
9219
|
+
assertNotAborted();
|
|
9220
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
9221
|
+
return {
|
|
9222
|
+
audioOutputPath,
|
|
9223
|
+
hasAudio: false,
|
|
9224
|
+
audioError: detail,
|
|
9225
|
+
// Synthesised rather than left undefined. The warning policy reads
|
|
9226
|
+
// owner, retryability, reason and stage off this list, so reporting the
|
|
9227
|
+
// FATAL failure — the one that took the whole mix down — with an empty
|
|
9228
|
+
// one gave it strictly less classification than a single dropped track
|
|
9229
|
+
// gets, which is the opposite of what this stage exists to do.
|
|
9230
|
+
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
|
9231
|
+
// and this is the FX render, which is none of them.
|
|
9232
|
+
audioFailures: [
|
|
9233
|
+
{
|
|
9234
|
+
stage: "internal",
|
|
9235
|
+
reason: "internal",
|
|
9236
|
+
owner: "system",
|
|
9237
|
+
retryable: false,
|
|
9238
|
+
detail: detail.slice(0, 2e3)
|
|
9239
|
+
}
|
|
9240
|
+
]
|
|
9241
|
+
};
|
|
9242
|
+
}
|
|
9243
|
+
assertNotAborted();
|
|
9244
|
+
assertAudioNotInterrupted(audioResult.failures);
|
|
9245
|
+
hasAudio = audioResult.success;
|
|
9246
|
+
audioFailures = audioResult.failures;
|
|
9247
|
+
if (!hasAudio) {
|
|
9248
|
+
audioError = audioResult.error ?? "audio mix failed for an unknown reason";
|
|
9249
|
+
for (const failure of audioResult.failures ?? []) {
|
|
9250
|
+
log?.warn("Audio track failed", {
|
|
9251
|
+
elementId: failure.elementId,
|
|
9252
|
+
stage: failure.stage,
|
|
9253
|
+
reason: failure.reason,
|
|
9254
|
+
owner: failure.owner,
|
|
9255
|
+
retryable: failure.retryable,
|
|
9256
|
+
detail: failure.detail
|
|
9257
|
+
});
|
|
9102
9258
|
}
|
|
9103
|
-
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
9104
|
-
} finally {
|
|
9105
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9106
|
-
await closeCaptureSession5(session);
|
|
9107
9259
|
}
|
|
9108
9260
|
}
|
|
9109
|
-
return {
|
|
9261
|
+
return {
|
|
9262
|
+
audioOutputPath,
|
|
9263
|
+
hasAudio,
|
|
9264
|
+
audioError,
|
|
9265
|
+
audioFailures
|
|
9266
|
+
};
|
|
9110
9267
|
}
|
|
9111
|
-
|
|
9112
|
-
|
|
9268
|
+
|
|
9269
|
+
// src/services/render/stages/captureStage.ts
|
|
9270
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
|
|
9271
|
+
import { join as join19 } from "node:path";
|
|
9272
|
+
import {
|
|
9273
|
+
classifyCaptureFailure as classifyCaptureFailure2,
|
|
9274
|
+
frameFileExtension,
|
|
9275
|
+
captureFrame,
|
|
9276
|
+
captureFrameToBufferPipelined as captureFrameToBufferPipelined2,
|
|
9277
|
+
verifyDiskDrawElementSamples,
|
|
9278
|
+
writeCapturedFrame,
|
|
9279
|
+
closeCaptureSession as closeCaptureSession5,
|
|
9280
|
+
completeDeferredDrawElementInit as completeDeferredDrawElementInit3,
|
|
9281
|
+
createCaptureSession as createCaptureSession5,
|
|
9282
|
+
getCapturePerfSummary as getCapturePerfSummary3,
|
|
9283
|
+
initializeSession as initializeSession5,
|
|
9284
|
+
prepareCaptureSessionForReuse as prepareCaptureSessionForReuse3
|
|
9285
|
+
} from "@hyperframes/engine";
|
|
9286
|
+
function shouldAllowAdaptiveCaptureRetry(workerCount, _explicitlyConfigured) {
|
|
9287
|
+
return workerCount > 1;
|
|
9113
9288
|
}
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9289
|
+
var JPEG_RAW_RGBA_DIVISOR = 8;
|
|
9290
|
+
var HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR = 4;
|
|
9291
|
+
var DISK_PROJECTION_SAMPLE_FRAMES = 10;
|
|
9292
|
+
function estimateDiskCaptureBytes(totalFrames, captureOptions) {
|
|
9293
|
+
const scale = captureOptions.deviceScaleFactor ?? 1;
|
|
9294
|
+
const outputWidth = Math.ceil(captureOptions.width * scale);
|
|
9295
|
+
const outputHeight = Math.ceil(captureOptions.height * scale);
|
|
9296
|
+
const rawBytes = Math.ceil(totalFrames) * outputWidth * outputHeight * 4;
|
|
9297
|
+
return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
|
|
9298
|
+
rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
|
|
9299
|
+
) : rawBytes;
|
|
9120
9300
|
}
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
return;
|
|
9125
|
-
}
|
|
9126
|
-
|
|
9127
|
-
await completeDeferredDrawElementInit3(session);
|
|
9301
|
+
function freeDiskBytesAt(path) {
|
|
9302
|
+
try {
|
|
9303
|
+
const stat = statfsSync2(path);
|
|
9304
|
+
return stat.bavail * stat.bsize;
|
|
9305
|
+
} catch {
|
|
9306
|
+
return null;
|
|
9128
9307
|
}
|
|
9129
9308
|
}
|
|
9130
|
-
|
|
9131
|
-
const
|
|
9132
|
-
const
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
checkDiskProjection(fileIndex + 1);
|
|
9136
|
-
reportFrameProgress(
|
|
9137
|
-
job,
|
|
9138
|
-
`Capturing frame ${fileIndex + 1}/${rangeFrames}`,
|
|
9139
|
-
Math.round(25 + (fileIndex + 1) / rangeFrames * 45),
|
|
9140
|
-
onProgress,
|
|
9141
|
-
fileIndex + 1,
|
|
9142
|
-
rangeFrames
|
|
9143
|
-
);
|
|
9144
|
-
};
|
|
9145
|
-
if (session.workerEncodeEnabled) {
|
|
9146
|
-
let prev = null;
|
|
9147
|
-
const drainPrev = async () => {
|
|
9148
|
-
if (!prev) return;
|
|
9149
|
-
assertNotAborted();
|
|
9150
|
-
const buf = await prev.encodeResult;
|
|
9151
|
-
writeCapturedFrame(session, prev.fileIndex, buf);
|
|
9152
|
-
reportFrame(prev.fileIndex);
|
|
9153
|
-
};
|
|
9154
|
-
for (let i = resumeFrom; i < rangeFrames; i++) {
|
|
9155
|
-
assertNotAborted();
|
|
9156
|
-
const absoluteIdx = rangeStart + i;
|
|
9157
|
-
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
9158
|
-
const { encodeResult } = await captureFrameToBufferPipelined2(session, i, time);
|
|
9159
|
-
await drainPrev();
|
|
9160
|
-
prev = { fileIndex: i, encodeResult };
|
|
9161
|
-
}
|
|
9162
|
-
await drainPrev();
|
|
9163
|
-
} else {
|
|
9164
|
-
for (let i = resumeFrom; i < rangeFrames; i++) {
|
|
9165
|
-
assertNotAborted();
|
|
9166
|
-
const absoluteIdx = rangeStart + i;
|
|
9167
|
-
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
9168
|
-
await captureFrame(session, i, time);
|
|
9169
|
-
reportFrame(i);
|
|
9170
|
-
}
|
|
9309
|
+
function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes = freeDiskBytesAt) {
|
|
9310
|
+
const freeBytes = freeDiskBytes(framesDir);
|
|
9311
|
+
const estimatedBytes = estimateDiskCaptureBytes(totalFrames, captureOptions);
|
|
9312
|
+
if (freeBytes === null || estimatedBytes <= freeBytes * 0.9) {
|
|
9313
|
+
return { available: true, estimatedBytes, freeBytes };
|
|
9171
9314
|
}
|
|
9172
|
-
|
|
9173
|
-
session,
|
|
9174
|
-
{
|
|
9175
|
-
workerId: 0,
|
|
9176
|
-
startFrame: rangeStart + resumeFrom,
|
|
9177
|
-
endFrame: rangeEnd,
|
|
9178
|
-
outputDir: framesDir,
|
|
9179
|
-
outputFrameOffset: rangeStart
|
|
9180
|
-
},
|
|
9181
|
-
false
|
|
9182
|
-
);
|
|
9183
|
-
dedupPerfs.push(getCapturePerfSummary3(session));
|
|
9315
|
+
return { available: false, estimatedBytes, freeBytes };
|
|
9184
9316
|
}
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
import {
|
|
9190
|
-
closeCaptureSession as closeCaptureSession7,
|
|
9191
|
-
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
9192
|
-
createCaptureSession as createCaptureSession7,
|
|
9193
|
-
initTransparentBackground as initTransparentBackground2,
|
|
9194
|
-
initializeSession as initializeSession7,
|
|
9195
|
-
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
9196
|
-
} from "@hyperframes/engine";
|
|
9197
|
-
import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
|
|
9198
|
-
|
|
9199
|
-
// src/services/hdrImageTransferCache.ts
|
|
9200
|
-
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
9201
|
-
var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
|
|
9202
|
-
function createHdrImageTransferCache(options = {}) {
|
|
9203
|
-
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
9204
|
-
if (!Number.isInteger(maxBytes) || maxBytes < 0) {
|
|
9205
|
-
throw new Error(
|
|
9206
|
-
`createHdrImageTransferCache: maxBytes must be a non-negative integer, got ${String(maxBytes)}`
|
|
9207
|
-
);
|
|
9317
|
+
function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
|
|
9318
|
+
const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
|
|
9319
|
+
if (workerCount > 1 && oneWorker.enabled) {
|
|
9320
|
+
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.`;
|
|
9208
9321
|
}
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9322
|
+
switch (oneWorker.reason) {
|
|
9323
|
+
case "format_excluded":
|
|
9324
|
+
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
9325
|
+
case "disabled_by_config":
|
|
9326
|
+
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
9327
|
+
case "duration_cap":
|
|
9328
|
+
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.";
|
|
9329
|
+
default:
|
|
9330
|
+
return "Free up disk space.";
|
|
9213
9331
|
}
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9332
|
+
}
|
|
9333
|
+
function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
|
|
9334
|
+
const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
|
|
9335
|
+
return new Error(
|
|
9336
|
+
`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}`
|
|
9337
|
+
);
|
|
9338
|
+
}
|
|
9339
|
+
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
9340
|
+
let total = sampleDirectoryBytes(framesDir);
|
|
9341
|
+
if (!existsSync12(workDir)) return total;
|
|
9342
|
+
for (const attempt of readdirSync3(workDir)) {
|
|
9343
|
+
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
9344
|
+
total += sampleDirectoryBytes(join19(workDir, attempt));
|
|
9345
|
+
continue;
|
|
9346
|
+
}
|
|
9347
|
+
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
9348
|
+
const attemptDir = join19(workDir, attempt);
|
|
9349
|
+
for (const name of readdirSync3(attemptDir)) {
|
|
9350
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join19(attemptDir, name));
|
|
9221
9351
|
}
|
|
9222
9352
|
}
|
|
9223
|
-
return
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
}
|
|
9240
|
-
const converted = Buffer.from(source);
|
|
9241
|
-
convertTransfer2(converted, sourceTransfer, targetTransfer);
|
|
9242
|
-
if (converted.byteLength > maxBytes) {
|
|
9243
|
-
return converted;
|
|
9244
|
-
}
|
|
9245
|
-
evictUntilRoom(converted.byteLength);
|
|
9246
|
-
entries.set(key, converted);
|
|
9247
|
-
totalBytes += converted.byteLength;
|
|
9248
|
-
return converted;
|
|
9249
|
-
},
|
|
9250
|
-
size() {
|
|
9251
|
-
return entries.size;
|
|
9252
|
-
},
|
|
9253
|
-
bytesUsed() {
|
|
9254
|
-
return totalBytes;
|
|
9353
|
+
return total;
|
|
9354
|
+
}
|
|
9355
|
+
function createDiskCaptureProjection(input) {
|
|
9356
|
+
const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
|
|
9357
|
+
let checked = false;
|
|
9358
|
+
return (capturedFrames) => {
|
|
9359
|
+
if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
|
|
9360
|
+
const measured = measureBytes();
|
|
9361
|
+
if (measured <= 0) return;
|
|
9362
|
+
checked = true;
|
|
9363
|
+
const remaining = totalFrames - capturedFrames;
|
|
9364
|
+
const freeBytes = freeDiskBytes(framesDir);
|
|
9365
|
+
if (remaining <= 0 || freeBytes === null) return;
|
|
9366
|
+
const needBytes = Math.ceil(measured / capturedFrames * remaining);
|
|
9367
|
+
if (needBytes > freeBytes * 0.9) {
|
|
9368
|
+
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
|
|
9255
9369
|
}
|
|
9256
9370
|
};
|
|
9257
9371
|
}
|
|
9258
|
-
|
|
9259
|
-
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
9260
|
-
import { join as join19 } from "node:path";
|
|
9261
|
-
import {
|
|
9262
|
-
TRANSITIONS,
|
|
9263
|
-
crossfade
|
|
9264
|
-
} from "@hyperframes/engine";
|
|
9265
|
-
async function runSequentialLayeredFrameLoop(input) {
|
|
9372
|
+
async function runCaptureStage(input) {
|
|
9266
9373
|
const {
|
|
9374
|
+
fileServer,
|
|
9375
|
+
workDir,
|
|
9376
|
+
framesDir,
|
|
9267
9377
|
job,
|
|
9268
|
-
log,
|
|
9269
|
-
width,
|
|
9270
|
-
height,
|
|
9271
9378
|
totalFrames,
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
hdrVideoEndTimes,
|
|
9283
|
-
cleanedUpVideos,
|
|
9284
|
-
hdrVideoFrameSources,
|
|
9285
|
-
debugDumpEnabled,
|
|
9286
|
-
debugDumpDir,
|
|
9287
|
-
assertNotAborted,
|
|
9288
|
-
onProgress
|
|
9379
|
+
cfg,
|
|
9380
|
+
plan: plan2,
|
|
9381
|
+
log,
|
|
9382
|
+
captureAttempts,
|
|
9383
|
+
buildCaptureOptions,
|
|
9384
|
+
createRenderVideoFrameInjector,
|
|
9385
|
+
abortSignal,
|
|
9386
|
+
onProgress,
|
|
9387
|
+
frameRange,
|
|
9388
|
+
dedupPerfs
|
|
9289
9389
|
} = input;
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
const
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
assertNotAborted();
|
|
9301
|
-
const time = i * job.config.fps.den / job.config.fps.num;
|
|
9302
|
-
if (hdrPerf) hdrPerf.frames += 1;
|
|
9303
|
-
const stackingInfo = await seekInjectAndQueryStacking(
|
|
9304
|
-
domSession.page,
|
|
9305
|
-
time,
|
|
9306
|
-
beforeCaptureHook,
|
|
9307
|
-
nativeHdrIds,
|
|
9308
|
-
hdrPerf,
|
|
9309
|
-
"frameSeekMs",
|
|
9310
|
-
"frameInjectMs",
|
|
9311
|
-
"stackingQueryMs"
|
|
9312
|
-
);
|
|
9313
|
-
const activeTransition = transitionRanges.find((t) => i >= t.startFrame && i <= t.endFrame);
|
|
9314
|
-
if (i % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true)) {
|
|
9315
|
-
const hdrEl = stackingInfo.find((e) => e.isHdr);
|
|
9316
|
-
log.debug("[Render] HDR layer composite frame", {
|
|
9317
|
-
frame: i,
|
|
9318
|
-
time: time.toFixed(2),
|
|
9319
|
-
hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
|
|
9320
|
-
stackingCount: stackingInfo.length,
|
|
9321
|
-
activeTransition: activeTransition?.shader
|
|
9322
|
-
});
|
|
9323
|
-
}
|
|
9324
|
-
if (activeTransition && transitionBuffers) {
|
|
9325
|
-
if (hdrPerf) hdrPerf.transitionFrames += 1;
|
|
9326
|
-
const transitionTimingStart = Date.now();
|
|
9327
|
-
const progress = activeTransition.endFrame === activeTransition.startFrame ? 1 : (i - activeTransition.startFrame) / (activeTransition.endFrame - activeTransition.startFrame);
|
|
9328
|
-
const sceneAIds = new Set(sceneElements[activeTransition.fromScene] ?? []);
|
|
9329
|
-
const sceneBIds = new Set(sceneElements[activeTransition.toScene] ?? []);
|
|
9330
|
-
timeHdrPhase(hdrPerf, "canvasClearMs", () => {
|
|
9331
|
-
transitionBuffers.bufferA.fill(0);
|
|
9332
|
-
transitionBuffers.bufferB.fill(0);
|
|
9333
|
-
});
|
|
9334
|
-
const sceneCaptures = [
|
|
9335
|
-
[transitionBuffers.bufferA, sceneAIds],
|
|
9336
|
-
[transitionBuffers.bufferB, sceneBIds]
|
|
9337
|
-
];
|
|
9338
|
-
for (const [sceneBuf, sceneIds] of sceneCaptures) {
|
|
9339
|
-
assertNotAborted();
|
|
9340
|
-
await captureSceneIntoBuffer({
|
|
9341
|
-
session: domSession,
|
|
9342
|
-
sceneBuf,
|
|
9343
|
-
sceneIds,
|
|
9344
|
-
stackingInfo,
|
|
9345
|
-
time,
|
|
9346
|
-
width,
|
|
9347
|
-
height,
|
|
9348
|
-
nativeHdrIds,
|
|
9349
|
-
nativeHdrImageIds,
|
|
9350
|
-
beforeCaptureHook,
|
|
9351
|
-
hdrCompositeCtx,
|
|
9352
|
-
compositeTransfer,
|
|
9353
|
-
hdrTargetTransfer,
|
|
9354
|
-
hdrPerf,
|
|
9355
|
-
log,
|
|
9356
|
-
frameIdx: i
|
|
9357
|
-
});
|
|
9358
|
-
}
|
|
9359
|
-
const shaderName = activeTransition.shader;
|
|
9360
|
-
const transitionFn = shaderName ? TRANSITIONS[shaderName] ?? crossfade : crossfade;
|
|
9361
|
-
transitionFn(
|
|
9362
|
-
transitionBuffers.bufferA,
|
|
9363
|
-
transitionBuffers.bufferB,
|
|
9364
|
-
transitionBuffers.output,
|
|
9365
|
-
width,
|
|
9366
|
-
height,
|
|
9367
|
-
progress
|
|
9368
|
-
);
|
|
9369
|
-
addHdrTiming(hdrPerf, "transitionCompositeMs", transitionTimingStart);
|
|
9370
|
-
await timeHdrPhaseAsync(
|
|
9371
|
-
hdrPerf,
|
|
9372
|
-
"encoderWriteMs",
|
|
9373
|
-
async () => ensureFrameWritten(await hdrEncoder.writeFrame(transitionBuffers.output), i, hdrEncoder)
|
|
9374
|
-
);
|
|
9375
|
-
} else {
|
|
9376
|
-
if (hdrPerf) hdrPerf.normalFrames += 1;
|
|
9377
|
-
timeHdrPhase(hdrPerf, "canvasClearMs", () => normalCanvas.fill(0));
|
|
9378
|
-
await timeHdrPhaseAsync(
|
|
9379
|
-
hdrPerf,
|
|
9380
|
-
"normalCompositeMs",
|
|
9381
|
-
() => compositeHdrFrame(hdrCompositeCtx, normalCanvas, time, stackingInfo, void 0, i)
|
|
9390
|
+
let { probeSession } = input;
|
|
9391
|
+
let { workerCount } = plan2;
|
|
9392
|
+
const { forceScreenshot } = plan2;
|
|
9393
|
+
let lastBrowserConsole = [];
|
|
9394
|
+
let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
|
|
9395
|
+
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
9396
|
+
if (frameRange !== void 0) {
|
|
9397
|
+
if (!Number.isFinite(frameRange.startFrame) || !Number.isFinite(frameRange.endFrame) || frameRange.startFrame < 0 || frameRange.endFrame <= frameRange.startFrame) {
|
|
9398
|
+
throw new Error(
|
|
9399
|
+
`[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
|
|
9382
9400
|
);
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
)
|
|
9388
|
-
}
|
|
9389
|
-
await timeHdrPhaseAsync(
|
|
9390
|
-
hdrPerf,
|
|
9391
|
-
"encoderWriteMs",
|
|
9392
|
-
async () => ensureFrameWritten(await hdrEncoder.writeFrame(normalCanvas), i, hdrEncoder)
|
|
9401
|
+
}
|
|
9402
|
+
const rangeFrames = frameRange.endFrame - frameRange.startFrame;
|
|
9403
|
+
if (rangeFrames !== totalFrames) {
|
|
9404
|
+
throw new Error(
|
|
9405
|
+
`[captureStage] frameRange size (${rangeFrames}) must equal totalFrames (${totalFrames}). Received frameRange=${JSON.stringify(frameRange)}.`
|
|
9393
9406
|
);
|
|
9394
9407
|
}
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9408
|
+
}
|
|
9409
|
+
const captureOptions = buildCaptureOptions();
|
|
9410
|
+
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
9411
|
+
const checkDiskProjection = createDiskCaptureProjection({
|
|
9412
|
+
framesDir,
|
|
9413
|
+
totalFrames,
|
|
9414
|
+
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
|
|
9415
|
+
remedy
|
|
9416
|
+
});
|
|
9417
|
+
if (workerCount > 1) {
|
|
9418
|
+
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
9419
|
+
serverUrl: fileServer.url,
|
|
9420
|
+
workDir,
|
|
9421
|
+
framesDir,
|
|
9422
|
+
totalFrames,
|
|
9423
|
+
initialWorkerCount: workerCount,
|
|
9424
|
+
allowRetry: shouldAllowAdaptiveCaptureRetry(workerCount, job.config.workers !== void 0),
|
|
9425
|
+
frameExt: frameFileExtension(captureOptions.format),
|
|
9426
|
+
captureOptions,
|
|
9427
|
+
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
9428
|
+
abortSignal,
|
|
9429
|
+
frameRangeStart: frameRange?.startFrame,
|
|
9430
|
+
dedupPerfs,
|
|
9431
|
+
onProgress: (progress) => {
|
|
9432
|
+
if (progress.latestWorkerPhase) {
|
|
9433
|
+
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
9434
|
+
return;
|
|
9435
|
+
}
|
|
9436
|
+
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
9437
|
+
job.framesRendered = progress.capturedFrames;
|
|
9438
|
+
checkDiskProjection(progress.capturedFrames);
|
|
9439
|
+
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
9440
|
+
reportFrameProgress(
|
|
9441
|
+
job,
|
|
9442
|
+
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
|
|
9443
|
+
Math.round(25 + frameProgress * 45),
|
|
9444
|
+
onProgress,
|
|
9445
|
+
progress.capturedFrames,
|
|
9446
|
+
progress.totalFrames
|
|
9447
|
+
);
|
|
9448
|
+
},
|
|
9449
|
+
cfg: captureCfg,
|
|
9402
9450
|
log
|
|
9403
9451
|
});
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9452
|
+
captureAttempts.push(...attempts);
|
|
9453
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
9454
|
+
if (lastAttempt) {
|
|
9455
|
+
workerCount = lastAttempt.workers;
|
|
9456
|
+
}
|
|
9457
|
+
if (probeSession) {
|
|
9458
|
+
captureBeyondViewport = probeSession.options.captureBeyondViewport;
|
|
9459
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
9460
|
+
await closeCaptureSession5(probeSession);
|
|
9461
|
+
probeSession = null;
|
|
9462
|
+
}
|
|
9463
|
+
} else {
|
|
9464
|
+
const sequential = await runSequentialDiskCapture(
|
|
9465
|
+
input,
|
|
9466
|
+
captureOptions,
|
|
9467
|
+
captureCfg,
|
|
9468
|
+
checkDiskProjection
|
|
9412
9469
|
);
|
|
9470
|
+
probeSession = null;
|
|
9471
|
+
lastBrowserConsole = sequential.lastBrowserConsole;
|
|
9472
|
+
captureBeyondViewport = sequential.captureBeyondViewport;
|
|
9473
|
+
}
|
|
9474
|
+
return { workerCount, probeSession, lastBrowserConsole, captureBeyondViewport };
|
|
9475
|
+
}
|
|
9476
|
+
async function runSequentialDiskCapture(input, captureOptions, captureCfg, checkDiskProjection) {
|
|
9477
|
+
const {
|
|
9478
|
+
fileServer,
|
|
9479
|
+
framesDir,
|
|
9480
|
+
totalFrames,
|
|
9481
|
+
log,
|
|
9482
|
+
createRenderVideoFrameInjector,
|
|
9483
|
+
abortSignal,
|
|
9484
|
+
assertNotAborted,
|
|
9485
|
+
frameRange
|
|
9486
|
+
} = input;
|
|
9487
|
+
let { probeSession } = input;
|
|
9488
|
+
let lastBrowserConsole = [];
|
|
9489
|
+
let captureBeyondViewport;
|
|
9490
|
+
const { rangeStart, rangeEnd } = resolveCaptureRange(frameRange, totalFrames);
|
|
9491
|
+
const rangeFrames = rangeEnd - rangeStart;
|
|
9492
|
+
const frameExt = frameFileExtension(captureOptions.format);
|
|
9493
|
+
let transientRetriesUsed = 0;
|
|
9494
|
+
let resumeFrom = 0;
|
|
9495
|
+
while (true) {
|
|
9496
|
+
const videoInjector = createRenderVideoFrameInjector();
|
|
9497
|
+
const session = probeSession ?? await createCaptureSession5(
|
|
9498
|
+
fileServer.url,
|
|
9499
|
+
framesDir,
|
|
9500
|
+
captureOptions,
|
|
9501
|
+
videoInjector,
|
|
9502
|
+
captureCfg
|
|
9503
|
+
);
|
|
9504
|
+
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
9505
|
+
try {
|
|
9506
|
+
if (probeSession) {
|
|
9507
|
+
prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
|
|
9508
|
+
probeSession = null;
|
|
9509
|
+
}
|
|
9510
|
+
await ensureSessionReady(session);
|
|
9511
|
+
assertNotAborted();
|
|
9512
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9513
|
+
await captureSessionFrames(session, {
|
|
9514
|
+
input,
|
|
9515
|
+
rangeStart,
|
|
9516
|
+
rangeEnd,
|
|
9517
|
+
resumeFrom,
|
|
9518
|
+
checkDiskProjection
|
|
9519
|
+
});
|
|
9520
|
+
break;
|
|
9521
|
+
} catch (error) {
|
|
9522
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9523
|
+
const resume = planTransientResume(error, {
|
|
9524
|
+
abortSignal,
|
|
9525
|
+
rangeFrames,
|
|
9526
|
+
framesDir,
|
|
9527
|
+
frameExt,
|
|
9528
|
+
retriesUsed: transientRetriesUsed
|
|
9529
|
+
});
|
|
9530
|
+
if (resume !== null) {
|
|
9531
|
+
transientRetriesUsed++;
|
|
9532
|
+
resumeFrom = resume;
|
|
9533
|
+
log.warn(
|
|
9534
|
+
"[Render] Transient browser failure during sequential capture; retrying once with a fresh session.",
|
|
9535
|
+
{
|
|
9536
|
+
resumeFrom,
|
|
9537
|
+
rangeFrames,
|
|
9538
|
+
transientRetriesUsed,
|
|
9539
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9540
|
+
}
|
|
9541
|
+
);
|
|
9542
|
+
continue;
|
|
9543
|
+
}
|
|
9544
|
+
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
9545
|
+
} finally {
|
|
9546
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
9547
|
+
await closeCaptureSession5(session);
|
|
9548
|
+
}
|
|
9549
|
+
}
|
|
9550
|
+
return { lastBrowserConsole, captureBeyondViewport };
|
|
9551
|
+
}
|
|
9552
|
+
function resolveCaptureRange(frameRange, totalFrames) {
|
|
9553
|
+
return { rangeStart: frameRange?.startFrame ?? 0, rangeEnd: frameRange?.endFrame ?? totalFrames };
|
|
9554
|
+
}
|
|
9555
|
+
function planTransientResume(error, ctx) {
|
|
9556
|
+
const failure = classifyCaptureFailure2(error, { signal: ctx.abortSignal });
|
|
9557
|
+
const missing = findMissingFrameRanges(ctx.rangeFrames, ctx.framesDir, ctx.frameExt);
|
|
9558
|
+
const [firstMissing] = missing;
|
|
9559
|
+
if (!firstMissing) return null;
|
|
9560
|
+
return isTransientCaptureRetryEligible(failure.kind, missing, ctx.retriesUsed) ? firstMissing.startFrame : null;
|
|
9561
|
+
}
|
|
9562
|
+
async function ensureSessionReady(session) {
|
|
9563
|
+
if (!session.isInitialized) {
|
|
9564
|
+
await initializeSession5(session);
|
|
9565
|
+
return;
|
|
9566
|
+
}
|
|
9567
|
+
if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
|
|
9568
|
+
await completeDeferredDrawElementInit3(session);
|
|
9569
|
+
}
|
|
9570
|
+
}
|
|
9571
|
+
async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resumeFrom, checkDiskProjection }) {
|
|
9572
|
+
const { job, framesDir, dedupPerfs, assertNotAborted, onProgress } = input;
|
|
9573
|
+
const rangeFrames = rangeEnd - rangeStart;
|
|
9574
|
+
const reportFrame = (fileIndex) => {
|
|
9575
|
+
job.framesRendered = fileIndex + 1;
|
|
9576
|
+
checkDiskProjection(fileIndex + 1);
|
|
9577
|
+
reportFrameProgress(
|
|
9578
|
+
job,
|
|
9579
|
+
`Capturing frame ${fileIndex + 1}/${rangeFrames}`,
|
|
9580
|
+
Math.round(25 + (fileIndex + 1) / rangeFrames * 45),
|
|
9581
|
+
onProgress,
|
|
9582
|
+
fileIndex + 1,
|
|
9583
|
+
rangeFrames
|
|
9584
|
+
);
|
|
9585
|
+
};
|
|
9586
|
+
if (session.workerEncodeEnabled) {
|
|
9587
|
+
let prev = null;
|
|
9588
|
+
const drainPrev = async () => {
|
|
9589
|
+
if (!prev) return;
|
|
9590
|
+
assertNotAborted();
|
|
9591
|
+
const buf = await prev.encodeResult;
|
|
9592
|
+
writeCapturedFrame(session, prev.fileIndex, buf);
|
|
9593
|
+
reportFrame(prev.fileIndex);
|
|
9594
|
+
};
|
|
9595
|
+
for (let i = resumeFrom; i < rangeFrames; i++) {
|
|
9596
|
+
assertNotAborted();
|
|
9597
|
+
const absoluteIdx = rangeStart + i;
|
|
9598
|
+
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
9599
|
+
const { encodeResult } = await captureFrameToBufferPipelined2(session, i, time);
|
|
9600
|
+
await drainPrev();
|
|
9601
|
+
prev = { fileIndex: i, encodeResult };
|
|
9602
|
+
}
|
|
9603
|
+
await drainPrev();
|
|
9604
|
+
} else {
|
|
9605
|
+
for (let i = resumeFrom; i < rangeFrames; i++) {
|
|
9606
|
+
assertNotAborted();
|
|
9607
|
+
const absoluteIdx = rangeStart + i;
|
|
9608
|
+
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
9609
|
+
await captureFrame(session, i, time);
|
|
9610
|
+
reportFrame(i);
|
|
9611
|
+
}
|
|
9413
9612
|
}
|
|
9613
|
+
await verifyDiskDrawElementSamples(
|
|
9614
|
+
session,
|
|
9615
|
+
{
|
|
9616
|
+
workerId: 0,
|
|
9617
|
+
startFrame: rangeStart + resumeFrom,
|
|
9618
|
+
endFrame: rangeEnd,
|
|
9619
|
+
outputDir: framesDir,
|
|
9620
|
+
outputFrameOffset: rangeStart
|
|
9621
|
+
},
|
|
9622
|
+
false
|
|
9623
|
+
);
|
|
9624
|
+
dedupPerfs.push(getCapturePerfSummary3(session));
|
|
9414
9625
|
}
|
|
9415
9626
|
|
|
9416
|
-
// src/services/render/stages/
|
|
9417
|
-
import {
|
|
9627
|
+
// src/services/render/stages/captureHdrStage.ts
|
|
9628
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
|
|
9629
|
+
import { join as join23 } from "node:path";
|
|
9418
9630
|
import {
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
createCaptureSession as
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
initializeSession as initializeSession6
|
|
9631
|
+
closeCaptureSession as closeCaptureSession7,
|
|
9632
|
+
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
9633
|
+
createCaptureSession as createCaptureSession7,
|
|
9634
|
+
initTransparentBackground as initTransparentBackground2,
|
|
9635
|
+
initializeSession as initializeSession7,
|
|
9636
|
+
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
9426
9637
|
} from "@hyperframes/engine";
|
|
9638
|
+
import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
|
|
9427
9639
|
|
|
9428
|
-
// src/services/
|
|
9429
|
-
import {
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
return { path: explicit, isTs: explicit.endsWith(".ts") };
|
|
9640
|
+
// src/services/hdrImageTransferCache.ts
|
|
9641
|
+
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
9642
|
+
var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
|
|
9643
|
+
function createHdrImageTransferCache(options = {}) {
|
|
9644
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
9645
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 0) {
|
|
9646
|
+
throw new Error(
|
|
9647
|
+
`createHdrImageTransferCache: maxBytes must be a non-negative integer, got ${String(maxBytes)}`
|
|
9648
|
+
);
|
|
9438
9649
|
}
|
|
9439
|
-
const
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
return {
|
|
9650
|
+
const entries = /* @__PURE__ */ new Map();
|
|
9651
|
+
let totalBytes = 0;
|
|
9652
|
+
function makeKey(imageId, targetTransfer) {
|
|
9653
|
+
return `${imageId}|${targetTransfer}`;
|
|
9443
9654
|
}
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
if (!entryIsTs) return inherited;
|
|
9453
|
-
const hasLoader = inherited.some(
|
|
9454
|
-
(a) => a.includes("tsx/esm") || a.includes("ts-node/esm") || a.includes("--import")
|
|
9455
|
-
);
|
|
9456
|
-
if (hasLoader) return inherited;
|
|
9457
|
-
try {
|
|
9458
|
-
const require2 = createRequire(import.meta.url);
|
|
9459
|
-
const tsxEsm = require2.resolve("tsx/esm");
|
|
9460
|
-
inherited.push("--import", pathToFileURL(tsxEsm).href);
|
|
9461
|
-
} catch {
|
|
9655
|
+
function evictUntilRoom(needed) {
|
|
9656
|
+
while (totalBytes + needed > maxBytes && entries.size > 0) {
|
|
9657
|
+
const lruKey = entries.keys().next().value;
|
|
9658
|
+
if (lruKey === void 0) break;
|
|
9659
|
+
const evicted = entries.get(lruKey);
|
|
9660
|
+
if (evicted) totalBytes -= evicted.byteLength;
|
|
9661
|
+
entries.delete(lruKey);
|
|
9662
|
+
}
|
|
9462
9663
|
}
|
|
9463
|
-
return
|
|
9664
|
+
return {
|
|
9665
|
+
getConverted(imageId, sourceTransfer, targetTransfer, source) {
|
|
9666
|
+
if (sourceTransfer === targetTransfer) {
|
|
9667
|
+
return source;
|
|
9668
|
+
}
|
|
9669
|
+
if (maxBytes === 0) {
|
|
9670
|
+
const fresh = Buffer.from(source);
|
|
9671
|
+
convertTransfer2(fresh, sourceTransfer, targetTransfer);
|
|
9672
|
+
return fresh;
|
|
9673
|
+
}
|
|
9674
|
+
const key = makeKey(imageId, targetTransfer);
|
|
9675
|
+
const existing = entries.get(key);
|
|
9676
|
+
if (existing) {
|
|
9677
|
+
entries.delete(key);
|
|
9678
|
+
entries.set(key, existing);
|
|
9679
|
+
return existing;
|
|
9680
|
+
}
|
|
9681
|
+
const converted = Buffer.from(source);
|
|
9682
|
+
convertTransfer2(converted, sourceTransfer, targetTransfer);
|
|
9683
|
+
if (converted.byteLength > maxBytes) {
|
|
9684
|
+
return converted;
|
|
9685
|
+
}
|
|
9686
|
+
evictUntilRoom(converted.byteLength);
|
|
9687
|
+
entries.set(key, converted);
|
|
9688
|
+
totalBytes += converted.byteLength;
|
|
9689
|
+
return converted;
|
|
9690
|
+
},
|
|
9691
|
+
size() {
|
|
9692
|
+
return entries.size;
|
|
9693
|
+
},
|
|
9694
|
+
bytesUsed() {
|
|
9695
|
+
return totalBytes;
|
|
9696
|
+
}
|
|
9697
|
+
};
|
|
9698
|
+
}
|
|
9699
|
+
|
|
9700
|
+
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
9701
|
+
import { join as join20 } from "node:path";
|
|
9702
|
+
import {
|
|
9703
|
+
TRANSITIONS,
|
|
9704
|
+
crossfade
|
|
9705
|
+
} from "@hyperframes/engine";
|
|
9706
|
+
async function runSequentialLayeredFrameLoop(input) {
|
|
9707
|
+
const {
|
|
9708
|
+
job,
|
|
9709
|
+
log,
|
|
9710
|
+
width,
|
|
9711
|
+
height,
|
|
9712
|
+
totalFrames,
|
|
9713
|
+
nativeHdrIds,
|
|
9714
|
+
nativeHdrImageIds,
|
|
9715
|
+
hdrCompositeCtx,
|
|
9716
|
+
hdrPerf,
|
|
9717
|
+
hdrEncoder,
|
|
9718
|
+
domSession,
|
|
9719
|
+
transitionRanges,
|
|
9720
|
+
sceneElements,
|
|
9721
|
+
compositeTransfer,
|
|
9722
|
+
hdrTargetTransfer,
|
|
9723
|
+
hdrVideoEndTimes,
|
|
9724
|
+
cleanedUpVideos,
|
|
9725
|
+
hdrVideoFrameSources,
|
|
9726
|
+
debugDumpEnabled,
|
|
9727
|
+
debugDumpDir,
|
|
9728
|
+
assertNotAborted,
|
|
9729
|
+
onProgress
|
|
9730
|
+
} = input;
|
|
9731
|
+
const beforeCaptureHook = domSession.onBeforeCapture;
|
|
9732
|
+
const bufSize = width * height * 6;
|
|
9733
|
+
const hasTransitions = transitionRanges.length > 0;
|
|
9734
|
+
const transitionBuffers = hasTransitions ? {
|
|
9735
|
+
bufferA: Buffer.alloc(bufSize),
|
|
9736
|
+
bufferB: Buffer.alloc(bufSize),
|
|
9737
|
+
output: Buffer.alloc(bufSize)
|
|
9738
|
+
} : null;
|
|
9739
|
+
const normalCanvas = Buffer.alloc(bufSize);
|
|
9740
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
9741
|
+
assertNotAborted();
|
|
9742
|
+
const time = i * job.config.fps.den / job.config.fps.num;
|
|
9743
|
+
if (hdrPerf) hdrPerf.frames += 1;
|
|
9744
|
+
const stackingInfo = await seekInjectAndQueryStacking(
|
|
9745
|
+
domSession.page,
|
|
9746
|
+
time,
|
|
9747
|
+
beforeCaptureHook,
|
|
9748
|
+
nativeHdrIds,
|
|
9749
|
+
hdrPerf,
|
|
9750
|
+
"frameSeekMs",
|
|
9751
|
+
"frameInjectMs",
|
|
9752
|
+
"stackingQueryMs"
|
|
9753
|
+
);
|
|
9754
|
+
const activeTransition = transitionRanges.find((t) => i >= t.startFrame && i <= t.endFrame);
|
|
9755
|
+
if (i % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true)) {
|
|
9756
|
+
const hdrEl = stackingInfo.find((e) => e.isHdr);
|
|
9757
|
+
log.debug("[Render] HDR layer composite frame", {
|
|
9758
|
+
frame: i,
|
|
9759
|
+
time: time.toFixed(2),
|
|
9760
|
+
hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
|
|
9761
|
+
stackingCount: stackingInfo.length,
|
|
9762
|
+
activeTransition: activeTransition?.shader
|
|
9763
|
+
});
|
|
9764
|
+
}
|
|
9765
|
+
if (activeTransition && transitionBuffers) {
|
|
9766
|
+
if (hdrPerf) hdrPerf.transitionFrames += 1;
|
|
9767
|
+
const transitionTimingStart = Date.now();
|
|
9768
|
+
const progress = activeTransition.endFrame === activeTransition.startFrame ? 1 : (i - activeTransition.startFrame) / (activeTransition.endFrame - activeTransition.startFrame);
|
|
9769
|
+
const sceneAIds = new Set(sceneElements[activeTransition.fromScene] ?? []);
|
|
9770
|
+
const sceneBIds = new Set(sceneElements[activeTransition.toScene] ?? []);
|
|
9771
|
+
timeHdrPhase(hdrPerf, "canvasClearMs", () => {
|
|
9772
|
+
transitionBuffers.bufferA.fill(0);
|
|
9773
|
+
transitionBuffers.bufferB.fill(0);
|
|
9774
|
+
});
|
|
9775
|
+
const sceneCaptures = [
|
|
9776
|
+
[transitionBuffers.bufferA, sceneAIds],
|
|
9777
|
+
[transitionBuffers.bufferB, sceneBIds]
|
|
9778
|
+
];
|
|
9779
|
+
for (const [sceneBuf, sceneIds] of sceneCaptures) {
|
|
9780
|
+
assertNotAborted();
|
|
9781
|
+
await captureSceneIntoBuffer({
|
|
9782
|
+
session: domSession,
|
|
9783
|
+
sceneBuf,
|
|
9784
|
+
sceneIds,
|
|
9785
|
+
stackingInfo,
|
|
9786
|
+
time,
|
|
9787
|
+
width,
|
|
9788
|
+
height,
|
|
9789
|
+
nativeHdrIds,
|
|
9790
|
+
nativeHdrImageIds,
|
|
9791
|
+
beforeCaptureHook,
|
|
9792
|
+
hdrCompositeCtx,
|
|
9793
|
+
compositeTransfer,
|
|
9794
|
+
hdrTargetTransfer,
|
|
9795
|
+
hdrPerf,
|
|
9796
|
+
log,
|
|
9797
|
+
frameIdx: i
|
|
9798
|
+
});
|
|
9799
|
+
}
|
|
9800
|
+
const shaderName = activeTransition.shader;
|
|
9801
|
+
const transitionFn = shaderName ? TRANSITIONS[shaderName] ?? crossfade : crossfade;
|
|
9802
|
+
transitionFn(
|
|
9803
|
+
transitionBuffers.bufferA,
|
|
9804
|
+
transitionBuffers.bufferB,
|
|
9805
|
+
transitionBuffers.output,
|
|
9806
|
+
width,
|
|
9807
|
+
height,
|
|
9808
|
+
progress
|
|
9809
|
+
);
|
|
9810
|
+
addHdrTiming(hdrPerf, "transitionCompositeMs", transitionTimingStart);
|
|
9811
|
+
await timeHdrPhaseAsync(
|
|
9812
|
+
hdrPerf,
|
|
9813
|
+
"encoderWriteMs",
|
|
9814
|
+
async () => ensureFrameWritten(await hdrEncoder.writeFrame(transitionBuffers.output), i, hdrEncoder)
|
|
9815
|
+
);
|
|
9816
|
+
} else {
|
|
9817
|
+
if (hdrPerf) hdrPerf.normalFrames += 1;
|
|
9818
|
+
timeHdrPhase(hdrPerf, "canvasClearMs", () => normalCanvas.fill(0));
|
|
9819
|
+
await timeHdrPhaseAsync(
|
|
9820
|
+
hdrPerf,
|
|
9821
|
+
"normalCompositeMs",
|
|
9822
|
+
() => compositeHdrFrame(hdrCompositeCtx, normalCanvas, time, stackingInfo, void 0, i)
|
|
9823
|
+
);
|
|
9824
|
+
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
9825
|
+
writeFileExclusiveSync(
|
|
9826
|
+
join20(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
9827
|
+
normalCanvas
|
|
9828
|
+
);
|
|
9829
|
+
}
|
|
9830
|
+
await timeHdrPhaseAsync(
|
|
9831
|
+
hdrPerf,
|
|
9832
|
+
"encoderWriteMs",
|
|
9833
|
+
async () => ensureFrameWritten(await hdrEncoder.writeFrame(normalCanvas), i, hdrEncoder)
|
|
9834
|
+
);
|
|
9835
|
+
}
|
|
9836
|
+
cleanupEndedHdrVideos({
|
|
9837
|
+
time,
|
|
9838
|
+
activeTransition,
|
|
9839
|
+
hdrVideoEndTimes,
|
|
9840
|
+
cleanedUpVideos,
|
|
9841
|
+
hdrVideoFrameSources,
|
|
9842
|
+
sceneElements,
|
|
9843
|
+
log
|
|
9844
|
+
});
|
|
9845
|
+
job.framesRendered = i + 1;
|
|
9846
|
+
reportFrameProgress(
|
|
9847
|
+
job,
|
|
9848
|
+
`Layered composite frame ${i + 1}/${job.totalFrames}`,
|
|
9849
|
+
Math.round(25 + (i + 1) / totalFrames * 55),
|
|
9850
|
+
onProgress,
|
|
9851
|
+
i + 1,
|
|
9852
|
+
totalFrames
|
|
9853
|
+
);
|
|
9854
|
+
}
|
|
9855
|
+
}
|
|
9856
|
+
|
|
9857
|
+
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
9858
|
+
import { join as join22 } from "node:path";
|
|
9859
|
+
import {
|
|
9860
|
+
TRANSITIONS as TRANSITIONS2,
|
|
9861
|
+
closeCaptureSession as closeCaptureSession6,
|
|
9862
|
+
createCaptureSession as createCaptureSession6,
|
|
9863
|
+
createFrameReorderBuffer as createFrameReorderBuffer2,
|
|
9864
|
+
crossfade as crossfade2,
|
|
9865
|
+
initTransparentBackground,
|
|
9866
|
+
initializeSession as initializeSession6
|
|
9867
|
+
} from "@hyperframes/engine";
|
|
9868
|
+
|
|
9869
|
+
// src/services/shaderTransitionWorkerPool.ts
|
|
9870
|
+
import { Worker } from "node:worker_threads";
|
|
9871
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
9872
|
+
import { dirname as dirname8, join as join21 } from "node:path";
|
|
9873
|
+
import { createRequire } from "node:module";
|
|
9874
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
9875
|
+
import { cpus as cpus2 } from "node:os";
|
|
9876
|
+
function resolveWorkerEntry(explicit) {
|
|
9877
|
+
if (explicit && explicit.length > 0) {
|
|
9878
|
+
return { path: explicit, isTs: explicit.endsWith(".ts") };
|
|
9879
|
+
}
|
|
9880
|
+
const override = process.env.HF_SHADER_WORKER_ENTRY;
|
|
9881
|
+
if (override && override.length > 0) {
|
|
9882
|
+
const isTs = override.endsWith(".ts");
|
|
9883
|
+
return { path: override, isTs };
|
|
9884
|
+
}
|
|
9885
|
+
const moduleDir = dirname8(fileURLToPath3(import.meta.url));
|
|
9886
|
+
const jsPath = join21(moduleDir, "shaderTransitionWorker.js");
|
|
9887
|
+
if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
|
|
9888
|
+
const tsPath = join21(moduleDir, "shaderTransitionWorker.ts");
|
|
9889
|
+
return { path: tsPath, isTs: true };
|
|
9890
|
+
}
|
|
9891
|
+
function buildExecArgv(entryIsTs) {
|
|
9892
|
+
const inherited = [...process.execArgv];
|
|
9893
|
+
if (!entryIsTs) return inherited;
|
|
9894
|
+
const hasLoader = inherited.some(
|
|
9895
|
+
(a) => a.includes("tsx/esm") || a.includes("ts-node/esm") || a.includes("--import")
|
|
9896
|
+
);
|
|
9897
|
+
if (hasLoader) return inherited;
|
|
9898
|
+
try {
|
|
9899
|
+
const require2 = createRequire(import.meta.url);
|
|
9900
|
+
const tsxEsm = require2.resolve("tsx/esm");
|
|
9901
|
+
inherited.push("--import", pathToFileURL(tsxEsm).href);
|
|
9902
|
+
} catch {
|
|
9903
|
+
}
|
|
9904
|
+
return inherited;
|
|
9464
9905
|
}
|
|
9465
9906
|
async function createShaderTransitionWorkerPool(opts) {
|
|
9466
9907
|
const cpuCount = Math.max(1, cpus2().length);
|
|
@@ -9812,7 +10253,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
9812
10253
|
);
|
|
9813
10254
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
9814
10255
|
writeFileExclusiveSync(
|
|
9815
|
-
|
|
10256
|
+
join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
9816
10257
|
canvas
|
|
9817
10258
|
);
|
|
9818
10259
|
}
|
|
@@ -9998,7 +10439,7 @@ async function runCaptureHdrStage(input) {
|
|
|
9998
10439
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
9999
10440
|
}
|
|
10000
10441
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
10001
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
10442
|
+
const debugDumpDir = debugDumpEnabled ? join23(framesDir, "debug-composite") : null;
|
|
10002
10443
|
if (debugDumpDir && !existsSync14(debugDumpDir)) {
|
|
10003
10444
|
mkdirSync7(debugDumpDir, { recursive: true });
|
|
10004
10445
|
}
|
|
@@ -10158,19 +10599,19 @@ import {
|
|
|
10158
10599
|
mkdirSync as mkdirSync8,
|
|
10159
10600
|
readdirSync as readdirSync4,
|
|
10160
10601
|
readFileSync as readFileSync7,
|
|
10161
|
-
rmSync as
|
|
10602
|
+
rmSync as rmSync9,
|
|
10162
10603
|
statSync as statSync5,
|
|
10163
10604
|
writeFileSync as writeFileSync4
|
|
10164
10605
|
} from "node:fs";
|
|
10165
10606
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
10166
|
-
import { dirname as
|
|
10607
|
+
import { dirname as dirname9, join as join24 } from "node:path";
|
|
10167
10608
|
import {
|
|
10168
10609
|
encodeFramesChunkedConcat,
|
|
10169
10610
|
encodeFramesFromDir,
|
|
10170
10611
|
formatFfmpegError,
|
|
10171
10612
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
10172
10613
|
resolveConfig,
|
|
10173
|
-
runFfmpeg as
|
|
10614
|
+
runFfmpeg as runFfmpeg5,
|
|
10174
10615
|
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
10175
10616
|
} from "@hyperframes/engine";
|
|
10176
10617
|
import {
|
|
@@ -10178,792 +10619,355 @@ import {
|
|
|
10178
10619
|
gifClearsAfterLeavingFrameInPlace
|
|
10179
10620
|
} from "@hyperframes/core";
|
|
10180
10621
|
|
|
10181
|
-
// src/services/render/stages/gifEncodeArgs.ts
|
|
10182
|
-
import {
|
|
10183
|
-
function fpsToFfmpegArg2(fps) {
|
|
10184
|
-
return fps.den === 1 ? String(fps.num) : `${fps.num}/${fps.den}`;
|
|
10185
|
-
}
|
|
10186
|
-
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
10187
|
-
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
10188
|
-
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
10189
|
-
function framesInput(input, fpsArg) {
|
|
10190
|
-
return [
|
|
10191
|
-
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
10192
|
-
"-framerate",
|
|
10193
|
-
fpsArg,
|
|
10194
|
-
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
10195
|
-
"-i",
|
|
10196
|
-
|
|
10197
|
-
];
|
|
10198
|
-
}
|
|
10199
|
-
function buildGifPalettegenArgs(input) {
|
|
10200
|
-
const fpsArg = fpsToFfmpegArg2(input.fps);
|
|
10201
|
-
const transparency = input.preserveAlpha ? ":reserve_transparent=1" : "";
|
|
10202
|
-
return [
|
|
10203
|
-
"-y",
|
|
10204
|
-
...framesInput(input, fpsArg),
|
|
10205
|
-
"-vf",
|
|
10206
|
-
`fps=${fpsArg},palettegen=stats_mode=diff${transparency}`,
|
|
10207
|
-
input.palettePath
|
|
10208
|
-
];
|
|
10209
|
-
}
|
|
10210
|
-
function buildGifPaletteuseArgs(input) {
|
|
10211
|
-
const fpsArg = fpsToFfmpegArg2(input.fps);
|
|
10212
|
-
const transparency = input.preserveAlpha ? ":alpha_threshold=128" : "";
|
|
10213
|
-
return [
|
|
10214
|
-
"-y",
|
|
10215
|
-
...framesInput(input, fpsArg),
|
|
10216
|
-
"-i",
|
|
10217
|
-
input.palettePath,
|
|
10218
|
-
"-lavfi",
|
|
10219
|
-
`fps=${fpsArg} [x]; [x][1:v] paletteuse=dither=sierra2_4a${transparency}`,
|
|
10220
|
-
"-loop",
|
|
10221
|
-
String(input.loop),
|
|
10222
|
-
...input.wholeFrames ? WRITE_EVERY_FRAME_WHOLE : [],
|
|
10223
|
-
input.outputPath
|
|
10224
|
-
];
|
|
10225
|
-
}
|
|
10226
|
-
|
|
10227
|
-
// src/services/render/stages/encodeStage.ts
|
|
10228
|
-
import { frameFileExtension as frameFileExtension2 } from "@hyperframes/engine";
|
|
10229
|
-
function resolveGifLoop(loop) {
|
|
10230
|
-
const resolved = loop ?? 0;
|
|
10231
|
-
if (!Number.isInteger(resolved) || resolved < 0 || resolved > 65535) {
|
|
10232
|
-
throw new Error(`[Render] gifLoop must be an integer between 0 and 65535 (got ${resolved})`);
|
|
10233
|
-
}
|
|
10234
|
-
return resolved;
|
|
10235
|
-
}
|
|
10236
|
-
function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
10237
|
-
return {
|
|
10238
|
-
success: false,
|
|
10239
|
-
outputPath,
|
|
10240
|
-
durationMs: Date.now() - startTime,
|
|
10241
|
-
framesEncoded: 0,
|
|
10242
|
-
fileSize: 0,
|
|
10243
|
-
error,
|
|
10244
|
-
failureReason
|
|
10245
|
-
};
|
|
10246
|
-
}
|
|
10247
|
-
async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
10248
|
-
const { outputPath } = argsInput;
|
|
10249
|
-
const result = await runFfmpeg3(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
|
|
10250
|
-
if (!result.success) {
|
|
10251
|
-
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
10252
|
-
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
10253
|
-
}
|
|
10254
|
-
const gif = readFileSync7(outputPath);
|
|
10255
|
-
if (!clearGifFramesBeforeNext(gif)) {
|
|
10256
|
-
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
10257
|
-
return gifEncodeFailure(startTime, outputPath, error);
|
|
10258
|
-
}
|
|
10259
|
-
writeFileSync4(outputPath, gif);
|
|
10260
|
-
return null;
|
|
10261
|
-
}
|
|
10262
|
-
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
10263
|
-
const startTime = Date.now();
|
|
10264
|
-
const files = readdirSync4(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
|
|
10265
|
-
const frameCount = files.length;
|
|
10266
|
-
if (frameCount === 0) {
|
|
10267
|
-
return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
|
|
10268
|
-
}
|
|
10269
|
-
const argsInput = {
|
|
10270
|
-
framesDir,
|
|
10271
|
-
framePattern,
|
|
10272
|
-
palettePath: input.palettePath,
|
|
10273
|
-
outputPath,
|
|
10274
|
-
fps: input.fps,
|
|
10275
|
-
loop: input.loop,
|
|
10276
|
-
preserveAlpha: input.preserveAlpha
|
|
10277
|
-
};
|
|
10278
|
-
try {
|
|
10279
|
-
const run = { signal: input.signal, timeout: input.timeout };
|
|
10280
|
-
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
10281
|
-
const result = await runFfmpeg3(args, run);
|
|
10282
|
-
if (!result.success) {
|
|
10283
|
-
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
10284
|
-
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
10285
|
-
}
|
|
10286
|
-
}
|
|
10287
|
-
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
|
|
10288
|
-
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
10289
|
-
if (failure) return failure;
|
|
10290
|
-
}
|
|
10291
|
-
const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
|
|
10292
|
-
return {
|
|
10293
|
-
success: true,
|
|
10294
|
-
outputPath,
|
|
10295
|
-
durationMs: Date.now() - startTime,
|
|
10296
|
-
framesEncoded: frameCount,
|
|
10297
|
-
fileSize
|
|
10298
|
-
};
|
|
10299
|
-
} finally {
|
|
10300
|
-
rmSync7(input.palettePath, { force: true });
|
|
10301
|
-
}
|
|
10302
|
-
}
|
|
10303
|
-
function startEncodeProgress(job, stage, onProgress) {
|
|
10304
|
-
const total = job.totalFrames ?? 0;
|
|
10305
|
-
updateJobStatus(job, "encoding", stage, 75, onProgress, {
|
|
10306
|
-
code: "encode",
|
|
10307
|
-
...total > 0 && { done: 0, total }
|
|
10308
|
-
});
|
|
10309
|
-
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
10310
|
-
}
|
|
10311
|
-
function resolveEngineConfig(input) {
|
|
10312
|
-
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
10313
|
-
}
|
|
10314
|
-
function videoEncoderOptions(input, engineCfg) {
|
|
10315
|
-
const { job, preset } = input;
|
|
10316
|
-
return {
|
|
10317
|
-
fps: job.config.fps,
|
|
10318
|
-
width: input.width,
|
|
10319
|
-
height: input.height,
|
|
10320
|
-
codec: preset.codec,
|
|
10321
|
-
preset: preset.preset,
|
|
10322
|
-
quality: input.effectiveQuality,
|
|
10323
|
-
bitrate: input.effectiveBitrate,
|
|
10324
|
-
pixelFormat: preset.pixelFormat,
|
|
10325
|
-
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
10326
|
-
useGpu: job.config.useGpu,
|
|
10327
|
-
hdr: preset.hdr,
|
|
10328
|
-
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
10329
|
-
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
10330
|
-
// renders leave both undefined → preserves the existing open-GOP output.
|
|
10331
|
-
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
10332
|
-
gopSize: input.gopSize
|
|
10333
|
-
};
|
|
10334
|
-
}
|
|
10335
|
-
function canEncodeWhileCapturing(input, workerCount, cpuCount) {
|
|
10336
|
-
return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.job.config.useGpu !== true && input.captureImageFormat === "jpeg" && input.lockGopForChunkConcat !== true;
|
|
10337
|
-
}
|
|
10338
|
-
async function readLandedFrame(workerPath, mergedPath) {
|
|
10339
|
-
if (workerPath !== void 0) {
|
|
10340
|
-
try {
|
|
10341
|
-
return await readFile2(workerPath);
|
|
10342
|
-
} catch (error) {
|
|
10343
|
-
if (error.code !== "ENOENT") throw error;
|
|
10344
|
-
}
|
|
10345
|
-
}
|
|
10346
|
-
return readFile2(mergedPath);
|
|
10347
|
-
}
|
|
10348
|
-
function encodeWhileCapturing(input, totalFrames) {
|
|
10349
|
-
const engineCfg = resolveEngineConfig(input);
|
|
10350
|
-
const stop = new AbortController();
|
|
10351
|
-
const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
|
|
10352
|
-
const landed = /* @__PURE__ */ new Map();
|
|
10353
|
-
let captureDone = false;
|
|
10354
|
-
let framesFed = 0;
|
|
10355
|
-
let wake = null;
|
|
10356
|
-
const nudge = () => {
|
|
10357
|
-
wake?.();
|
|
10358
|
-
wake = null;
|
|
10359
|
-
};
|
|
10360
|
-
signal.addEventListener("abort", nudge, { once: true });
|
|
10361
|
-
let reportFed = null;
|
|
10362
|
-
const encoder = spawnStreamingEncoder4(
|
|
10363
|
-
input.videoOnlyPath,
|
|
10364
|
-
{ ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
|
|
10365
|
-
signal,
|
|
10366
|
-
engineCfg
|
|
10367
|
-
);
|
|
10368
|
-
const fed = (async () => {
|
|
10369
|
-
const stream = await encoder;
|
|
10370
|
-
for (let next = 0; next < totalFrames; next++) {
|
|
10371
|
-
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
10372
|
-
await new Promise((resolve12) => wake = resolve12);
|
|
10373
|
-
}
|
|
10374
|
-
if (signal.aborted) return;
|
|
10375
|
-
const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
10376
|
-
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
10377
|
-
landed.delete(next);
|
|
10378
|
-
if (!await stream.writeFrame(frame)) {
|
|
10379
|
-
throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
|
|
10380
|
-
}
|
|
10381
|
-
framesFed++;
|
|
10382
|
-
reportFed?.(framesFed);
|
|
10383
|
-
}
|
|
10384
|
-
})().then(
|
|
10385
|
-
() => void 0,
|
|
10386
|
-
(error) => error ?? new Error("encode failed")
|
|
10387
|
-
);
|
|
10388
|
-
const closeEncoder = async () => (await encoder.catch(() => null))?.close();
|
|
10389
|
-
return {
|
|
10390
|
-
onFrameLanded: (frame) => {
|
|
10391
|
-
landed.set(frame.index, frame.path);
|
|
10392
|
-
nudge();
|
|
10393
|
-
},
|
|
10394
|
-
finish: async () => {
|
|
10395
|
-
const start = Date.now();
|
|
10396
|
-
captureDone = true;
|
|
10397
|
-
input.log.info("[Render] Encoded frames while capturing", {
|
|
10398
|
-
framesFed,
|
|
10399
|
-
totalFrames
|
|
10400
|
-
});
|
|
10401
|
-
const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
|
|
10402
|
-
reportFed = encoded;
|
|
10403
|
-
nudge();
|
|
10404
|
-
const failure = await fed;
|
|
10405
|
-
const result = await closeEncoder();
|
|
10406
|
-
input.assertNotAborted();
|
|
10407
|
-
if (failure === void 0 && result?.success) {
|
|
10408
|
-
encoded();
|
|
10409
|
-
return { encodeMs: Date.now() - start };
|
|
10410
|
-
}
|
|
10411
|
-
input.log.warn(
|
|
10412
|
-
"[Render] Encoding while capturing failed; encoding the frames after capture",
|
|
10413
|
-
{
|
|
10414
|
-
error: String(failure ?? result?.error)
|
|
10415
|
-
}
|
|
10416
|
-
);
|
|
10417
|
-
return runEncodeStage(input);
|
|
10418
|
-
},
|
|
10419
|
-
abort: async () => {
|
|
10420
|
-
stop.abort();
|
|
10421
|
-
await fed;
|
|
10422
|
-
await closeEncoder();
|
|
10423
|
-
},
|
|
10424
|
-
get framesFed() {
|
|
10425
|
-
return framesFed;
|
|
10426
|
-
}
|
|
10427
|
-
};
|
|
10428
|
-
}
|
|
10429
|
-
async function runEncodeStage(input) {
|
|
10430
|
-
const {
|
|
10431
|
-
job,
|
|
10432
|
-
log,
|
|
10433
|
-
outputPath,
|
|
10434
|
-
framesDir,
|
|
10435
|
-
videoOnlyPath,
|
|
10436
|
-
needsAlpha,
|
|
10437
|
-
hasAudio,
|
|
10438
|
-
audioOutputPath,
|
|
10439
|
-
isPngSequence,
|
|
10440
|
-
isGif,
|
|
10441
|
-
enableChunkedEncode,
|
|
10442
|
-
chunkedEncodeSize,
|
|
10443
|
-
abortSignal,
|
|
10444
|
-
assertNotAborted,
|
|
10445
|
-
onProgress
|
|
10446
|
-
} = input;
|
|
10447
|
-
const stage5Start = Date.now();
|
|
10448
|
-
if (isPngSequence) {
|
|
10449
|
-
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
10450
|
-
if (!existsSync15(outputPath)) mkdirSync8(outputPath, { recursive: true });
|
|
10451
|
-
const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
10452
|
-
if (captured.length === 0) {
|
|
10453
|
-
throw new Error(
|
|
10454
|
-
`[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
|
|
10455
|
-
);
|
|
10456
|
-
}
|
|
10457
|
-
captured.forEach((name, i) => {
|
|
10458
|
-
const dst = join24(outputPath, formatExportFrameName(i, "png"));
|
|
10459
|
-
copyFileSync3(join24(framesDir, name), dst);
|
|
10460
|
-
});
|
|
10461
|
-
if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
|
|
10462
|
-
copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
|
|
10463
|
-
log.info(
|
|
10464
|
-
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
10465
|
-
);
|
|
10466
|
-
}
|
|
10467
|
-
encoded();
|
|
10468
|
-
return { encodeMs: Date.now() - stage5Start };
|
|
10469
|
-
}
|
|
10470
|
-
const engineCfg = resolveEngineConfig(input);
|
|
10471
|
-
if (isGif) {
|
|
10472
|
-
const encoded = startEncodeProgress(job, "Encoding GIF", onProgress);
|
|
10473
|
-
if (hasAudio) {
|
|
10474
|
-
log.warn("[Render] GIF output does not support audio; audio tracks will be ignored.");
|
|
10475
|
-
}
|
|
10476
|
-
const frameExt2 = frameFileExtension2(input.captureImageFormat);
|
|
10477
|
-
const framePattern2 = `frame_%06d.${frameExt2}`;
|
|
10478
|
-
const loop = resolveGifLoop(job.config.gifLoop);
|
|
10479
|
-
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
10480
|
-
fps: job.config.fps,
|
|
10481
|
-
loop,
|
|
10482
|
-
palettePath: join24(dirname8(videoOnlyPath), "gif-palette.png"),
|
|
10483
|
-
preserveAlpha: needsAlpha,
|
|
10484
|
-
signal: abortSignal,
|
|
10485
|
-
timeout: engineCfg.ffmpegEncodeTimeout
|
|
10486
|
-
});
|
|
10487
|
-
assertNotAborted();
|
|
10488
|
-
if (!encodeResult2.success) {
|
|
10489
|
-
throw encoderFailureError("Encoding failed", encodeResult2);
|
|
10490
|
-
}
|
|
10491
|
-
encoded();
|
|
10492
|
-
return { encodeMs: Date.now() - stage5Start };
|
|
10493
|
-
}
|
|
10494
|
-
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
10495
|
-
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
10496
|
-
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
10497
|
-
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
10498
|
-
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
10499
|
-
const framePattern = `frame_%06d.${frameExt}`;
|
|
10500
|
-
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
10501
|
-
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
10502
|
-
framesDir,
|
|
10503
|
-
framePattern,
|
|
10504
|
-
videoOnlyPath,
|
|
10505
|
-
encoderOpts,
|
|
10506
|
-
chunkedEncodeSize,
|
|
10507
|
-
abortSignal,
|
|
10508
|
-
videoEngineCfg,
|
|
10509
|
-
onFramesEncoded
|
|
10510
|
-
) : await encodeFramesFromDir(
|
|
10511
|
-
framesDir,
|
|
10512
|
-
framePattern,
|
|
10513
|
-
videoOnlyPath,
|
|
10514
|
-
encoderOpts,
|
|
10515
|
-
abortSignal,
|
|
10516
|
-
videoEngineCfg,
|
|
10517
|
-
onFramesEncoded
|
|
10518
|
-
);
|
|
10519
|
-
assertNotAborted();
|
|
10520
|
-
if (!encodeResult.success) {
|
|
10521
|
-
throw encoderFailureError("Encoding failed", encodeResult);
|
|
10522
|
-
}
|
|
10523
|
-
onFramesEncoded();
|
|
10524
|
-
return { encodeMs: Date.now() - stage5Start };
|
|
10525
|
-
}
|
|
10526
|
-
|
|
10527
|
-
// src/services/render/stages/assembleStage.ts
|
|
10528
|
-
import { applyFaststart, muxVideoWithAudio, packageHls } from "@hyperframes/engine";
|
|
10529
|
-
import { extname as extname3 } from "node:path";
|
|
10530
|
-
|
|
10531
|
-
// src/services/render/audioPadTrim.ts
|
|
10532
|
-
import { spawn } from "node:child_process";
|
|
10533
|
-
import { mkdtempSync as mkdtempSync3, renameSync as renameSync4, rmSync as rmSync8 } from "node:fs";
|
|
10534
|
-
import { dirname as dirname9, join as join25 } from "node:path";
|
|
10535
|
-
import {
|
|
10536
|
-
DEFAULT_CONFIG,
|
|
10537
|
-
describeFfmpegFailure as describeFfmpegFailure2,
|
|
10538
|
-
encodeAac,
|
|
10539
|
-
extractAudioMetadata as extractAudioMetadata3,
|
|
10540
|
-
getFfprobeBinary,
|
|
10541
|
-
ManagedChildProcess,
|
|
10542
|
-
runFfmpeg as runFfmpeg5,
|
|
10543
|
-
trackChildProcess
|
|
10544
|
-
} from "@hyperframes/engine";
|
|
10545
|
-
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
10546
|
-
|
|
10547
|
-
// src/services/render/audioTruePeak.ts
|
|
10548
|
-
import {
|
|
10549
|
-
aacSegmentCount,
|
|
10550
|
-
describeFfmpegFailure,
|
|
10551
|
-
extractAudioMetadata as extractAudioMetadata2,
|
|
10552
|
-
runFfmpeg as runFfmpeg4
|
|
10553
|
-
} from "@hyperframes/engine";
|
|
10554
|
-
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
10555
|
-
function checkedDbfs(value) {
|
|
10556
|
-
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
10557
|
-
throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
|
|
10558
|
-
}
|
|
10559
|
-
return value;
|
|
10560
|
-
}
|
|
10561
|
-
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
10562
|
-
let firstBlock;
|
|
10563
|
-
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
10564
|
-
let partialLine = "";
|
|
10565
|
-
const readLines = (chunk) => {
|
|
10566
|
-
const lines = (partialLine + chunk).split(/\r|\n/);
|
|
10567
|
-
partialLine = lines.pop() ?? "";
|
|
10568
|
-
for (const line of lines) {
|
|
10569
|
-
const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
|
|
10570
|
-
if (!ftpk) continue;
|
|
10571
|
-
const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
|
|
10572
|
-
if (firstBlock === void 0) firstBlock = block;
|
|
10573
|
-
else laterBlocks = Math.max(laterBlocks, block);
|
|
10574
|
-
}
|
|
10575
|
-
};
|
|
10576
|
-
const result = await runFfmpeg4(
|
|
10577
|
-
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
10578
|
-
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
10579
|
-
);
|
|
10580
|
-
if (!result.success) {
|
|
10581
|
-
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
10582
|
-
}
|
|
10583
|
-
const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
10584
|
-
return {
|
|
10585
|
-
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
10586
|
-
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
10587
|
-
laterBlocks: checkedDbfs(laterBlocks)
|
|
10588
|
-
};
|
|
10589
|
-
}
|
|
10590
|
-
async function measureTruePeakDbfs(audioPath, options) {
|
|
10591
|
-
const { timeoutMs, signal } = options;
|
|
10592
|
-
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
10593
|
-
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
10594
|
-
const block = 4800;
|
|
10595
|
-
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
10596
|
-
if (count === 1) return wholeFile();
|
|
10597
|
-
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
10598
|
-
const peers = new AbortController();
|
|
10599
|
-
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
10600
|
-
const scans = await Promise.all(
|
|
10601
|
-
Array.from({ length: count }, (_, k) => {
|
|
10602
|
-
const start = k === 0 ? 0 : cut(k) - block;
|
|
10603
|
-
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
10604
|
-
return scanTruePeak(
|
|
10605
|
-
audioPath,
|
|
10606
|
-
`atrim=start_sample=${start}${end},`,
|
|
10607
|
-
timeoutMs,
|
|
10608
|
-
segmentSignal
|
|
10609
|
-
);
|
|
10610
|
-
})
|
|
10611
|
-
).catch((error) => {
|
|
10612
|
-
peers.abort();
|
|
10613
|
-
throw error;
|
|
10614
|
-
});
|
|
10615
|
-
const last = scans[count - 1];
|
|
10616
|
-
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
10617
|
-
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
10618
|
-
return wholeFile();
|
|
10619
|
-
}
|
|
10620
|
-
|
|
10621
|
-
// src/services/render/audioPadTrim.ts
|
|
10622
|
-
var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
10623
|
-
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
10624
|
-
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
10625
|
-
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
10626
|
-
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
10627
|
-
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
10628
|
-
}
|
|
10629
|
-
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
10630
|
-
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
10631
|
-
const targetSec = formatSeconds(targetDurationSeconds);
|
|
10632
|
-
if (Math.abs(delta) < AUDIO_DURATION_TOLERANCE_SECONDS) {
|
|
10633
|
-
return {
|
|
10634
|
-
operation: "copy",
|
|
10635
|
-
steps: [{ kind: "copy", args: ["-i", audioPath, "-c:a", "copy", "-y", outputPath] }],
|
|
10636
|
-
cleanupPaths: []
|
|
10637
|
-
};
|
|
10638
|
-
}
|
|
10639
|
-
if (delta > 0) {
|
|
10640
|
-
return {
|
|
10641
|
-
operation: "pad",
|
|
10642
|
-
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
10643
|
-
cleanupPaths: []
|
|
10644
|
-
};
|
|
10645
|
-
}
|
|
10646
|
-
return {
|
|
10647
|
-
operation: "trim",
|
|
10648
|
-
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
10649
|
-
cleanupPaths: []
|
|
10650
|
-
};
|
|
10651
|
-
}
|
|
10652
|
-
function formatSeconds(sec) {
|
|
10653
|
-
return sec.toFixed(6);
|
|
10654
|
-
}
|
|
10655
|
-
function sanitizeProbeFailure(reason, paths) {
|
|
10656
|
-
const message = reason instanceof Error ? reason.message : String(reason);
|
|
10657
|
-
const keepFfmpegsLastLineUncapped = Number.POSITIVE_INFINITY;
|
|
10658
|
-
return redactTelemetryString3(redactKnownPaths(message, paths), keepFfmpegsLastLineUncapped);
|
|
10659
|
-
}
|
|
10660
|
-
async function padOrTrimAudioToVideoFrameCount(input) {
|
|
10661
|
-
const probeVideo = input.probeVideoFrameInfo ?? ((videoPath) => defaultProbeVideoFrameInfo(videoPath, input.signal));
|
|
10662
|
-
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
10663
|
-
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
10664
|
-
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
10665
|
-
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
10666
|
-
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
10667
|
-
const [videoResult, audioResult] = await Promise.allSettled([
|
|
10668
|
-
probeVideo(input.videoPath),
|
|
10669
|
-
probeAudio(input.audioPath, input.signal)
|
|
10670
|
-
]);
|
|
10671
|
-
const probePaths = [input.videoPath, input.audioPath, input.outputPath];
|
|
10672
|
-
if (videoResult.status === "rejected") {
|
|
10673
|
-
return failResult(
|
|
10674
|
-
input.outputPath,
|
|
10675
|
-
0,
|
|
10676
|
-
audioResult.status === "fulfilled" ? audioResult.value.durationSeconds : 0,
|
|
10677
|
-
`audioPadTrim: failed to probe video: ${sanitizeProbeFailure(
|
|
10678
|
-
videoResult.reason,
|
|
10679
|
-
probePaths
|
|
10680
|
-
)}`
|
|
10681
|
-
);
|
|
10682
|
-
}
|
|
10683
|
-
if (audioResult.status === "rejected") {
|
|
10684
|
-
return failResult(
|
|
10685
|
-
input.outputPath,
|
|
10686
|
-
0,
|
|
10687
|
-
0,
|
|
10688
|
-
`audioPadTrim: failed to probe audio: ${sanitizeProbeFailure(
|
|
10689
|
-
audioResult.reason,
|
|
10690
|
-
probePaths
|
|
10691
|
-
)}`
|
|
10692
|
-
);
|
|
10693
|
-
}
|
|
10694
|
-
const videoInfo = videoResult.value;
|
|
10695
|
-
const audioInfo = audioResult.value;
|
|
10696
|
-
if (!Number.isFinite(videoInfo.frameCount) || videoInfo.frameCount <= 0 || !Number.isFinite(videoInfo.fpsNum) || videoInfo.fpsNum <= 0 || !Number.isFinite(videoInfo.fpsDen) || videoInfo.fpsDen <= 0) {
|
|
10697
|
-
return failResult(
|
|
10698
|
-
input.outputPath,
|
|
10699
|
-
0,
|
|
10700
|
-
audioInfo.durationSeconds,
|
|
10701
|
-
`audioPadTrim: invalid video frame info: ${JSON.stringify(videoInfo)}`
|
|
10702
|
-
);
|
|
10703
|
-
}
|
|
10704
|
-
const targetDurationSeconds = videoInfo.frameCount * videoInfo.fpsDen / videoInfo.fpsNum;
|
|
10705
|
-
const plan2 = buildPadTrimAudioPlan(
|
|
10706
|
-
input.audioPath,
|
|
10707
|
-
input.outputPath,
|
|
10708
|
-
audioInfo.durationSeconds,
|
|
10709
|
-
targetDurationSeconds
|
|
10710
|
-
);
|
|
10711
|
-
try {
|
|
10712
|
-
for (const step of plan2.steps) {
|
|
10713
|
-
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
10714
|
-
if (!ffmpegResult.success) {
|
|
10715
|
-
return {
|
|
10716
|
-
success: false,
|
|
10717
|
-
outputPath: input.outputPath,
|
|
10718
|
-
targetDurationSeconds,
|
|
10719
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
10720
|
-
operation: plan2.operation,
|
|
10721
|
-
error: ffmpegResult.error,
|
|
10722
|
-
failureReason: ffmpegResult.failureReason
|
|
10723
|
-
};
|
|
10724
|
-
}
|
|
10725
|
-
}
|
|
10726
|
-
} catch (err) {
|
|
10727
|
-
return {
|
|
10728
|
-
success: false,
|
|
10729
|
-
outputPath: input.outputPath,
|
|
10730
|
-
targetDurationSeconds,
|
|
10731
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
10732
|
-
operation: plan2.operation,
|
|
10733
|
-
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
10734
|
-
};
|
|
10735
|
-
} finally {
|
|
10736
|
-
for (const path of plan2.cleanupPaths) rmSync8(path, { force: true });
|
|
10737
|
-
}
|
|
10738
|
-
let loweredDb = 0;
|
|
10739
|
-
if (probeTruePeak) {
|
|
10740
|
-
const correction = await enforceAacTruePeak({
|
|
10741
|
-
audioPath: input.outputPath,
|
|
10742
|
-
targetDurationSeconds,
|
|
10743
|
-
signal: input.signal,
|
|
10744
|
-
probeTruePeak,
|
|
10745
|
-
encode
|
|
10746
|
-
});
|
|
10747
|
-
if (!correction.success) {
|
|
10748
|
-
return {
|
|
10749
|
-
success: false,
|
|
10750
|
-
outputPath: input.outputPath,
|
|
10751
|
-
targetDurationSeconds,
|
|
10752
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
10753
|
-
operation: plan2.operation,
|
|
10754
|
-
error: correction.error,
|
|
10755
|
-
failureReason: correction.failureReason
|
|
10756
|
-
};
|
|
10757
|
-
}
|
|
10758
|
-
loweredDb = correction.loweredDb ?? 0;
|
|
10622
|
+
// src/services/render/stages/gifEncodeArgs.ts
|
|
10623
|
+
import { frameSequencePath } from "@hyperframes/engine";
|
|
10624
|
+
function fpsToFfmpegArg2(fps) {
|
|
10625
|
+
return fps.den === 1 ? String(fps.num) : `${fps.num}/${fps.den}`;
|
|
10626
|
+
}
|
|
10627
|
+
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
10628
|
+
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
10629
|
+
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
10630
|
+
function framesInput(input, fpsArg) {
|
|
10631
|
+
return [
|
|
10632
|
+
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
10633
|
+
"-framerate",
|
|
10634
|
+
fpsArg,
|
|
10635
|
+
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
10636
|
+
"-i",
|
|
10637
|
+
frameSequencePath(input.framesDir, input.framePattern)
|
|
10638
|
+
];
|
|
10639
|
+
}
|
|
10640
|
+
function buildGifPalettegenArgs(input) {
|
|
10641
|
+
const fpsArg = fpsToFfmpegArg2(input.fps);
|
|
10642
|
+
const transparency = input.preserveAlpha ? ":reserve_transparent=1" : "";
|
|
10643
|
+
return [
|
|
10644
|
+
"-y",
|
|
10645
|
+
...framesInput(input, fpsArg),
|
|
10646
|
+
"-vf",
|
|
10647
|
+
`fps=${fpsArg},palettegen=stats_mode=diff${transparency}`,
|
|
10648
|
+
input.palettePath
|
|
10649
|
+
];
|
|
10650
|
+
}
|
|
10651
|
+
function buildGifPaletteuseArgs(input) {
|
|
10652
|
+
const fpsArg = fpsToFfmpegArg2(input.fps);
|
|
10653
|
+
const transparency = input.preserveAlpha ? ":alpha_threshold=128" : "";
|
|
10654
|
+
return [
|
|
10655
|
+
"-y",
|
|
10656
|
+
...framesInput(input, fpsArg),
|
|
10657
|
+
"-i",
|
|
10658
|
+
input.palettePath,
|
|
10659
|
+
"-lavfi",
|
|
10660
|
+
`fps=${fpsArg} [x]; [x][1:v] paletteuse=dither=sierra2_4a${transparency}`,
|
|
10661
|
+
"-loop",
|
|
10662
|
+
String(input.loop),
|
|
10663
|
+
...input.wholeFrames ? WRITE_EVERY_FRAME_WHOLE : [],
|
|
10664
|
+
input.outputPath
|
|
10665
|
+
];
|
|
10666
|
+
}
|
|
10667
|
+
|
|
10668
|
+
// src/services/render/stages/encodeStage.ts
|
|
10669
|
+
import { frameFileExtension as frameFileExtension2 } from "@hyperframes/engine";
|
|
10670
|
+
function resolveGifLoop(loop) {
|
|
10671
|
+
const resolved = loop ?? 0;
|
|
10672
|
+
if (!Number.isInteger(resolved) || resolved < 0 || resolved > 65535) {
|
|
10673
|
+
throw new Error(`[Render] gifLoop must be an integer between 0 and 65535 (got ${resolved})`);
|
|
10759
10674
|
}
|
|
10675
|
+
return resolved;
|
|
10676
|
+
}
|
|
10677
|
+
function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
10760
10678
|
return {
|
|
10761
|
-
success:
|
|
10762
|
-
outputPath
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10679
|
+
success: false,
|
|
10680
|
+
outputPath,
|
|
10681
|
+
durationMs: Date.now() - startTime,
|
|
10682
|
+
framesEncoded: 0,
|
|
10683
|
+
fileSize: 0,
|
|
10684
|
+
error,
|
|
10685
|
+
failureReason
|
|
10767
10686
|
};
|
|
10768
10687
|
}
|
|
10769
|
-
async function
|
|
10770
|
-
const
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10777
|
-
|
|
10688
|
+
async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
10689
|
+
const { outputPath } = argsInput;
|
|
10690
|
+
const result = await runFfmpeg5(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
|
|
10691
|
+
if (!result.success) {
|
|
10692
|
+
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
10693
|
+
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
10694
|
+
}
|
|
10695
|
+
const gif = readFileSync7(outputPath);
|
|
10696
|
+
if (!clearGifFramesBeforeNext(gif)) {
|
|
10697
|
+
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
10698
|
+
return gifEncodeFailure(startTime, outputPath, error);
|
|
10699
|
+
}
|
|
10700
|
+
writeFileSync4(outputPath, gif);
|
|
10701
|
+
return null;
|
|
10778
10702
|
}
|
|
10779
|
-
async function
|
|
10780
|
-
|
|
10703
|
+
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
10704
|
+
const startTime = Date.now();
|
|
10705
|
+
const files = readdirSync4(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
|
|
10706
|
+
const frameCount = files.length;
|
|
10707
|
+
if (frameCount === 0) {
|
|
10708
|
+
return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
|
|
10709
|
+
}
|
|
10710
|
+
const argsInput = {
|
|
10711
|
+
framesDir,
|
|
10712
|
+
framePattern,
|
|
10713
|
+
palettePath: input.palettePath,
|
|
10714
|
+
outputPath,
|
|
10715
|
+
fps: input.fps,
|
|
10716
|
+
loop: input.loop,
|
|
10717
|
+
preserveAlpha: input.preserveAlpha
|
|
10718
|
+
};
|
|
10781
10719
|
try {
|
|
10782
|
-
|
|
10783
|
-
const
|
|
10784
|
-
|
|
10785
|
-
let attenuationDb = 0;
|
|
10786
|
-
let measuredPath = input.audioPath;
|
|
10787
|
-
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
10788
|
-
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
10789
|
-
measurements.push(
|
|
10790
|
-
`pass ${pass}: ${truePeakDbfs.toFixed(3)} dBFS at ${attenuationDb.toFixed(3)} dB attenuation`
|
|
10791
|
-
);
|
|
10792
|
-
if (Number.isNaN(truePeakDbfs) || truePeakDbfs === Number.POSITIVE_INFINITY) {
|
|
10793
|
-
return { success: false, error: "audioPadTrim: FFmpeg reported an invalid true peak" };
|
|
10794
|
-
}
|
|
10795
|
-
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
10796
|
-
if (measuredPath !== correctedPath) return { success: true };
|
|
10797
|
-
renameSync4(correctedPath, input.audioPath);
|
|
10798
|
-
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
10799
|
-
}
|
|
10800
|
-
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
10801
|
-
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
10802
|
-
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
|
|
10803
|
-
const result = await input.encode(
|
|
10804
|
-
input.audioPath,
|
|
10805
|
-
correctedPath,
|
|
10806
|
-
input.targetDurationSeconds,
|
|
10807
|
-
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
10808
|
-
);
|
|
10720
|
+
const run = { signal: input.signal, timeout: input.timeout };
|
|
10721
|
+
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
10722
|
+
const result = await runFfmpeg5(args, run);
|
|
10809
10723
|
if (!result.success) {
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
error: sanitizeProbeFailure(
|
|
10813
|
-
result.error ?? "audioPadTrim: failed to constrain AAC true peak",
|
|
10814
|
-
[input.audioPath, correctedPath]
|
|
10815
|
-
),
|
|
10816
|
-
failureReason: result.failureReason
|
|
10817
|
-
};
|
|
10724
|
+
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
10725
|
+
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
10818
10726
|
}
|
|
10819
|
-
measuredPath = correctedPath;
|
|
10820
10727
|
}
|
|
10728
|
+
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
|
|
10729
|
+
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
10730
|
+
if (failure) return failure;
|
|
10731
|
+
}
|
|
10732
|
+
const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
|
|
10821
10733
|
return {
|
|
10822
|
-
success:
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
success: false,
|
|
10828
|
-
error: `audioPadTrim: failed to validate AAC true peak: ${sanitizeProbeFailure(
|
|
10829
|
-
err,
|
|
10830
|
-
scratchDir ? [input.audioPath, scratchDir] : [input.audioPath]
|
|
10831
|
-
)}`
|
|
10734
|
+
success: true,
|
|
10735
|
+
outputPath,
|
|
10736
|
+
durationMs: Date.now() - startTime,
|
|
10737
|
+
framesEncoded: frameCount,
|
|
10738
|
+
fileSize
|
|
10832
10739
|
};
|
|
10833
10740
|
} finally {
|
|
10834
|
-
|
|
10741
|
+
rmSync9(input.palettePath, { force: true });
|
|
10835
10742
|
}
|
|
10836
10743
|
}
|
|
10837
|
-
function
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
error
|
|
10845
|
-
};
|
|
10846
|
-
}
|
|
10847
|
-
async function defaultProbeVideoFrameInfo(videoPath, signal) {
|
|
10848
|
-
const fastInfo = await runFfprobeJson(
|
|
10849
|
-
[
|
|
10850
|
-
"-v",
|
|
10851
|
-
"error",
|
|
10852
|
-
"-select_streams",
|
|
10853
|
-
"v:0",
|
|
10854
|
-
"-show_entries",
|
|
10855
|
-
"stream=nb_frames,r_frame_rate",
|
|
10856
|
-
"-of",
|
|
10857
|
-
"json",
|
|
10858
|
-
"--",
|
|
10859
|
-
videoPath
|
|
10860
|
-
],
|
|
10861
|
-
signal
|
|
10862
|
-
);
|
|
10863
|
-
let stream = fastInfo.streams?.[0];
|
|
10864
|
-
const fastCount = Number(stream?.nb_frames);
|
|
10865
|
-
if (stream && Number.isFinite(fastCount) && fastCount > 0) {
|
|
10866
|
-
return { frameCount: fastCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
10867
|
-
}
|
|
10868
|
-
const slowInfo = await runFfprobeJson(
|
|
10869
|
-
[
|
|
10870
|
-
"-v",
|
|
10871
|
-
"error",
|
|
10872
|
-
"-select_streams",
|
|
10873
|
-
"v:0",
|
|
10874
|
-
"-count_packets",
|
|
10875
|
-
"-show_entries",
|
|
10876
|
-
"stream=nb_read_packets,r_frame_rate",
|
|
10877
|
-
"-of",
|
|
10878
|
-
"json",
|
|
10879
|
-
"--",
|
|
10880
|
-
videoPath
|
|
10881
|
-
],
|
|
10882
|
-
signal
|
|
10883
|
-
);
|
|
10884
|
-
stream = slowInfo.streams?.[0];
|
|
10885
|
-
if (!stream) throw new Error(`ffprobe found no video stream in ${videoPath}`);
|
|
10886
|
-
const slowCount = Number(stream.nb_read_packets);
|
|
10887
|
-
if (!Number.isFinite(slowCount) || slowCount <= 0) {
|
|
10888
|
-
throw new Error(`ffprobe returned no frame count: ${JSON.stringify(stream)}`);
|
|
10889
|
-
}
|
|
10890
|
-
return { frameCount: slowCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
10744
|
+
function startEncodeProgress(job, stage, onProgress) {
|
|
10745
|
+
const total = job.totalFrames ?? 0;
|
|
10746
|
+
updateJobStatus(job, "encoding", stage, 75, onProgress, {
|
|
10747
|
+
code: "encode",
|
|
10748
|
+
...total > 0 && { done: 0, total }
|
|
10749
|
+
});
|
|
10750
|
+
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
10891
10751
|
}
|
|
10892
|
-
function
|
|
10893
|
-
|
|
10894
|
-
const fpsNum = Number(n);
|
|
10895
|
-
const fpsDen = d === void 0 ? 1 : Number(d);
|
|
10896
|
-
if (!Number.isFinite(fpsNum) || !Number.isFinite(fpsDen) || fpsNum <= 0 || fpsDen <= 0) {
|
|
10897
|
-
throw new Error(`Invalid r_frame_rate: ${JSON.stringify(rate)}`);
|
|
10898
|
-
}
|
|
10899
|
-
return { fpsNum, fpsDen };
|
|
10752
|
+
function resolveEngineConfig(input) {
|
|
10753
|
+
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
10900
10754
|
}
|
|
10901
|
-
|
|
10902
|
-
const
|
|
10755
|
+
function videoEncoderOptions(input, engineCfg) {
|
|
10756
|
+
const { job, preset } = input;
|
|
10903
10757
|
return {
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10758
|
+
fps: job.config.fps,
|
|
10759
|
+
width: input.width,
|
|
10760
|
+
height: input.height,
|
|
10761
|
+
codec: preset.codec,
|
|
10762
|
+
preset: preset.preset,
|
|
10763
|
+
quality: input.effectiveQuality,
|
|
10764
|
+
bitrate: input.effectiveBitrate,
|
|
10765
|
+
pixelFormat: preset.pixelFormat,
|
|
10766
|
+
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
10767
|
+
useGpu: job.config.useGpu,
|
|
10768
|
+
hdr: preset.hdr,
|
|
10769
|
+
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
10770
|
+
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
10771
|
+
// renders leave both undefined → preserves the existing open-GOP output.
|
|
10772
|
+
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
10773
|
+
gopSize: input.gopSize
|
|
10908
10774
|
};
|
|
10909
10775
|
}
|
|
10910
|
-
|
|
10911
|
-
return
|
|
10776
|
+
function canEncodeWhileCapturing(input, workerCount, cpuCount) {
|
|
10777
|
+
return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.job.config.useGpu !== true && input.captureImageFormat === "jpeg" && input.lockGopForChunkConcat !== true;
|
|
10912
10778
|
}
|
|
10913
|
-
function
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
}
|
|
10779
|
+
async function readLandedFrame(workerPath, mergedPath) {
|
|
10780
|
+
if (workerPath !== void 0) {
|
|
10781
|
+
try {
|
|
10782
|
+
return await readFile2(workerPath);
|
|
10783
|
+
} catch (error) {
|
|
10784
|
+
if (error.code !== "ENOENT") throw error;
|
|
10785
|
+
}
|
|
10786
|
+
}
|
|
10787
|
+
return readFile2(mergedPath);
|
|
10921
10788
|
}
|
|
10922
|
-
function
|
|
10923
|
-
|
|
10789
|
+
function encodeWhileCapturing(input, totalFrames) {
|
|
10790
|
+
const engineCfg = resolveEngineConfig(input);
|
|
10791
|
+
const stop = new AbortController();
|
|
10792
|
+
const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
|
|
10793
|
+
const landed = /* @__PURE__ */ new Map();
|
|
10794
|
+
let captureDone = false;
|
|
10795
|
+
let framesFed = 0;
|
|
10796
|
+
let wake = null;
|
|
10797
|
+
const nudge = () => {
|
|
10798
|
+
wake?.();
|
|
10799
|
+
wake = null;
|
|
10800
|
+
};
|
|
10801
|
+
signal.addEventListener("abort", nudge, { once: true });
|
|
10802
|
+
let reportFed = null;
|
|
10803
|
+
const encoder = spawnStreamingEncoder4(
|
|
10804
|
+
input.videoOnlyPath,
|
|
10805
|
+
{ ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
|
|
10806
|
+
signal,
|
|
10807
|
+
engineCfg
|
|
10808
|
+
);
|
|
10809
|
+
const fed = (async () => {
|
|
10810
|
+
const stream = await encoder;
|
|
10811
|
+
for (let next = 0; next < totalFrames; next++) {
|
|
10812
|
+
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
10813
|
+
await new Promise((resolve12) => wake = resolve12);
|
|
10814
|
+
}
|
|
10815
|
+
if (signal.aborted) return;
|
|
10816
|
+
const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
10817
|
+
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
10818
|
+
landed.delete(next);
|
|
10819
|
+
if (!await stream.writeFrame(frame)) {
|
|
10820
|
+
throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
|
|
10821
|
+
}
|
|
10822
|
+
framesFed++;
|
|
10823
|
+
reportFed?.(framesFed);
|
|
10824
|
+
}
|
|
10825
|
+
})().then(
|
|
10826
|
+
() => void 0,
|
|
10827
|
+
(error) => error ?? new Error("encode failed")
|
|
10828
|
+
);
|
|
10829
|
+
const closeEncoder = async () => (await encoder.catch(() => null))?.close();
|
|
10924
10830
|
return {
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10831
|
+
onFrameLanded: (frame) => {
|
|
10832
|
+
landed.set(frame.index, frame.path);
|
|
10833
|
+
nudge();
|
|
10834
|
+
},
|
|
10835
|
+
finish: async () => {
|
|
10836
|
+
const start = Date.now();
|
|
10837
|
+
captureDone = true;
|
|
10838
|
+
input.log.info("[Render] Encoded frames while capturing", {
|
|
10839
|
+
framesFed,
|
|
10840
|
+
totalFrames
|
|
10841
|
+
});
|
|
10842
|
+
const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
|
|
10843
|
+
reportFed = encoded;
|
|
10844
|
+
nudge();
|
|
10845
|
+
const failure = await fed;
|
|
10846
|
+
const result = await closeEncoder();
|
|
10847
|
+
input.assertNotAborted();
|
|
10848
|
+
if (failure === void 0 && result?.success) {
|
|
10849
|
+
encoded();
|
|
10850
|
+
return { encodeMs: Date.now() - start };
|
|
10851
|
+
}
|
|
10852
|
+
input.log.warn(
|
|
10853
|
+
"[Render] Encoding while capturing failed; encoding the frames after capture",
|
|
10854
|
+
{
|
|
10855
|
+
error: String(failure ?? result?.error)
|
|
10856
|
+
}
|
|
10857
|
+
);
|
|
10858
|
+
return runEncodeStage(input);
|
|
10859
|
+
},
|
|
10860
|
+
abort: async () => {
|
|
10861
|
+
stop.abort();
|
|
10862
|
+
await fed;
|
|
10863
|
+
await closeEncoder();
|
|
10864
|
+
},
|
|
10865
|
+
get framesFed() {
|
|
10866
|
+
return framesFed;
|
|
10867
|
+
}
|
|
10928
10868
|
};
|
|
10929
10869
|
}
|
|
10930
|
-
async function
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
|
|
10870
|
+
async function runEncodeStage(input) {
|
|
10871
|
+
const {
|
|
10872
|
+
job,
|
|
10873
|
+
log,
|
|
10874
|
+
outputPath,
|
|
10875
|
+
framesDir,
|
|
10876
|
+
videoOnlyPath,
|
|
10877
|
+
needsAlpha,
|
|
10878
|
+
hasAudio,
|
|
10879
|
+
audioOutputPath,
|
|
10880
|
+
isPngSequence,
|
|
10881
|
+
isGif,
|
|
10882
|
+
enableChunkedEncode,
|
|
10883
|
+
chunkedEncodeSize,
|
|
10884
|
+
abortSignal,
|
|
10885
|
+
assertNotAborted,
|
|
10886
|
+
onProgress
|
|
10887
|
+
} = input;
|
|
10888
|
+
const stage5Start = Date.now();
|
|
10889
|
+
if (isPngSequence) {
|
|
10890
|
+
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
10891
|
+
if (!existsSync15(outputPath)) mkdirSync8(outputPath, { recursive: true });
|
|
10892
|
+
const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
10893
|
+
if (captured.length === 0) {
|
|
10894
|
+
throw new Error(
|
|
10895
|
+
`[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
|
|
10896
|
+
);
|
|
10951
10897
|
}
|
|
10952
|
-
|
|
10898
|
+
captured.forEach((name, i) => {
|
|
10899
|
+
const dst = join24(outputPath, formatExportFrameName(i, "png"));
|
|
10900
|
+
copyFileSync3(join24(framesDir, name), dst);
|
|
10901
|
+
});
|
|
10902
|
+
if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
|
|
10903
|
+
copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
|
|
10904
|
+
log.info(
|
|
10905
|
+
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
10906
|
+
);
|
|
10907
|
+
}
|
|
10908
|
+
encoded();
|
|
10909
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
10953
10910
|
}
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
const
|
|
10957
|
-
|
|
10911
|
+
const engineCfg = resolveEngineConfig(input);
|
|
10912
|
+
if (isGif) {
|
|
10913
|
+
const encoded = startEncodeProgress(job, "Encoding GIF", onProgress);
|
|
10914
|
+
if (hasAudio) {
|
|
10915
|
+
log.warn("[Render] GIF output does not support audio; audio tracks will be ignored.");
|
|
10916
|
+
}
|
|
10917
|
+
const frameExt2 = frameFileExtension2(input.captureImageFormat);
|
|
10918
|
+
const framePattern2 = `frame_%06d.${frameExt2}`;
|
|
10919
|
+
const loop = resolveGifLoop(job.config.gifLoop);
|
|
10920
|
+
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
10921
|
+
fps: job.config.fps,
|
|
10922
|
+
loop,
|
|
10923
|
+
palettePath: join24(dirname9(videoOnlyPath), "gif-palette.png"),
|
|
10924
|
+
preserveAlpha: needsAlpha,
|
|
10925
|
+
signal: abortSignal,
|
|
10926
|
+
timeout: engineCfg.ffmpegEncodeTimeout
|
|
10927
|
+
});
|
|
10928
|
+
assertNotAborted();
|
|
10929
|
+
if (!encodeResult2.success) {
|
|
10930
|
+
throw encoderFailureError("Encoding failed", encodeResult2);
|
|
10931
|
+
}
|
|
10932
|
+
encoded();
|
|
10933
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
10958
10934
|
}
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10935
|
+
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
10936
|
+
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
10937
|
+
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
10938
|
+
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
10939
|
+
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
10940
|
+
const framePattern = `frame_%06d.${frameExt}`;
|
|
10941
|
+
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
10942
|
+
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
10943
|
+
framesDir,
|
|
10944
|
+
framePattern,
|
|
10945
|
+
videoOnlyPath,
|
|
10946
|
+
encoderOpts,
|
|
10947
|
+
chunkedEncodeSize,
|
|
10948
|
+
abortSignal,
|
|
10949
|
+
videoEngineCfg,
|
|
10950
|
+
onFramesEncoded
|
|
10951
|
+
) : await encodeFramesFromDir(
|
|
10952
|
+
framesDir,
|
|
10953
|
+
framePattern,
|
|
10954
|
+
videoOnlyPath,
|
|
10955
|
+
encoderOpts,
|
|
10956
|
+
abortSignal,
|
|
10957
|
+
videoEngineCfg,
|
|
10958
|
+
onFramesEncoded
|
|
10959
|
+
);
|
|
10960
|
+
assertNotAborted();
|
|
10961
|
+
if (!encodeResult.success) {
|
|
10962
|
+
throw encoderFailureError("Encoding failed", encodeResult);
|
|
10963
10963
|
}
|
|
10964
|
+
onFramesEncoded();
|
|
10965
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
10964
10966
|
}
|
|
10965
10967
|
|
|
10966
10968
|
// src/services/render/stages/assembleStage.ts
|
|
10969
|
+
import { applyFaststart, muxVideoWithAudio, packageHls } from "@hyperframes/engine";
|
|
10970
|
+
import { extname as extname3 } from "node:path";
|
|
10967
10971
|
function recordLimiterAttenuation(job, audioLoweredDb) {
|
|
10968
10972
|
if (audioLoweredDb === void 0) return;
|
|
10969
10973
|
job.audioLoweredDb = audioLoweredDb;
|
|
@@ -11129,7 +11133,7 @@ function sampleDirectoryBytes(dir) {
|
|
|
11129
11133
|
continue;
|
|
11130
11134
|
}
|
|
11131
11135
|
for (const name of entries) {
|
|
11132
|
-
const full =
|
|
11136
|
+
const full = join25(current, name);
|
|
11133
11137
|
try {
|
|
11134
11138
|
const st = statSync6(full);
|
|
11135
11139
|
if (st.isDirectory()) {
|
|
@@ -11277,7 +11281,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11277
11281
|
const ranges = [];
|
|
11278
11282
|
let rangeStart = null;
|
|
11279
11283
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11280
|
-
const framePath =
|
|
11284
|
+
const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11281
11285
|
const missing = !existsSync16(framePath) || statSync6(framePath).size <= 8;
|
|
11282
11286
|
if (missing && rangeStart === null) {
|
|
11283
11287
|
rangeStart = frameIndex;
|
|
@@ -11300,7 +11304,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
|
|
|
11300
11304
|
workerId,
|
|
11301
11305
|
startFrame: rangeStart + range.startFrame,
|
|
11302
11306
|
endFrame: rangeStart + range.endFrame,
|
|
11303
|
-
outputDir:
|
|
11307
|
+
outputDir: join25(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
|
|
11304
11308
|
outputFrameOffset: rangeStart
|
|
11305
11309
|
}));
|
|
11306
11310
|
batches.push(batch);
|
|
@@ -11328,8 +11332,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11328
11332
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11329
11333
|
}
|
|
11330
11334
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11331
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
11332
|
-
return
|
|
11335
|
+
if (platform2 === "win32" || platform2 === "darwin") return join25(systemTempDir, "hf-render-");
|
|
11336
|
+
return join25(dirname10(outputPath), `work-${jobId}-`);
|
|
11333
11337
|
}
|
|
11334
11338
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
11335
11339
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -11361,7 +11365,7 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
11361
11365
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
11362
11366
|
let captured = 0;
|
|
11363
11367
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11364
|
-
const framePath =
|
|
11368
|
+
const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11365
11369
|
if (existsSync16(framePath)) captured++;
|
|
11366
11370
|
}
|
|
11367
11371
|
return captured;
|
|
@@ -11387,7 +11391,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11387
11391
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
11388
11392
|
});
|
|
11389
11393
|
pendingTransientRetry = false;
|
|
11390
|
-
const attemptWorkDir =
|
|
11394
|
+
const attemptWorkDir = join25(options.workDir, `capture-attempt-${attempt}`);
|
|
11391
11395
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
11392
11396
|
missingRanges,
|
|
11393
11397
|
currentWorkers,
|
|
@@ -11956,17 +11960,17 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
11956
11960
|
}
|
|
11957
11961
|
function resolveRenderDebugDir() {
|
|
11958
11962
|
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
|
|
11959
|
-
return
|
|
11963
|
+
return join25(producerRoot, ".debug");
|
|
11960
11964
|
}
|
|
11961
11965
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
11962
11966
|
validateHlsRenderConfig(job.config);
|
|
11963
11967
|
const debugDir = resolveRenderDebugDir();
|
|
11964
11968
|
const outputDir = dirname10(outputPath);
|
|
11965
11969
|
if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
|
|
11966
|
-
const workDir = job.config.debug ?
|
|
11970
|
+
const workDir = job.config.debug ? join25(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
11967
11971
|
const pipelineStart = Date.now();
|
|
11968
11972
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
11969
|
-
const logPath = job.config.debug ?
|
|
11973
|
+
const logPath = job.config.debug ? join25(workDir, "render.log") : null;
|
|
11970
11974
|
const execution = new RenderExecutionContext({
|
|
11971
11975
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
11972
11976
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -11980,7 +11984,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
11980
11984
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
11981
11985
|
return;
|
|
11982
11986
|
}
|
|
11983
|
-
|
|
11987
|
+
rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
11984
11988
|
});
|
|
11985
11989
|
try {
|
|
11986
11990
|
await executeRenderPipeline({
|
|
@@ -12024,7 +12028,7 @@ async function executeRenderPipeline(input) {
|
|
|
12024
12028
|
imageDecodeFailures: 0
|
|
12025
12029
|
};
|
|
12026
12030
|
let hdrPerf;
|
|
12027
|
-
const perfOutputPath =
|
|
12031
|
+
const perfOutputPath = join25(workDir, "perf-summary.json");
|
|
12028
12032
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
12029
12033
|
const observability = new RenderObservabilityRecorder({
|
|
12030
12034
|
pipelineStartMs: pipelineStart,
|
|
@@ -12123,15 +12127,15 @@ async function executeRenderPipeline(input) {
|
|
|
12123
12127
|
requestedWorkers: job.config.workers ?? "auto"
|
|
12124
12128
|
});
|
|
12125
12129
|
const entryFile = job.config.entryFile || "index.html";
|
|
12126
|
-
let htmlPath =
|
|
12130
|
+
let htmlPath = join25(projectDir, entryFile);
|
|
12127
12131
|
if (!existsSync16(htmlPath)) {
|
|
12128
12132
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
12129
12133
|
}
|
|
12130
12134
|
assertNotAborted();
|
|
12131
12135
|
const rawEntry = readFileSync8(htmlPath, "utf-8");
|
|
12132
12136
|
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
12133
|
-
const wrapperPath =
|
|
12134
|
-
const projectIndexPath =
|
|
12137
|
+
const wrapperPath = join25(workDir, "standalone-entry.html");
|
|
12138
|
+
const projectIndexPath = join25(projectDir, "index.html");
|
|
12135
12139
|
if (!existsSync16(projectIndexPath)) {
|
|
12136
12140
|
throw new Error(
|
|
12137
12141
|
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
@@ -12294,7 +12298,7 @@ async function executeRenderPipeline(input) {
|
|
|
12294
12298
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
12295
12299
|
code: "extract_video"
|
|
12296
12300
|
});
|
|
12297
|
-
const compiledDir =
|
|
12301
|
+
const compiledDir = join25(workDir, "compiled");
|
|
12298
12302
|
const extractResult = await observeRenderStage(
|
|
12299
12303
|
observability,
|
|
12300
12304
|
"video_extract",
|
|
@@ -12382,7 +12386,8 @@ async function executeRenderPipeline(input) {
|
|
|
12382
12386
|
projectDir,
|
|
12383
12387
|
workDir,
|
|
12384
12388
|
compiledDir,
|
|
12385
|
-
|
|
12389
|
+
totalFrames: probeResult.totalFrames,
|
|
12390
|
+
fps: job.config.fps,
|
|
12386
12391
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
12387
12392
|
audioGain: cfg.audioGain,
|
|
12388
12393
|
audios: composition.audios,
|
|
@@ -12394,6 +12399,7 @@ async function executeRenderPipeline(input) {
|
|
|
12394
12399
|
);
|
|
12395
12400
|
const { audioOutputPath, hasAudio } = audioResult;
|
|
12396
12401
|
perfStages.audioProcessMs = audioResult.audioProcessMs;
|
|
12402
|
+
recordLimiterAttenuation(job, audioResult.audioLoweredDb);
|
|
12397
12403
|
if (audioResult.audioError) {
|
|
12398
12404
|
const audioFailures = audioResult.audioFailures ?? [];
|
|
12399
12405
|
const failureOwner = audioFailures.length === 0 ? void 0 : audioFailures.some((failure) => failure.owner === "system") ? "system" : "user";
|
|
@@ -12426,7 +12432,7 @@ async function executeRenderPipeline(input) {
|
|
|
12426
12432
|
try {
|
|
12427
12433
|
fileServer = await createFileServer({
|
|
12428
12434
|
projectDir,
|
|
12429
|
-
compiledDir:
|
|
12435
|
+
compiledDir: join25(workDir, "compiled"),
|
|
12430
12436
|
port: 0,
|
|
12431
12437
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
12432
12438
|
fps: job.config.fps
|
|
@@ -12444,7 +12450,7 @@ async function executeRenderPipeline(input) {
|
|
|
12444
12450
|
if (!activeFileServer) {
|
|
12445
12451
|
throw new Error("File server failed to initialize before frame capture");
|
|
12446
12452
|
}
|
|
12447
|
-
const framesDir =
|
|
12453
|
+
const framesDir = join25(workDir, "captured-frames");
|
|
12448
12454
|
if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
|
|
12449
12455
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
12450
12456
|
code: "start_browsers"
|
|
@@ -12894,7 +12900,7 @@ async function executeRenderPipeline(input) {
|
|
|
12894
12900
|
hls: ".mp4"
|
|
12895
12901
|
};
|
|
12896
12902
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
12897
|
-
const videoOnlyPath =
|
|
12903
|
+
const videoOnlyPath = join25(workDir, `video-only${videoExt}`);
|
|
12898
12904
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
12899
12905
|
if (usePageSideCompositingForTransitions) {
|
|
12900
12906
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -13137,9 +13143,10 @@ async function executeRenderPipeline(input) {
|
|
|
13137
13143
|
outputWidth: captureCompositionWidth ?? width,
|
|
13138
13144
|
outputHeight: captureCompositionHeight ?? height,
|
|
13139
13145
|
motionBlur: job.config.motionBlur ? JSON.stringify(job.config.motionBlur) : "",
|
|
13146
|
+
pageMotionBlur: job.config.pageMotionBlur,
|
|
13140
13147
|
captureQuality: captureOptions.quality
|
|
13141
13148
|
});
|
|
13142
|
-
const segmentDir = segmentDirFor(
|
|
13149
|
+
const segmentDir = segmentDirFor(join25(projectDir, "renders"), segmentPlanHash);
|
|
13143
13150
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
13144
13151
|
if (job.config.resumeSegments === true) {
|
|
13145
13152
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -13154,10 +13161,10 @@ async function executeRenderPipeline(input) {
|
|
|
13154
13161
|
});
|
|
13155
13162
|
}
|
|
13156
13163
|
if (completedSegments.size === 0) {
|
|
13157
|
-
|
|
13164
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13158
13165
|
}
|
|
13159
13166
|
} else {
|
|
13160
|
-
|
|
13167
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13161
13168
|
}
|
|
13162
13169
|
const segmentManifest = readSegmentManifest(segmentDir) ?? {
|
|
13163
13170
|
version: 1,
|
|
@@ -13238,7 +13245,7 @@ async function executeRenderPipeline(input) {
|
|
|
13238
13245
|
);
|
|
13239
13246
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
13240
13247
|
if (job.config.keepSegments !== true) {
|
|
13241
|
-
|
|
13248
|
+
rmSync10(segmentDir, { recursive: true, force: true });
|
|
13242
13249
|
}
|
|
13243
13250
|
} else {
|
|
13244
13251
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -13507,7 +13514,7 @@ async function executeRenderPipeline(input) {
|
|
|
13507
13514
|
"capture_disk",
|
|
13508
13515
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
13509
13516
|
);
|
|
13510
|
-
|
|
13517
|
+
rmSync10(framesDir, { recursive: true, force: true });
|
|
13511
13518
|
mkdirSync9(framesDir, { recursive: true });
|
|
13512
13519
|
resetCaptureAttemptProgress(job);
|
|
13513
13520
|
dedupPerfs.length = 0;
|
|
@@ -13694,7 +13701,7 @@ async function executeRenderPipeline(input) {
|
|
|
13694
13701
|
}
|
|
13695
13702
|
if (job.config.debug) {
|
|
13696
13703
|
if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
|
|
13697
|
-
const debugOutput =
|
|
13704
|
+
const debugOutput = join25(workDir, `output${videoExt}`);
|
|
13698
13705
|
copyFileSync4(stagedOutputPath, debugOutput);
|
|
13699
13706
|
}
|
|
13700
13707
|
}
|
|
@@ -13824,10 +13831,10 @@ import {
|
|
|
13824
13831
|
linkSync,
|
|
13825
13832
|
mkdirSync as mkdirSync13,
|
|
13826
13833
|
renameSync as renameSync6,
|
|
13827
|
-
rmSync as
|
|
13834
|
+
rmSync as rmSync12,
|
|
13828
13835
|
writeFileSync as writeFileSync8
|
|
13829
13836
|
} from "node:fs";
|
|
13830
|
-
import { extname as extname6, isAbsolute as isAbsolute3, join as
|
|
13837
|
+
import { extname as extname6, isAbsolute as isAbsolute3, join as join33, relative as relative5, sep as sep2 } from "node:path";
|
|
13831
13838
|
import {
|
|
13832
13839
|
fpsToNumber as fpsToNumber10,
|
|
13833
13840
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -13839,7 +13846,7 @@ import {
|
|
|
13839
13846
|
} from "@hyperframes/engine";
|
|
13840
13847
|
|
|
13841
13848
|
// src/services/distributed/planFrameCost.ts
|
|
13842
|
-
import { join as
|
|
13849
|
+
import { join as join28 } from "node:path";
|
|
13843
13850
|
import {
|
|
13844
13851
|
closeCaptureSession as closeCaptureSession9,
|
|
13845
13852
|
createCaptureSession as createCaptureSession9,
|
|
@@ -13849,7 +13856,7 @@ import {
|
|
|
13849
13856
|
|
|
13850
13857
|
// src/services/distributed/chunkFrames.ts
|
|
13851
13858
|
import { existsSync as existsSync18, readdirSync as readdirSync6 } from "node:fs";
|
|
13852
|
-
import { extname as extname4, join as
|
|
13859
|
+
import { extname as extname4, join as join27 } from "node:path";
|
|
13853
13860
|
import {
|
|
13854
13861
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
13855
13862
|
createFrameLookupTable as createFrameLookupTable2,
|
|
@@ -13859,7 +13866,7 @@ import {
|
|
|
13859
13866
|
|
|
13860
13867
|
// src/services/distributed/shared.ts
|
|
13861
13868
|
import { execFile as execFileCallback } from "node:child_process";
|
|
13862
|
-
import { dirname as dirname11, join as
|
|
13869
|
+
import { dirname as dirname11, join as join26 } from "node:path";
|
|
13863
13870
|
import { existsSync as existsSync17, readFileSync as readFileSync9 } from "node:fs";
|
|
13864
13871
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
13865
13872
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -13875,7 +13882,7 @@ function isPlanAudioArtifactPath(path) {
|
|
|
13875
13882
|
}
|
|
13876
13883
|
function resolvePlanAudioPath(planDir) {
|
|
13877
13884
|
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
13878
|
-
const candidate =
|
|
13885
|
+
const candidate = join26(planDir, name);
|
|
13879
13886
|
if (existsSync17(candidate)) return candidate;
|
|
13880
13887
|
}
|
|
13881
13888
|
return null;
|
|
@@ -14131,7 +14138,7 @@ function readProducerVersion() {
|
|
|
14131
14138
|
const startDir = dirname11(fileURLToPath5(import.meta.url));
|
|
14132
14139
|
let current = startDir;
|
|
14133
14140
|
for (let i = 0; i < 10; i++) {
|
|
14134
|
-
const candidate =
|
|
14141
|
+
const candidate = join26(current, "package.json");
|
|
14135
14142
|
if (existsSync17(candidate)) {
|
|
14136
14143
|
try {
|
|
14137
14144
|
const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
|
|
@@ -14184,7 +14191,7 @@ function frameNumberFromFileName(name) {
|
|
|
14184
14191
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
14185
14192
|
const result = [];
|
|
14186
14193
|
for (const v of videos) {
|
|
14187
|
-
const outputDir =
|
|
14194
|
+
const outputDir = join27(planDir, "video-frames", v.videoId);
|
|
14188
14195
|
if (!existsSync18(outputDir)) {
|
|
14189
14196
|
throw new Error(
|
|
14190
14197
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
@@ -14203,7 +14210,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
14203
14210
|
if (!frameName) continue;
|
|
14204
14211
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
14205
14212
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
14206
|
-
framePaths.set(frameIndex,
|
|
14213
|
+
framePaths.set(frameIndex, join27(outputDir, frameName));
|
|
14207
14214
|
}
|
|
14208
14215
|
result.push({
|
|
14209
14216
|
videoId: v.videoId,
|
|
@@ -14245,7 +14252,7 @@ function buildChunkCaptureOptions(input) {
|
|
|
14245
14252
|
skipReadinessVideoIds: input.skipReadinessVideoIds,
|
|
14246
14253
|
videoMetadataHints: input.videoMetadataHints,
|
|
14247
14254
|
...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
|
|
14248
|
-
pageMotionBlur: "distributed-render",
|
|
14255
|
+
pageMotionBlur: encoder.pageMotionBlur ?? "distributed-render",
|
|
14249
14256
|
// A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
|
|
14250
14257
|
lockWarmupTicks: true,
|
|
14251
14258
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
@@ -14293,14 +14300,14 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
14293
14300
|
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
14294
14301
|
return Promise.all(
|
|
14295
14302
|
deferred.map(async (video) => {
|
|
14296
|
-
const outputDir =
|
|
14303
|
+
const outputDir = join27(input.outputRoot, video.videoId);
|
|
14297
14304
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
14298
14305
|
if (indexes.length === 0) {
|
|
14299
14306
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
14300
14307
|
}
|
|
14301
14308
|
const range = video.deferredRange;
|
|
14302
14309
|
const remoteUrl = remoteSources[range.sourcePath];
|
|
14303
|
-
const sourcePath = remoteUrl ??
|
|
14310
|
+
const sourcePath = remoteUrl ?? join27(input.planDir, ...range.sourcePath.split("/"));
|
|
14304
14311
|
if (remoteUrl === void 0 && !existsSync18(sourcePath)) {
|
|
14305
14312
|
throw new RenderChunkValidationError(
|
|
14306
14313
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -14349,12 +14356,12 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14349
14356
|
planVideos,
|
|
14350
14357
|
frames: sampled,
|
|
14351
14358
|
fps: fpsRatio,
|
|
14352
|
-
outputRoot:
|
|
14359
|
+
outputRoot: join28(workDir, "frame-cost-video-frames"),
|
|
14353
14360
|
cfg: input.cfg
|
|
14354
14361
|
})
|
|
14355
14362
|
];
|
|
14356
14363
|
const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
14357
|
-
const compiledDir =
|
|
14364
|
+
const compiledDir = join28(planDir, "compiled");
|
|
14358
14365
|
const fileServer = await createFileServer({
|
|
14359
14366
|
projectDir: compiledDir,
|
|
14360
14367
|
compiledDir,
|
|
@@ -14366,7 +14373,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14366
14373
|
try {
|
|
14367
14374
|
session = await createCaptureSession9(
|
|
14368
14375
|
fileServer.url,
|
|
14369
|
-
|
|
14376
|
+
join28(workDir, "frame-cost"),
|
|
14370
14377
|
buildChunkCaptureOptions({
|
|
14371
14378
|
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
14372
14379
|
encoder: input,
|
|
@@ -14388,7 +14395,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
14388
14395
|
|
|
14389
14396
|
// src/services/render/stages/freezePlan.ts
|
|
14390
14397
|
import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
|
|
14391
|
-
import { join as
|
|
14398
|
+
import { join as join29, relative as relative3, resolve as resolve8 } from "node:path";
|
|
14392
14399
|
|
|
14393
14400
|
// src/services/distributed/planProtocol.ts
|
|
14394
14401
|
var PLAN_SCHEMA_VERSION = 1;
|
|
@@ -14606,7 +14613,7 @@ function listPlanFiles(planDir) {
|
|
|
14606
14613
|
function walk(dir) {
|
|
14607
14614
|
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
14608
14615
|
for (const entry of entries) {
|
|
14609
|
-
const full =
|
|
14616
|
+
const full = join29(dir, entry.name);
|
|
14610
14617
|
if (entry.isDirectory()) {
|
|
14611
14618
|
walk(full);
|
|
14612
14619
|
} else if (entry.isFile()) {
|
|
@@ -14647,15 +14654,15 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
|
14647
14654
|
function writePlanRemoteFiles(planDir, files) {
|
|
14648
14655
|
if (files.length === 0) return;
|
|
14649
14656
|
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
14650
|
-
writeFileSync6(
|
|
14657
|
+
writeFileSync6(join29(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
14651
14658
|
}
|
|
14652
14659
|
function readPlanRemoteFiles(planDir) {
|
|
14653
|
-
const path =
|
|
14660
|
+
const path = join29(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
14654
14661
|
return existsSync19(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
|
|
14655
14662
|
}
|
|
14656
14663
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
14657
|
-
const planJsonPath =
|
|
14658
|
-
const encoderJsonPath =
|
|
14664
|
+
const planJsonPath = join29(planDir, "plan.json");
|
|
14665
|
+
const encoderJsonPath = join29(planDir, "meta", "encoder.json");
|
|
14659
14666
|
if (!existsSync19(planJsonPath)) {
|
|
14660
14667
|
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
14661
14668
|
}
|
|
@@ -14692,18 +14699,18 @@ async function freezePlan(input) {
|
|
|
14692
14699
|
if (!existsSync19(planDir)) {
|
|
14693
14700
|
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
14694
14701
|
}
|
|
14695
|
-
const metaDir =
|
|
14702
|
+
const metaDir = join29(planDir, "meta");
|
|
14696
14703
|
if (!existsSync19(metaDir)) mkdirSync10(metaDir, { recursive: true });
|
|
14697
14704
|
writeFileSync6(
|
|
14698
|
-
|
|
14705
|
+
join29(metaDir, "composition.json"),
|
|
14699
14706
|
`${JSON.stringify(composition, null, 2)}
|
|
14700
14707
|
`,
|
|
14701
14708
|
"utf-8"
|
|
14702
14709
|
);
|
|
14703
14710
|
const encoderForCanonical = stripUndefined(encoder);
|
|
14704
14711
|
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
14705
|
-
writeFileSync6(
|
|
14706
|
-
writeFileSync6(
|
|
14712
|
+
writeFileSync6(join29(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
14713
|
+
writeFileSync6(join29(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
14707
14714
|
`, "utf-8");
|
|
14708
14715
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
14709
14716
|
const planHash = computePlanHash({
|
|
@@ -14727,7 +14734,7 @@ async function freezePlan(input) {
|
|
|
14727
14734
|
duration: durationSeconds,
|
|
14728
14735
|
hasAudio
|
|
14729
14736
|
};
|
|
14730
|
-
const planJsonPath =
|
|
14737
|
+
const planJsonPath = join29(planDir, "plan.json");
|
|
14731
14738
|
writeFileSync6(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
14732
14739
|
`, "utf-8");
|
|
14733
14740
|
return { planJsonPath, planHash };
|
|
@@ -14771,7 +14778,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
14771
14778
|
// src/services/distributed/chunkAudio.ts
|
|
14772
14779
|
import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
|
|
14773
14780
|
import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
|
|
14774
|
-
import { join as
|
|
14781
|
+
import { join as join31 } from "node:path";
|
|
14775
14782
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
14776
14783
|
|
|
14777
14784
|
// src/services/distributed/mp4Stitch.ts
|
|
@@ -14781,10 +14788,10 @@ import {
|
|
|
14781
14788
|
mkdirSync as mkdirSync11,
|
|
14782
14789
|
mkdtempSync as mkdtempSync4,
|
|
14783
14790
|
renameSync as renameSync5,
|
|
14784
|
-
rmSync as
|
|
14791
|
+
rmSync as rmSync11
|
|
14785
14792
|
} from "node:fs";
|
|
14786
14793
|
import { open } from "node:fs/promises";
|
|
14787
|
-
import { dirname as dirname12, join as
|
|
14794
|
+
import { dirname as dirname12, join as join30 } from "node:path";
|
|
14788
14795
|
import { finished, pipeline } from "node:stream/promises";
|
|
14789
14796
|
function boxAt(src, off, at, limit) {
|
|
14790
14797
|
let size = src.readUInt32BE(off);
|
|
@@ -15150,16 +15157,16 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
15150
15157
|
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
15151
15158
|
mkdirSync11(dirname12(outputPath), { recursive: true });
|
|
15152
15159
|
const workDir = mkdtempSync4(`${outputPath}.stitch-`);
|
|
15153
|
-
const out = createWriteStream(
|
|
15160
|
+
const out = createWriteStream(join30(workDir, "out.mp4"));
|
|
15154
15161
|
try {
|
|
15155
15162
|
out.write(header);
|
|
15156
15163
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
15157
15164
|
out.end();
|
|
15158
15165
|
await finished(out);
|
|
15159
|
-
renameSync5(
|
|
15166
|
+
renameSync5(join30(workDir, "out.mp4"), outputPath);
|
|
15160
15167
|
} finally {
|
|
15161
15168
|
out.destroy();
|
|
15162
|
-
|
|
15169
|
+
rmSync11(workDir, { recursive: true, force: true });
|
|
15163
15170
|
}
|
|
15164
15171
|
}
|
|
15165
15172
|
|
|
@@ -15250,7 +15257,7 @@ async function writeChunkAudioSlices(input) {
|
|
|
15250
15257
|
});
|
|
15251
15258
|
const bytes = readFileSync11(input.audioPath);
|
|
15252
15259
|
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
15253
|
-
mkdirSync12(
|
|
15260
|
+
mkdirSync12(join31(input.planDir, SLICE_DIR), { recursive: true });
|
|
15254
15261
|
input.chunks.forEach((_, index) => {
|
|
15255
15262
|
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
15256
15263
|
if (!cut) return;
|
|
@@ -15258,17 +15265,17 @@ async function writeChunkAudioSlices(input) {
|
|
|
15258
15265
|
const start = offsets[cut.firstPacket];
|
|
15259
15266
|
layout.partStarts[index] = start;
|
|
15260
15267
|
const end = offsets[last] + track.sizes[last];
|
|
15261
|
-
writeFileSync7(
|
|
15268
|
+
writeFileSync7(join31(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
15262
15269
|
});
|
|
15263
15270
|
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
15264
|
-
writeFileSync7(
|
|
15265
|
-
writeFileSync7(
|
|
15271
|
+
writeFileSync7(join31(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
15272
|
+
writeFileSync7(join31(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
15266
15273
|
}
|
|
15267
15274
|
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
15268
|
-
const layoutPath =
|
|
15275
|
+
const layoutPath = join31(planDir, SLICE_LAYOUT);
|
|
15269
15276
|
if (!existsSync20(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
15270
15277
|
const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
|
|
15271
|
-
const frame = readFileSync11(
|
|
15278
|
+
const frame = readFileSync11(join31(planDir, SLICE_FRAME));
|
|
15272
15279
|
const fd = openSync5(scratchPath, "w");
|
|
15273
15280
|
try {
|
|
15274
15281
|
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
@@ -15281,7 +15288,7 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
|
15281
15288
|
);
|
|
15282
15289
|
const start = layout.partStarts[chunkIndex];
|
|
15283
15290
|
if (start !== void 0) {
|
|
15284
|
-
const part = readFileSync11(
|
|
15291
|
+
const part = readFileSync11(join31(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
15285
15292
|
writeSync(fd, part, 0, part.length, start);
|
|
15286
15293
|
}
|
|
15287
15294
|
ftruncateSync(fd, layout.sizeBytes);
|
|
@@ -15337,7 +15344,7 @@ async function muxChunkWithAudio(input) {
|
|
|
15337
15344
|
// src/services/distributed/planSize.ts
|
|
15338
15345
|
import { createHash as createHash7 } from "node:crypto";
|
|
15339
15346
|
import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
|
|
15340
|
-
import { extname as extname5, join as
|
|
15347
|
+
import { extname as extname5, join as join32, relative as relative4 } from "node:path";
|
|
15341
15348
|
var MAX_TOP_COMPONENTS = 10;
|
|
15342
15349
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
15343
15350
|
".aac",
|
|
@@ -15449,7 +15456,7 @@ function walkRegularFiles(dir, visit) {
|
|
|
15449
15456
|
return;
|
|
15450
15457
|
}
|
|
15451
15458
|
for (const entry of entries) {
|
|
15452
|
-
const full =
|
|
15459
|
+
const full = join32(dir, entry.name);
|
|
15453
15460
|
if (entry.isDirectory()) {
|
|
15454
15461
|
walkRegularFiles(full, visit);
|
|
15455
15462
|
} else if (entry.isFile()) {
|
|
@@ -15524,7 +15531,6 @@ function chunkSizingUsesFrameCost(config) {
|
|
|
15524
15531
|
}
|
|
15525
15532
|
var CHUNK_EXCEEDS_WORKER_TIMEOUT = "CHUNK_EXCEEDS_WORKER_TIMEOUT";
|
|
15526
15533
|
var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
15527
|
-
// fallow-ignore-next-line unused-class-member
|
|
15528
15534
|
code = CHUNK_EXCEEDS_WORKER_TIMEOUT;
|
|
15529
15535
|
constructor(predictedChunkSeconds, workerTimeoutSeconds) {
|
|
15530
15536
|
super(
|
|
@@ -15587,7 +15593,6 @@ var PlanTooLargeError = class extends Error {
|
|
|
15587
15593
|
};
|
|
15588
15594
|
var FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED = "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED";
|
|
15589
15595
|
var FormatNotSupportedInDistributedError = class extends Error {
|
|
15590
|
-
// fallow-ignore-next-line unused-class-member
|
|
15591
15596
|
code = FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED;
|
|
15592
15597
|
format;
|
|
15593
15598
|
reason;
|
|
@@ -15659,13 +15664,13 @@ function assertPositiveInteger2(name, value) {
|
|
|
15659
15664
|
}
|
|
15660
15665
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
15661
15666
|
"plan.json",
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15667
|
+
join33("meta", "composition.json"),
|
|
15668
|
+
join33("meta", "encoder.json"),
|
|
15669
|
+
join33("meta", "chunks.json")
|
|
15665
15670
|
];
|
|
15666
15671
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
15667
15672
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
15668
|
-
|
|
15673
|
+
rmSync12(join33(planDir, relativePath), { force: true });
|
|
15669
15674
|
}
|
|
15670
15675
|
}
|
|
15671
15676
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -15724,6 +15729,7 @@ function buildLockedRenderConfig(input) {
|
|
|
15724
15729
|
chunkCount: input.chunkCount,
|
|
15725
15730
|
runtimeEnv: input.runtimeEnv,
|
|
15726
15731
|
variables: config.variables,
|
|
15732
|
+
pageMotionBlur: config.pageMotionBlur,
|
|
15727
15733
|
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
15728
15734
|
...input.separateAudio ? { separateAudio: true } : {}
|
|
15729
15735
|
};
|
|
@@ -15797,19 +15803,19 @@ function createVideoSourceShipper(input) {
|
|
|
15797
15803
|
const known = shipped.get(srcPath);
|
|
15798
15804
|
if (known) return known;
|
|
15799
15805
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
15800
|
-
const current = stagedRel === null ? srcPath :
|
|
15806
|
+
const current = stagedRel === null ? srcPath : join33(input.planDir, "video-frames", stagedRel);
|
|
15801
15807
|
let result = null;
|
|
15802
15808
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
15803
15809
|
const rel = pathInside(root, srcPath);
|
|
15804
|
-
if (rel !== null && (existsSync21(
|
|
15810
|
+
if (rel !== null && (existsSync21(join33(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
15805
15811
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
15806
15812
|
break;
|
|
15807
15813
|
}
|
|
15808
15814
|
}
|
|
15809
15815
|
if (result === null) {
|
|
15810
15816
|
result = `video-sources/${shipped.size}${extname6(current)}`;
|
|
15811
|
-
const destination =
|
|
15812
|
-
mkdirSync13(
|
|
15817
|
+
const destination = join33(input.planDir, ...result.split("/"));
|
|
15818
|
+
mkdirSync13(join33(input.planDir, "video-sources"), { recursive: true });
|
|
15813
15819
|
try {
|
|
15814
15820
|
linkSync(current, destination);
|
|
15815
15821
|
} catch {
|
|
@@ -15821,7 +15827,7 @@ function createVideoSourceShipper(input) {
|
|
|
15821
15827
|
};
|
|
15822
15828
|
}
|
|
15823
15829
|
function plansSeparateAudio(config, options) {
|
|
15824
|
-
return options.deferAudio === true && config.format
|
|
15830
|
+
return options.deferAudio === true && config.format !== "png-sequence" && config.cfr !== true;
|
|
15825
15831
|
}
|
|
15826
15832
|
async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
15827
15833
|
rejectUnsupportedDistributedFormat(config);
|
|
@@ -15858,14 +15864,14 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15858
15864
|
variables: config.variables
|
|
15859
15865
|
});
|
|
15860
15866
|
const entryFile = config.entryFile ?? "index.html";
|
|
15861
|
-
const htmlPath =
|
|
15867
|
+
const htmlPath = join33(projectDir, entryFile);
|
|
15862
15868
|
if (!existsSync21(htmlPath)) {
|
|
15863
15869
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
15864
15870
|
}
|
|
15865
|
-
const workDir =
|
|
15866
|
-
|
|
15871
|
+
const workDir = join33(planDir, ".plan-work");
|
|
15872
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
15867
15873
|
mkdirSync13(workDir, { recursive: true });
|
|
15868
|
-
const compiledDir =
|
|
15874
|
+
const compiledDir = join33(workDir, "compiled");
|
|
15869
15875
|
mkdirSync13(compiledDir, { recursive: true });
|
|
15870
15876
|
let stageStarted = Date.now();
|
|
15871
15877
|
cpSync2(projectDir, compiledDir, {
|
|
@@ -15879,10 +15885,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15879
15885
|
}
|
|
15880
15886
|
});
|
|
15881
15887
|
logPlanStage(log, "copy", stageStarted);
|
|
15882
|
-
const finalCompiledDir =
|
|
15888
|
+
const finalCompiledDir = join33(planDir, "compiled");
|
|
15883
15889
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
15884
15890
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
15885
|
-
const remoteLocalPath = (rel) =>
|
|
15891
|
+
const remoteLocalPath = (rel) => join33(projectDir, ...rel.split("/"));
|
|
15886
15892
|
const urlByAbsentLocalPath = new Map(
|
|
15887
15893
|
remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
|
|
15888
15894
|
);
|
|
@@ -15981,22 +15987,21 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15981
15987
|
forceScreenshot,
|
|
15982
15988
|
width,
|
|
15983
15989
|
height,
|
|
15984
|
-
probeResult,
|
|
15985
15990
|
totalFrames
|
|
15986
15991
|
};
|
|
15987
15992
|
}
|
|
15988
|
-
function mixPlanAudio(c, compiledDir
|
|
15993
|
+
function mixPlanAudio(c, compiledDir) {
|
|
15989
15994
|
return runAudioStage({
|
|
15990
15995
|
projectDir: c.projectDir,
|
|
15991
15996
|
workDir: c.workDir,
|
|
15992
15997
|
compiledDir,
|
|
15993
15998
|
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
15994
|
-
|
|
15999
|
+
totalFrames: c.totalFrames,
|
|
16000
|
+
fps: c.job.config.fps,
|
|
15995
16001
|
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
15996
16002
|
audioGain: c.cfg.audioGain,
|
|
15997
16003
|
audios: c.composition.audios,
|
|
15998
16004
|
masterFxChain: c.composition.masterFxChain,
|
|
15999
|
-
...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
|
|
16000
16005
|
abortSignal: c.abortSignal,
|
|
16001
16006
|
assertNotAborted: c.assertNotAborted
|
|
16002
16007
|
});
|
|
@@ -16020,16 +16025,6 @@ async function extractPlanVideos(c, deferRangeExtraction) {
|
|
|
16020
16025
|
}
|
|
16021
16026
|
return extractResult;
|
|
16022
16027
|
}
|
|
16023
|
-
async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
|
|
16024
|
-
const peak = await constrainAacTruePeak({
|
|
16025
|
-
audioPath,
|
|
16026
|
-
...existsSync21(pcmPath) ? { sourcePath: pcmPath } : {},
|
|
16027
|
-
targetDurationSeconds: seconds,
|
|
16028
|
-
signal: c.abortSignal,
|
|
16029
|
-
timeoutMs: c.cfg.ffmpegProcessTimeout
|
|
16030
|
-
}).finally(() => rmSync11(pcmPath, { force: true }));
|
|
16031
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
16032
|
-
}
|
|
16033
16028
|
async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
|
|
16034
16029
|
const planDir = executionPlanDir;
|
|
16035
16030
|
const c = await compilePlanComposition(projectDir, config, planDir, options);
|
|
@@ -16051,7 +16046,6 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16051
16046
|
forceScreenshot,
|
|
16052
16047
|
width,
|
|
16053
16048
|
height,
|
|
16054
|
-
probeResult,
|
|
16055
16049
|
totalFrames
|
|
16056
16050
|
} = c;
|
|
16057
16051
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
@@ -16067,17 +16061,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16067
16061
|
let stageStarted = Date.now();
|
|
16068
16062
|
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
16069
16063
|
logPlanStage(log, "extract", stageStarted);
|
|
16070
|
-
const stagedVideoFrames =
|
|
16071
|
-
const videoFramesDst =
|
|
16072
|
-
if (existsSync21(videoFramesDst))
|
|
16073
|
-
const videoSourcesDir =
|
|
16074
|
-
|
|
16064
|
+
const stagedVideoFrames = join33(compiledDir, "__hyperframes_video_frames");
|
|
16065
|
+
const videoFramesDst = join33(planDir, "video-frames");
|
|
16066
|
+
if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
|
|
16067
|
+
const videoSourcesDir = join33(planDir, "video-sources");
|
|
16068
|
+
rmSync12(videoSourcesDir, { recursive: true, force: true });
|
|
16075
16069
|
if (existsSync21(stagedVideoFrames)) {
|
|
16076
16070
|
renameSync6(stagedVideoFrames, videoFramesDst);
|
|
16077
16071
|
} else {
|
|
16078
16072
|
mkdirSync13(videoFramesDst, { recursive: true });
|
|
16079
16073
|
}
|
|
16080
|
-
if (existsSync21(finalCompiledDir))
|
|
16074
|
+
if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
|
|
16081
16075
|
renameSync6(compiledDir, finalCompiledDir);
|
|
16082
16076
|
const shipVideoSource = createVideoSourceShipper({
|
|
16083
16077
|
planDir,
|
|
@@ -16107,7 +16101,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16107
16101
|
};
|
|
16108
16102
|
})
|
|
16109
16103
|
});
|
|
16110
|
-
mkdirSync13(
|
|
16104
|
+
mkdirSync13(join33(planDir, "meta"), { recursive: true });
|
|
16111
16105
|
writePlanRemoteFiles(
|
|
16112
16106
|
planDir,
|
|
16113
16107
|
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
@@ -16117,23 +16111,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16117
16111
|
}))
|
|
16118
16112
|
);
|
|
16119
16113
|
writeFileSync8(
|
|
16120
|
-
|
|
16114
|
+
join33(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
16121
16115
|
JSON.stringify(planVideosJson, null, 2),
|
|
16122
16116
|
"utf-8"
|
|
16123
16117
|
);
|
|
16124
16118
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
16125
16119
|
const separateAudio = plansSeparateAudio(config, options);
|
|
16126
16120
|
const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
|
|
16127
|
-
const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
|
|
16128
|
-
const planMixPcmPath = join34(workDir, "plan-mix.wav");
|
|
16129
16121
|
stageStarted = Date.now();
|
|
16130
16122
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
16131
|
-
separateAudio ? null : mixPlanAudio(
|
|
16132
|
-
c,
|
|
16133
|
-
finalCompiledDir,
|
|
16134
|
-
audioSeconds,
|
|
16135
|
-
chunksCarryAudio ? planMixPcmPath : void 0
|
|
16136
|
-
).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
16123
|
+
separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
16137
16124
|
(async () => {
|
|
16138
16125
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
16139
16126
|
await prehashPlanFiles(dir, digests);
|
|
@@ -16186,14 +16173,11 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16186
16173
|
if (audioResult?.audioError) {
|
|
16187
16174
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
16188
16175
|
}
|
|
16189
|
-
const planAudioPath =
|
|
16176
|
+
const planAudioPath = join33(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
16190
16177
|
if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
|
|
16191
16178
|
renameSync6(audioResult.audioOutputPath, planAudioPath);
|
|
16192
16179
|
}
|
|
16193
16180
|
if (chunksCarryAudio && existsSync21(planAudioPath)) {
|
|
16194
|
-
stageStarted = Date.now();
|
|
16195
|
-
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
16196
|
-
logPlanStage(log, "audio-peak", stageStarted);
|
|
16197
16181
|
if (options.sliceChunkAudio) {
|
|
16198
16182
|
stageStarted = Date.now();
|
|
16199
16183
|
await writeChunkAudioSlices({
|
|
@@ -16238,7 +16222,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
16238
16222
|
format: config.format
|
|
16239
16223
|
};
|
|
16240
16224
|
try {
|
|
16241
|
-
|
|
16225
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
16242
16226
|
} catch (err) {
|
|
16243
16227
|
log.warn("[plan] failed to remove temp work dir", {
|
|
16244
16228
|
workDir,
|
|
@@ -16343,6 +16327,9 @@ function validateDistributedRenderConfig(config) {
|
|
|
16343
16327
|
}
|
|
16344
16328
|
validateIntDimension("config.width", config.width);
|
|
16345
16329
|
validateIntDimension("config.height", config.height);
|
|
16330
|
+
if (config.pageMotionBlur !== void 0 && config.pageMotionBlur !== "off") {
|
|
16331
|
+
throw new InvalidConfigError("config.pageMotionBlur", 'must be "off" when provided');
|
|
16332
|
+
}
|
|
16346
16333
|
if (!ALLOWED_FORMATS.includes(config.format)) {
|
|
16347
16334
|
throw new InvalidConfigError(
|
|
16348
16335
|
"config.format",
|
|
@@ -16716,6 +16703,7 @@ function distributedConfigFromRequest(request, runtime = {}) {
|
|
|
16716
16703
|
codec: distributed.codec,
|
|
16717
16704
|
quality: options.quality,
|
|
16718
16705
|
crf: options.crf,
|
|
16706
|
+
pageMotionBlur: options.pageMotionBlur,
|
|
16719
16707
|
bitrate: options.videoBitrate,
|
|
16720
16708
|
videoFrameFormat: options.videoFrameFormat,
|
|
16721
16709
|
outputResolution: options.outputResolution,
|
|
@@ -16762,6 +16750,7 @@ function renderRequestFromDistributedConfig(input) {
|
|
|
16762
16750
|
format: config.format,
|
|
16763
16751
|
distributed,
|
|
16764
16752
|
...optionalProperty("crf", config.crf),
|
|
16753
|
+
...optionalProperty("pageMotionBlur", config.pageMotionBlur),
|
|
16765
16754
|
...optionalProperty("videoBitrate", config.bitrate),
|
|
16766
16755
|
...optionalProperty("videoFrameFormat", config.videoFrameFormat),
|
|
16767
16756
|
...optionalProperty("outputResolution", config.outputResolution),
|
|
@@ -16808,10 +16797,10 @@ import {
|
|
|
16808
16797
|
statSync as statSync8,
|
|
16809
16798
|
mkdtempSync as mkdtempSync5,
|
|
16810
16799
|
writeFileSync as writeFileSync9,
|
|
16811
|
-
rmSync as
|
|
16800
|
+
rmSync as rmSync13,
|
|
16812
16801
|
createReadStream as createReadStream5
|
|
16813
16802
|
} from "node:fs";
|
|
16814
|
-
import { resolve as resolve10, dirname as dirname14, join as
|
|
16803
|
+
import { resolve as resolve10, dirname as dirname14, join as join36 } from "node:path";
|
|
16815
16804
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
16816
16805
|
import { parseArgs } from "node:util";
|
|
16817
16806
|
import crypto from "node:crypto";
|
|
@@ -16829,7 +16818,7 @@ import {
|
|
|
16829
16818
|
closeSync as closeSync7,
|
|
16830
16819
|
constants as constants3
|
|
16831
16820
|
} from "node:fs";
|
|
16832
|
-
import { resolve as resolve9, join as
|
|
16821
|
+
import { resolve as resolve9, join as join34, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
|
|
16833
16822
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
16834
16823
|
function isStringRecord(value) {
|
|
16835
16824
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -16897,7 +16886,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
16897
16886
|
}
|
|
16898
16887
|
}
|
|
16899
16888
|
return {
|
|
16900
|
-
error: `No HTML entry file found in project directory: ${
|
|
16889
|
+
error: `No HTML entry file found in project directory: ${join34(absProjectDir, preferredEntryFile || "index.html")}`
|
|
16901
16890
|
};
|
|
16902
16891
|
}
|
|
16903
16892
|
function prepareHyperframeLintBody(body) {
|
|
@@ -16943,7 +16932,7 @@ async function runHyperframeLint(prepared) {
|
|
|
16943
16932
|
// src/services/healthWorker.ts
|
|
16944
16933
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
16945
16934
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
16946
|
-
import { dirname as dirname13, join as
|
|
16935
|
+
import { dirname as dirname13, join as join35 } from "node:path";
|
|
16947
16936
|
import { existsSync as existsSync23 } from "node:fs";
|
|
16948
16937
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
16949
16938
|
async function startHealthWorker(options = {}) {
|
|
@@ -17019,7 +17008,7 @@ function defaultLogger2() {
|
|
|
17019
17008
|
}
|
|
17020
17009
|
function resolveWorkerEntry2() {
|
|
17021
17010
|
const here = dirname13(fileURLToPath6(import.meta.url));
|
|
17022
|
-
const candidates = [
|
|
17011
|
+
const candidates = [join35(here, "healthWorkerThread.js"), join35(here, "healthWorkerThread.ts")];
|
|
17023
17012
|
for (const candidate of candidates) {
|
|
17024
17013
|
if (existsSync23(candidate)) return candidate;
|
|
17025
17014
|
}
|
|
@@ -17157,6 +17146,7 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
17157
17146
|
outputResolution: input.outputResolution,
|
|
17158
17147
|
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
|
17159
17148
|
videoFrameFormat: input.videoFrameFormat,
|
|
17149
|
+
pageMotionBlur: input.pageMotionBlur,
|
|
17160
17150
|
hdrMode: toRenderHdrMode(input.outputDynamicRange)
|
|
17161
17151
|
}
|
|
17162
17152
|
});
|
|
@@ -17232,8 +17222,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
17232
17222
|
}
|
|
17233
17223
|
function materializeInlineProject(html, options) {
|
|
17234
17224
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
17235
|
-
const tempProjectDir = mkdtempSync5(
|
|
17236
|
-
writeFileSync9(
|
|
17225
|
+
const tempProjectDir = mkdtempSync5(join36(tempRoot, "producer-project-"));
|
|
17226
|
+
writeFileSync9(join36(tempProjectDir, "index.html"), html, "utf-8");
|
|
17237
17227
|
return {
|
|
17238
17228
|
prepared: {
|
|
17239
17229
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -17244,7 +17234,11 @@ function materializeInlineProject(html, options) {
|
|
|
17244
17234
|
async function prepareRenderBody(body) {
|
|
17245
17235
|
const overrideError = validateRenderOverrides(body);
|
|
17246
17236
|
if (overrideError) return { error: overrideError };
|
|
17247
|
-
const
|
|
17237
|
+
const pageMotionBlur = body.pageMotionBlur;
|
|
17238
|
+
if (pageMotionBlur !== void 0 && pageMotionBlur !== "off") {
|
|
17239
|
+
return { error: "pageMotionBlur must be off when supplied" };
|
|
17240
|
+
}
|
|
17241
|
+
const options = { ...parseRenderOptions(body), pageMotionBlur };
|
|
17248
17242
|
const project = prepareProjectDirectory(body.projectDir, options);
|
|
17249
17243
|
if (project) return project;
|
|
17250
17244
|
const source = await resolveInlineRenderHtml(body);
|
|
@@ -17290,7 +17284,7 @@ function createArtifactStore(ttlMs) {
|
|
|
17290
17284
|
function cleanupTempDir(dir, log) {
|
|
17291
17285
|
if (!dir) return;
|
|
17292
17286
|
try {
|
|
17293
|
-
|
|
17287
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
17294
17288
|
} catch (error) {
|
|
17295
17289
|
log.warn("Failed to cleanup temp project dir", {
|
|
17296
17290
|
cleanupProjectDir: dir,
|
|
@@ -17699,12 +17693,12 @@ import {
|
|
|
17699
17693
|
readFileSync as readFileSync13,
|
|
17700
17694
|
readdirSync as readdirSync9,
|
|
17701
17695
|
renameSync as renameSync8,
|
|
17702
|
-
rmSync as
|
|
17696
|
+
rmSync as rmSync15,
|
|
17703
17697
|
statSync as statSync10,
|
|
17704
17698
|
writeFileSync as writeFileSync11
|
|
17705
17699
|
} from "node:fs";
|
|
17706
17700
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
17707
|
-
import { dirname as dirname16, isAbsolute as isAbsolute5, join as
|
|
17701
|
+
import { dirname as dirname16, isAbsolute as isAbsolute5, join as join39, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
|
|
17708
17702
|
import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
|
|
17709
17703
|
import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
|
|
17710
17704
|
|
|
@@ -17716,11 +17710,11 @@ import {
|
|
|
17716
17710
|
mkdirSync as mkdirSync15,
|
|
17717
17711
|
mkdtempSync as mkdtempSync6,
|
|
17718
17712
|
renameSync as renameSync7,
|
|
17719
|
-
rmSync as
|
|
17713
|
+
rmSync as rmSync14,
|
|
17720
17714
|
statSync as statSync9,
|
|
17721
17715
|
writeFileSync as writeFileSync10
|
|
17722
17716
|
} from "node:fs";
|
|
17723
|
-
import { dirname as dirname15, join as
|
|
17717
|
+
import { dirname as dirname15, join as join38 } from "node:path";
|
|
17724
17718
|
|
|
17725
17719
|
// src/services/distributed/planV2Errors.ts
|
|
17726
17720
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -17735,7 +17729,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
17735
17729
|
};
|
|
17736
17730
|
|
|
17737
17731
|
// src/services/distributed/planV2Layout.ts
|
|
17738
|
-
import { join as
|
|
17732
|
+
import { join as join37 } from "node:path";
|
|
17739
17733
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
17740
17734
|
function assertPlanV2Sha256(value, field) {
|
|
17741
17735
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -17745,7 +17739,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
17745
17739
|
}
|
|
17746
17740
|
function planV2BlobPath(root, sha256) {
|
|
17747
17741
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
17748
|
-
return
|
|
17742
|
+
return join37(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
17749
17743
|
}
|
|
17750
17744
|
|
|
17751
17745
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -17786,7 +17780,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
17786
17780
|
this.destinationDir = destinationDir;
|
|
17787
17781
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
17788
17782
|
mkdirSync15(dirname15(destinationDir), { recursive: true });
|
|
17789
|
-
this.temporaryDir = mkdtempSync6(
|
|
17783
|
+
this.temporaryDir = mkdtempSync6(join38(dirname15(destinationDir), ".plan-v2-publish-"));
|
|
17790
17784
|
}
|
|
17791
17785
|
async putBlob(blob) {
|
|
17792
17786
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
@@ -17799,8 +17793,8 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
17799
17793
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
17800
17794
|
if (existsSync25(destinationPath)) return;
|
|
17801
17795
|
mkdirSync15(dirname15(destinationPath), { recursive: true });
|
|
17802
|
-
const stagingDir = mkdtempSync6(
|
|
17803
|
-
const temporaryPath =
|
|
17796
|
+
const stagingDir = mkdtempSync6(join38(dirname15(destinationPath), ".plan-v2-blob-"));
|
|
17797
|
+
const temporaryPath = join38(stagingDir, "blob");
|
|
17804
17798
|
try {
|
|
17805
17799
|
try {
|
|
17806
17800
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
@@ -17810,7 +17804,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
17810
17804
|
}
|
|
17811
17805
|
renameSync7(temporaryPath, destinationPath);
|
|
17812
17806
|
} finally {
|
|
17813
|
-
|
|
17807
|
+
rmSync14(stagingDir, { recursive: true, force: true });
|
|
17814
17808
|
}
|
|
17815
17809
|
}
|
|
17816
17810
|
async commitManifest(manifestBytes) {
|
|
@@ -17821,13 +17815,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
17821
17815
|
);
|
|
17822
17816
|
}
|
|
17823
17817
|
}
|
|
17824
|
-
writeFileSync10(
|
|
17818
|
+
writeFileSync10(join38(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
17825
17819
|
renameSync7(this.temporaryDir, this.destinationDir);
|
|
17826
17820
|
this.#committed = true;
|
|
17827
17821
|
}
|
|
17828
17822
|
async abort() {
|
|
17829
17823
|
if (!this.#committed) {
|
|
17830
|
-
|
|
17824
|
+
rmSync14(this.temporaryDir, { recursive: true, force: true });
|
|
17831
17825
|
}
|
|
17832
17826
|
}
|
|
17833
17827
|
};
|
|
@@ -17866,7 +17860,7 @@ function listFiles(root) {
|
|
|
17866
17860
|
const rootResolved = resolve11(root);
|
|
17867
17861
|
function walk(dir) {
|
|
17868
17862
|
for (const entry of readdirSync9(dir, { withFileTypes: true })) {
|
|
17869
|
-
const absolutePath =
|
|
17863
|
+
const absolutePath = join39(dir, entry.name);
|
|
17870
17864
|
if (entry.isDirectory()) {
|
|
17871
17865
|
walk(absolutePath);
|
|
17872
17866
|
} else if (entry.isFile()) {
|
|
@@ -17893,13 +17887,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
17893
17887
|
}
|
|
17894
17888
|
}
|
|
17895
17889
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
17896
|
-
const videoRoot =
|
|
17890
|
+
const videoRoot = join39(executionPlanDir, "video-frames");
|
|
17897
17891
|
if (!existsSync26(videoRoot)) return;
|
|
17898
17892
|
for (const videoEntry of readdirSync9(videoRoot, { withFileTypes: true })) {
|
|
17899
17893
|
if (!videoEntry.isDirectory()) continue;
|
|
17900
|
-
const videoDir =
|
|
17894
|
+
const videoDir = join39(videoRoot, videoEntry.name);
|
|
17901
17895
|
if (readdirSync9(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
17902
|
-
const sentinelPath =
|
|
17896
|
+
const sentinelPath = join39(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
17903
17897
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
17904
17898
|
}
|
|
17905
17899
|
}
|
|
@@ -17946,7 +17940,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
17946
17940
|
if (video.deferredRange) {
|
|
17947
17941
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
17948
17942
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
17949
|
-
framePaths2.set(index,
|
|
17943
|
+
framePaths2.set(index, join39(outputDir, String(index)));
|
|
17950
17944
|
}
|
|
17951
17945
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
17952
17946
|
}
|
|
@@ -17954,7 +17948,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
17954
17948
|
const framePaths = /* @__PURE__ */ new Map();
|
|
17955
17949
|
for (const frameName of frameNames) {
|
|
17956
17950
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
17957
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
17951
|
+
assertValidExtractionCacheCompleteSentinel(join39(outputDir, frameName));
|
|
17958
17952
|
continue;
|
|
17959
17953
|
}
|
|
17960
17954
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -17973,7 +17967,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
17973
17967
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
17974
17968
|
);
|
|
17975
17969
|
}
|
|
17976
|
-
framePaths.set(frameIndex,
|
|
17970
|
+
framePaths.set(frameIndex, join39(outputDir, frameName));
|
|
17977
17971
|
}
|
|
17978
17972
|
return {
|
|
17979
17973
|
...video,
|
|
@@ -17994,7 +17988,7 @@ function parsePlanVideosJson2(value) {
|
|
|
17994
17988
|
}
|
|
17995
17989
|
}
|
|
17996
17990
|
function materializeExtractedVideoDirectories(planDir) {
|
|
17997
|
-
const videosPath =
|
|
17991
|
+
const videosPath = join39(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
17998
17992
|
if (!existsSync26(videosPath)) return;
|
|
17999
17993
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
18000
17994
|
for (const video of videos.extracted) {
|
|
@@ -18023,13 +18017,13 @@ function parseChunkSlices(value) {
|
|
|
18023
18017
|
});
|
|
18024
18018
|
}
|
|
18025
18019
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
18026
|
-
const videoRoot =
|
|
18020
|
+
const videoRoot = join39(executionPlanDir, "video-frames");
|
|
18027
18021
|
const hasExtractedFrames = existsSync26(videoRoot) && listFiles(videoRoot).length > 0;
|
|
18028
|
-
const videosPath =
|
|
18022
|
+
const videosPath = join39(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18029
18023
|
if (!existsSync26(videosPath)) {
|
|
18030
18024
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
18031
18025
|
}
|
|
18032
|
-
const chunksPath =
|
|
18026
|
+
const chunksPath = join39(executionPlanDir, "meta", "chunks.json");
|
|
18033
18027
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
18034
18028
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
18035
18029
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -18085,13 +18079,13 @@ function computeManifestHash(manifest) {
|
|
|
18085
18079
|
function writeBlob(sourcePath, destinationPath) {
|
|
18086
18080
|
if (existsSync26(destinationPath)) return;
|
|
18087
18081
|
mkdirSync16(dirname16(destinationPath), { recursive: true });
|
|
18088
|
-
const temporaryDir = mkdtempSync7(
|
|
18089
|
-
const temporaryPath =
|
|
18082
|
+
const temporaryDir = mkdtempSync7(join39(dirname16(destinationPath), ".plan-v2-blob-"));
|
|
18083
|
+
const temporaryPath = join39(temporaryDir, "blob");
|
|
18090
18084
|
try {
|
|
18091
18085
|
copyFileSync7(sourcePath, temporaryPath);
|
|
18092
18086
|
renameSync8(temporaryPath, destinationPath);
|
|
18093
18087
|
} finally {
|
|
18094
|
-
|
|
18088
|
+
rmSync15(temporaryDir, { recursive: true, force: true });
|
|
18095
18089
|
}
|
|
18096
18090
|
}
|
|
18097
18091
|
function publishBlobFor(absolutePath, digests) {
|
|
@@ -18122,7 +18116,7 @@ async function putTasks(tasks) {
|
|
|
18122
18116
|
if (failure) throw failure.reason;
|
|
18123
18117
|
}
|
|
18124
18118
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
18125
|
-
const executionPlanPath =
|
|
18119
|
+
const executionPlanPath = join39(executionPlanDir, "plan.json");
|
|
18126
18120
|
if (!existsSync26(executionPlanPath)) {
|
|
18127
18121
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
18128
18122
|
}
|
|
@@ -18150,7 +18144,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
18150
18144
|
}
|
|
18151
18145
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
18152
18146
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
18153
|
-
const encoder = readJsonFile(
|
|
18147
|
+
const encoder = readJsonFile(join39(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
18154
18148
|
const files = listFiles(executionPlanDir);
|
|
18155
18149
|
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
18156
18150
|
for (const file of files) {
|
|
@@ -18211,20 +18205,20 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
18211
18205
|
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
18212
18206
|
}
|
|
18213
18207
|
mkdirSync16(dirname16(planV2Dir), { recursive: true });
|
|
18214
|
-
const tempDir = mkdtempSync7(
|
|
18208
|
+
const tempDir = mkdtempSync7(join39(dirname16(planV2Dir), ".plan-v2-build-"));
|
|
18215
18209
|
try {
|
|
18216
18210
|
for (const blob of publication.blobs) {
|
|
18217
18211
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
18218
18212
|
}
|
|
18219
18213
|
writeFileSync11(
|
|
18220
|
-
|
|
18214
|
+
join39(tempDir, "plan.json"),
|
|
18221
18215
|
canonicalJsonStringify(publication.manifest),
|
|
18222
18216
|
"utf-8"
|
|
18223
18217
|
);
|
|
18224
18218
|
renameSync8(tempDir, planV2Dir);
|
|
18225
18219
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
18226
18220
|
} catch (error) {
|
|
18227
|
-
|
|
18221
|
+
rmSync15(tempDir, { recursive: true, force: true });
|
|
18228
18222
|
throw error;
|
|
18229
18223
|
}
|
|
18230
18224
|
}
|
|
@@ -18264,7 +18258,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
|
18264
18258
|
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
18265
18259
|
}
|
|
18266
18260
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
18267
|
-
const stagingRoot = mkdtempSync7(
|
|
18261
|
+
const stagingRoot = mkdtempSync7(join39(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
|
|
18268
18262
|
const digests = /* @__PURE__ */ new Map();
|
|
18269
18263
|
try {
|
|
18270
18264
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -18275,7 +18269,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
18275
18269
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
18276
18270
|
whileAudioMixes: () => putBlobs(
|
|
18277
18271
|
publisher,
|
|
18278
|
-
listFiles(
|
|
18272
|
+
listFiles(join39(stagingRoot, "compiled")).map(
|
|
18279
18273
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
18280
18274
|
)
|
|
18281
18275
|
),
|
|
@@ -18291,7 +18285,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
18291
18285
|
}
|
|
18292
18286
|
throw error;
|
|
18293
18287
|
} finally {
|
|
18294
|
-
|
|
18288
|
+
rmSync15(stagingRoot, { recursive: true, force: true });
|
|
18295
18289
|
}
|
|
18296
18290
|
}
|
|
18297
18291
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -18307,7 +18301,7 @@ async function planV2(projectDir, config, planV2Dir) {
|
|
|
18307
18301
|
function resultFromManifest(planV2Dir, manifest) {
|
|
18308
18302
|
return {
|
|
18309
18303
|
planDir: planV2Dir,
|
|
18310
|
-
manifestPath:
|
|
18304
|
+
manifestPath: join39(planV2Dir, "plan.json"),
|
|
18311
18305
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
18312
18306
|
planHash: manifest.planHash,
|
|
18313
18307
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -18443,7 +18437,7 @@ function parsePlanV2Manifest(value) {
|
|
|
18443
18437
|
return parsed;
|
|
18444
18438
|
}
|
|
18445
18439
|
function readPlanV2Manifest(planV2Dir) {
|
|
18446
|
-
const manifestPath =
|
|
18440
|
+
const manifestPath = join39(planV2Dir, "plan.json");
|
|
18447
18441
|
if (!existsSync26(manifestPath)) {
|
|
18448
18442
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
18449
18443
|
}
|
|
@@ -18489,16 +18483,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
18489
18483
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
18490
18484
|
}));
|
|
18491
18485
|
mkdirSync16(dirname16(destinationDir), { recursive: true });
|
|
18492
|
-
const tempDir = mkdtempSync7(
|
|
18486
|
+
const tempDir = mkdtempSync7(join39(dirname16(destinationDir), ".plan-v2-materialize-"));
|
|
18493
18487
|
try {
|
|
18494
18488
|
for (const { artifact, sourcePath } of verified) {
|
|
18495
|
-
const destinationPath =
|
|
18489
|
+
const destinationPath = join39(tempDir, ...artifact.path.split("/"));
|
|
18496
18490
|
mkdirSync16(dirname16(destinationPath), { recursive: true });
|
|
18497
18491
|
copyFileSync7(sourcePath, destinationPath);
|
|
18498
18492
|
}
|
|
18499
18493
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
18500
18494
|
writeFileSync11(
|
|
18501
|
-
|
|
18495
|
+
join39(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
18502
18496
|
canonicalJsonStringify({
|
|
18503
18497
|
manifest,
|
|
18504
18498
|
target,
|
|
@@ -18508,7 +18502,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
18508
18502
|
);
|
|
18509
18503
|
renameSync8(tempDir, destinationDir);
|
|
18510
18504
|
} catch (error) {
|
|
18511
|
-
|
|
18505
|
+
rmSync15(tempDir, { recursive: true, force: true });
|
|
18512
18506
|
throw error;
|
|
18513
18507
|
}
|
|
18514
18508
|
return {
|
|
@@ -18521,7 +18515,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
18521
18515
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
18522
18516
|
// before the container change carries the legacy name, and materializing it
|
|
18523
18517
|
// under the new one would point at a file that was never written.
|
|
18524
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
18518
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join39(destinationDir, audioArtifact.path) : null : null
|
|
18525
18519
|
};
|
|
18526
18520
|
}
|
|
18527
18521
|
function assertRemoteSources(remoteSources, artifacts) {
|
|
@@ -18540,7 +18534,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
18540
18534
|
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
18541
18535
|
}
|
|
18542
18536
|
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
18543
|
-
const markerPath =
|
|
18537
|
+
const markerPath = join39(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
18544
18538
|
if (!existsSync26(markerPath)) return null;
|
|
18545
18539
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
18546
18540
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
@@ -18558,7 +18552,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
18558
18552
|
assertRemoteSources(remoteSources, required);
|
|
18559
18553
|
for (const artifact of required) {
|
|
18560
18554
|
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
18561
|
-
const path =
|
|
18555
|
+
const path = join39(planDir, ...artifact.path.split("/"));
|
|
18562
18556
|
if (!existsSync26(path) || statSync10(path).size !== artifact.sizeBytes) {
|
|
18563
18557
|
throw new PlanV2IntegrityError(
|
|
18564
18558
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
@@ -18572,9 +18566,9 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
18572
18566
|
}
|
|
18573
18567
|
|
|
18574
18568
|
// src/services/distributed/planV2Execution.ts
|
|
18575
|
-
import { mkdtempSync as mkdtempSync9, rmSync as
|
|
18569
|
+
import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
|
|
18576
18570
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
18577
|
-
import { join as
|
|
18571
|
+
import { join as join42 } from "node:path";
|
|
18578
18572
|
|
|
18579
18573
|
// src/services/distributed/assemble.ts
|
|
18580
18574
|
import {
|
|
@@ -18585,11 +18579,11 @@ import {
|
|
|
18585
18579
|
readFileSync as readFileSync14,
|
|
18586
18580
|
readdirSync as readdirSync10,
|
|
18587
18581
|
renameSync as renameSync9,
|
|
18588
|
-
rmSync as
|
|
18582
|
+
rmSync as rmSync16,
|
|
18589
18583
|
statSync as statSync11,
|
|
18590
18584
|
writeFileSync as writeFileSync12
|
|
18591
18585
|
} from "node:fs";
|
|
18592
|
-
import { dirname as dirname17, join as
|
|
18586
|
+
import { dirname as dirname17, join as join40 } from "node:path";
|
|
18593
18587
|
import {
|
|
18594
18588
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
18595
18589
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
@@ -18651,7 +18645,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
18651
18645
|
};
|
|
18652
18646
|
try {
|
|
18653
18647
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
18654
|
-
const finalPath =
|
|
18648
|
+
const finalPath = join40(workDir, `output.${format}`);
|
|
18655
18649
|
const audio = audioPath !== null && existsSync27(audioPath) ? audioPath : null;
|
|
18656
18650
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
18657
18651
|
renameSync9(finalPath, outputPath);
|
|
@@ -18662,7 +18656,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
18662
18656
|
});
|
|
18663
18657
|
} finally {
|
|
18664
18658
|
try {
|
|
18665
|
-
|
|
18659
|
+
rmSync16(workDir, { recursive: true, force: true });
|
|
18666
18660
|
} catch (err) {
|
|
18667
18661
|
log.warn("[assemble] failed to remove work dir", {
|
|
18668
18662
|
workDir,
|
|
@@ -18679,8 +18673,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
18679
18673
|
};
|
|
18680
18674
|
}
|
|
18681
18675
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
18682
|
-
const planJsonPath =
|
|
18683
|
-
const chunksJsonPath =
|
|
18676
|
+
const planJsonPath = join40(planDir, "plan.json");
|
|
18677
|
+
const chunksJsonPath = join40(planDir, "meta", "chunks.json");
|
|
18684
18678
|
if (!existsSync27(planJsonPath)) {
|
|
18685
18679
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
18686
18680
|
}
|
|
@@ -18712,7 +18706,7 @@ function videoInputArgs(ctx, videoSources) {
|
|
|
18712
18706
|
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
18713
18707
|
}
|
|
18714
18708
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
18715
|
-
const cfrOutputPath =
|
|
18709
|
+
const cfrOutputPath = join40(ctx.workDir, `cfr.${ctx.format}`);
|
|
18716
18710
|
const cfrArgs = [
|
|
18717
18711
|
...videoInputArgs(ctx, chunkPaths),
|
|
18718
18712
|
"-c:v",
|
|
@@ -18750,7 +18744,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
18750
18744
|
);
|
|
18751
18745
|
return;
|
|
18752
18746
|
}
|
|
18753
|
-
const paddedAudioPath =
|
|
18747
|
+
const paddedAudioPath = join40(ctx.workDir, "audio-padded.m4a");
|
|
18754
18748
|
const padTrimResult = await ctx.timed(
|
|
18755
18749
|
"audioMs",
|
|
18756
18750
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -18790,7 +18784,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
18790
18784
|
}
|
|
18791
18785
|
}
|
|
18792
18786
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
18793
|
-
const listPath =
|
|
18787
|
+
const listPath = join40(workDir, "concat-list.txt");
|
|
18794
18788
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
18795
18789
|
writeFileSync12(listPath, `${body}
|
|
18796
18790
|
`, "utf-8");
|
|
@@ -18818,6 +18812,7 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
18818
18812
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
18819
18813
|
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
18820
18814
|
if (!chunksCarryAudio && !separateAudio) return false;
|
|
18815
|
+
if (planFormat(planDir) !== "mp4") return false;
|
|
18821
18816
|
if (cfr) {
|
|
18822
18817
|
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
|
|
18823
18818
|
}
|
|
@@ -18826,8 +18821,12 @@ function chunksAreStitchedMp4(planDir, cfr) {
|
|
|
18826
18821
|
function stitchesSeparateAudio(planDir) {
|
|
18827
18822
|
return lockedAudio(planDir).separateAudio === true;
|
|
18828
18823
|
}
|
|
18824
|
+
function planFormat(planDir) {
|
|
18825
|
+
const plan2 = JSON.parse(readFileSync14(join40(planDir, "plan.json"), "utf-8"));
|
|
18826
|
+
return plan2.dimensions.format;
|
|
18827
|
+
}
|
|
18829
18828
|
function lockedAudio(planDir) {
|
|
18830
|
-
const encoderJsonPath =
|
|
18829
|
+
const encoderJsonPath = join40(planDir, "meta", "encoder.json");
|
|
18831
18830
|
if (!existsSync27(encoderJsonPath)) return {};
|
|
18832
18831
|
return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
|
|
18833
18832
|
}
|
|
@@ -18837,7 +18836,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
18837
18836
|
`[assemble] cfr=true is only supported for format="mp4" (got "${format}"). Stream-copy paths for webm and mov already produce exact avg_frame_rate; cfr re-encode is not needed.`
|
|
18838
18837
|
);
|
|
18839
18838
|
}
|
|
18840
|
-
const encoderJsonPath =
|
|
18839
|
+
const encoderJsonPath = join40(planDir, "meta", "encoder.json");
|
|
18841
18840
|
if (!existsSync27(encoderJsonPath)) {
|
|
18842
18841
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
18843
18842
|
}
|
|
@@ -18850,7 +18849,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
18850
18849
|
return encoderJson;
|
|
18851
18850
|
}
|
|
18852
18851
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
18853
|
-
if (existsSync27(outputPath))
|
|
18852
|
+
if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
|
|
18854
18853
|
mkdirSync17(outputPath, { recursive: true });
|
|
18855
18854
|
let globalIdx = 0;
|
|
18856
18855
|
for (const chunkDir of chunkPaths) {
|
|
@@ -18864,8 +18863,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
18864
18863
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
18865
18864
|
}
|
|
18866
18865
|
for (const frame of frames) {
|
|
18867
|
-
const dst =
|
|
18868
|
-
cpSync3(
|
|
18866
|
+
const dst = join40(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
18867
|
+
cpSync3(join40(chunkDir, frame), dst);
|
|
18869
18868
|
globalIdx += 1;
|
|
18870
18869
|
}
|
|
18871
18870
|
}
|
|
@@ -18875,13 +18874,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
18875
18874
|
);
|
|
18876
18875
|
}
|
|
18877
18876
|
if (audioPath !== null && existsSync27(audioPath)) {
|
|
18878
|
-
const sidecar =
|
|
18877
|
+
const sidecar = join40(outputPath, MIXED_AUDIO_FILENAME4);
|
|
18879
18878
|
cpSync3(audioPath, sidecar);
|
|
18880
18879
|
}
|
|
18881
18880
|
let fileSize = 0;
|
|
18882
18881
|
for (const name of readdirSync10(outputPath)) {
|
|
18883
18882
|
try {
|
|
18884
|
-
fileSize += statSync11(
|
|
18883
|
+
fileSize += statSync11(join40(outputPath, name)).size;
|
|
18885
18884
|
} catch {
|
|
18886
18885
|
}
|
|
18887
18886
|
}
|
|
@@ -18895,8 +18894,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
18895
18894
|
|
|
18896
18895
|
// src/services/distributed/renderChunk.ts
|
|
18897
18896
|
import { randomBytes } from "node:crypto";
|
|
18898
|
-
import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as
|
|
18899
|
-
import { join as
|
|
18897
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
|
|
18898
|
+
import { join as join41 } from "node:path";
|
|
18900
18899
|
import {
|
|
18901
18900
|
assertSwiftShader,
|
|
18902
18901
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -18998,7 +18997,7 @@ function hashChunkOutput(outputPath, kind) {
|
|
|
18998
18997
|
if (kind === "file") return sha256Hex(readFileSync15(outputPath));
|
|
18999
18998
|
const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
19000
18999
|
const lines = entries.map(
|
|
19001
|
-
(name) => `${name}\0${sha256Hex(readFileSync15(
|
|
19000
|
+
(name) => `${name}\0${sha256Hex(readFileSync15(join41(outputPath, name)))}`
|
|
19002
19001
|
);
|
|
19003
19002
|
return sha256Hex(lines.join("\0"));
|
|
19004
19003
|
}
|
|
@@ -19015,9 +19014,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
19015
19014
|
async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
19016
19015
|
const start = Date.now();
|
|
19017
19016
|
const log = defaultLogger;
|
|
19018
|
-
const planJsonPath =
|
|
19019
|
-
const encoderJsonPath =
|
|
19020
|
-
const chunksJsonPath =
|
|
19017
|
+
const planJsonPath = join41(planDir, "plan.json");
|
|
19018
|
+
const encoderJsonPath = join41(planDir, "meta", "encoder.json");
|
|
19019
|
+
const chunksJsonPath = join41(planDir, "meta", "chunks.json");
|
|
19021
19020
|
if (!existsSync28(planJsonPath)) {
|
|
19022
19021
|
throw new RenderChunkValidationError(
|
|
19023
19022
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -19043,7 +19042,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19043
19042
|
}
|
|
19044
19043
|
const encoder = JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
|
|
19045
19044
|
const chunks = JSON.parse(readFileSync15(chunksJsonPath, "utf-8"));
|
|
19046
|
-
const videosJsonPath =
|
|
19045
|
+
const videosJsonPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
19047
19046
|
let planVideos = null;
|
|
19048
19047
|
if (existsSync28(videosJsonPath)) {
|
|
19049
19048
|
try {
|
|
@@ -19075,7 +19074,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19075
19074
|
}
|
|
19076
19075
|
const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
|
|
19077
19076
|
const framesInChunk = range.endFrame - range.startFrame;
|
|
19078
|
-
const compiledDir =
|
|
19077
|
+
const compiledDir = join41(planDir, "compiled");
|
|
19079
19078
|
if (!existsSync28(compiledDir)) {
|
|
19080
19079
|
throw new RenderChunkValidationError(
|
|
19081
19080
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -19133,7 +19132,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19133
19132
|
};
|
|
19134
19133
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
19135
19134
|
mkdirSync18(workDir, { recursive: true });
|
|
19136
|
-
const framesDir =
|
|
19135
|
+
const framesDir = join41(workDir, "captured-frames");
|
|
19137
19136
|
mkdirSync18(framesDir, { recursive: true });
|
|
19138
19137
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
19139
19138
|
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
@@ -19158,7 +19157,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19158
19157
|
dimensions: plan2.dimensions,
|
|
19159
19158
|
encoder,
|
|
19160
19159
|
videoCount: planVideos?.videos.length ?? 0,
|
|
19161
|
-
html: readFileSync15(
|
|
19160
|
+
html: readFileSync15(join41(compiledDir, "index.html"), "utf-8"),
|
|
19162
19161
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
19163
19162
|
skipReadinessVideoIds: deferredVideos.filter(
|
|
19164
19163
|
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
@@ -19334,7 +19333,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19334
19333
|
return observedMode;
|
|
19335
19334
|
},
|
|
19336
19335
|
resetForScreenshotRetry: () => {
|
|
19337
|
-
|
|
19336
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
19338
19337
|
mkdirSync18(framesDir, { recursive: true });
|
|
19339
19338
|
capturePerfs.length = 0;
|
|
19340
19339
|
captureAttempts.length = 0;
|
|
@@ -19372,7 +19371,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19372
19371
|
if (isPngSequence) {
|
|
19373
19372
|
if (!existsSync28(outputChunkPath)) mkdirSync18(outputChunkPath, { recursive: true });
|
|
19374
19373
|
} else {
|
|
19375
|
-
const outDir =
|
|
19374
|
+
const outDir = join41(outputChunkPath, "..");
|
|
19376
19375
|
if (!existsSync28(outDir)) mkdirSync18(outDir, { recursive: true });
|
|
19377
19376
|
}
|
|
19378
19377
|
const encodeStarted = Date.now();
|
|
@@ -19427,8 +19426,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19427
19426
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
19428
19427
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
19429
19428
|
});
|
|
19430
|
-
|
|
19431
|
-
|
|
19429
|
+
rmSync17(videoOnlyPath, { force: true });
|
|
19430
|
+
rmSync17(audioScratchPath, { force: true });
|
|
19432
19431
|
}
|
|
19433
19432
|
encodeStageMs = Date.now() - encodeStarted;
|
|
19434
19433
|
} finally {
|
|
@@ -19474,8 +19473,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19474
19473
|
writeFileSync13(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
19475
19474
|
`, "utf-8");
|
|
19476
19475
|
try {
|
|
19477
|
-
|
|
19478
|
-
|
|
19476
|
+
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
19477
|
+
rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
19479
19478
|
} catch (err) {
|
|
19480
19479
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
19481
19480
|
workDir,
|
|
@@ -19504,18 +19503,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19504
19503
|
|
|
19505
19504
|
// src/services/distributed/planV2Execution.ts
|
|
19506
19505
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
19507
|
-
const workRoot = mkdtempSync9(
|
|
19508
|
-
const materializedPlanDir =
|
|
19506
|
+
const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-chunk-"));
|
|
19507
|
+
const materializedPlanDir = join42(workRoot, "plan");
|
|
19509
19508
|
try {
|
|
19510
19509
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
19511
19510
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
19512
19511
|
} finally {
|
|
19513
|
-
|
|
19512
|
+
rmSync18(workRoot, { recursive: true, force: true });
|
|
19514
19513
|
}
|
|
19515
19514
|
}
|
|
19516
19515
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
19517
|
-
const workRoot = mkdtempSync9(
|
|
19518
|
-
const materializedPlanDir =
|
|
19516
|
+
const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-assembler-"));
|
|
19517
|
+
const materializedPlanDir = join42(workRoot, "plan");
|
|
19519
19518
|
try {
|
|
19520
19519
|
const materialized = materializePlanV2Target(
|
|
19521
19520
|
planV2Dir,
|
|
@@ -19530,7 +19529,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
19530
19529
|
options
|
|
19531
19530
|
);
|
|
19532
19531
|
} finally {
|
|
19533
|
-
|
|
19532
|
+
rmSync18(workRoot, { recursive: true, force: true });
|
|
19534
19533
|
}
|
|
19535
19534
|
}
|
|
19536
19535
|
|