@hyperframes/producer 0.8.185 → 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 +1307 -1328
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +70 -69
- package/dist/index.js +1958 -1935
- package/dist/public-server.js +1566 -1523
- package/dist/renderRequest.d.ts +1 -0
- 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/htmlCompiler.d.ts +3 -2
- package/dist/services/render/motionBlurRoute.d.ts +11 -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/compileStage.d.ts +2 -0
- package/dist/services/render/stages/freezePlan.d.ts +2 -1
- package/dist/services/renderOrchestrator.d.ts +1 -0
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
linkSync,
|
|
8
8
|
mkdirSync as mkdirSync10,
|
|
9
9
|
renameSync as renameSync4,
|
|
10
|
-
rmSync as
|
|
10
|
+
rmSync as rmSync9,
|
|
11
11
|
writeFileSync as writeFileSync6
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { extname as extname4, isAbsolute as isAbsolute3, join as
|
|
13
|
+
import { extname as extname4, isAbsolute as isAbsolute3, join as join21, relative as relative5, sep as sep2 } from "node:path";
|
|
14
14
|
import {
|
|
15
15
|
fpsToNumber as fpsToNumber10,
|
|
16
16
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
mkdirSync as mkdirSync6,
|
|
68
68
|
readFileSync as readFileSync6,
|
|
69
69
|
readdirSync as readdirSync3,
|
|
70
|
-
rmSync as
|
|
70
|
+
rmSync as rmSync7,
|
|
71
71
|
statSync as statSync4,
|
|
72
72
|
writeFileSync as writeFileSync3,
|
|
73
73
|
copyFileSync as copyFileSync4,
|
|
@@ -116,7 +116,7 @@ import {
|
|
|
116
116
|
augmentProtocolTimeoutError,
|
|
117
117
|
augmentPageNavigationTimeoutError
|
|
118
118
|
} from "@hyperframes/engine";
|
|
119
|
-
import { join as
|
|
119
|
+
import { join as join13, dirname as dirname7, resolve as resolve6 } from "path";
|
|
120
120
|
import { randomUUID } from "crypto";
|
|
121
121
|
|
|
122
122
|
// src/services/fileServer.ts
|
|
@@ -2255,7 +2255,10 @@ function stripCompilerMountBootstrap(source) {
|
|
|
2255
2255
|
""
|
|
2256
2256
|
);
|
|
2257
2257
|
}
|
|
2258
|
-
function
|
|
2258
|
+
function runtimeRoot(document2) {
|
|
2259
|
+
return document2.querySelector('[data-composition-id][data-root="true"]') ?? document2.querySelector("[data-composition-id]");
|
|
2260
|
+
}
|
|
2261
|
+
function detectRenderModeHints(html, pageMotionBlur) {
|
|
2259
2262
|
const reasons = [];
|
|
2260
2263
|
const { document: document2 } = parseHTML4(html);
|
|
2261
2264
|
if (document2.querySelector("canvas[layoutsubtree]")) {
|
|
@@ -2263,6 +2266,11 @@ function detectRenderModeHints(html) {
|
|
|
2263
2266
|
code: "htmlInCanvas",
|
|
2264
2267
|
message: "Detected html-in-canvas API (layoutsubtree canvas). Chrome does not support concurrent drawElementImage across multiple workers; render is pinned to a single worker."
|
|
2265
2268
|
});
|
|
2269
|
+
} else if (pageMotionBlur !== "off" && runtimeRoot(document2)?.getAttribute("data-motion-blur") === "on") {
|
|
2270
|
+
reasons.push({
|
|
2271
|
+
code: "captureMotionBlur",
|
|
2272
|
+
message: 'Detected capture motion blur (data-motion-blur="on"), which draws each frame with html-in-canvas; routed through screenshot capture mode.'
|
|
2273
|
+
});
|
|
2266
2274
|
}
|
|
2267
2275
|
if (document2.querySelector("iframe")) {
|
|
2268
2276
|
reasons.push({
|
|
@@ -3294,7 +3302,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
3294
3302
|
/(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
|
|
3295
3303
|
"$1"
|
|
3296
3304
|
);
|
|
3297
|
-
const renderModeHints = detectRenderModeHints(sanitizedHtml);
|
|
3305
|
+
const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
|
|
3298
3306
|
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
3299
3307
|
const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
|
|
3300
3308
|
const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
|
|
@@ -3436,7 +3444,9 @@ async function discoverMediaFromBrowser(page) {
|
|
|
3436
3444
|
const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
|
|
3437
3445
|
const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
|
|
3438
3446
|
if (!id) return;
|
|
3439
|
-
const
|
|
3447
|
+
const ownSrc = htmlEl.getAttribute("src");
|
|
3448
|
+
const blankSrc = ownSrc !== null && ownSrc.trim() === "" && !htmlEl.querySelector("source[src]");
|
|
3449
|
+
const src = blankSrc ? "" : htmlEl.currentSrc || htmlEl.src || ownSrc || "";
|
|
3440
3450
|
const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
|
|
3441
3451
|
const endRaw = htmlEl.getAttribute("data-end");
|
|
3442
3452
|
const durationRaw = htmlEl.getAttribute("data-duration");
|
|
@@ -3779,6 +3789,7 @@ async function runCompileStage(input) {
|
|
|
3779
3789
|
abortSignal,
|
|
3780
3790
|
variables: input.variables,
|
|
3781
3791
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
3792
|
+
pageMotionBlur: input.pageMotionBlur,
|
|
3782
3793
|
animatedGifCacheDir: cfg.extractCacheDir ? join6(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
3783
3794
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
3784
3795
|
});
|
|
@@ -4075,6 +4086,7 @@ async function runProbeStage(input) {
|
|
|
4075
4086
|
variables: job.config.variables,
|
|
4076
4087
|
deviceScaleFactor,
|
|
4077
4088
|
motionBlur,
|
|
4089
|
+
pageMotionBlur: job.config.pageMotionBlur,
|
|
4078
4090
|
requiresWebGpu: compositionRequiresWebGpu(compiled.html),
|
|
4079
4091
|
subTimelineWaitMemo
|
|
4080
4092
|
};
|
|
@@ -4937,1241 +4949,1225 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
4937
4949
|
}
|
|
4938
4950
|
|
|
4939
4951
|
// src/services/render/stages/audioStage.ts
|
|
4940
|
-
import { join as
|
|
4952
|
+
import { join as join10 } from "node:path";
|
|
4953
|
+
import { rmSync as rmSync5 } from "node:fs";
|
|
4941
4954
|
import {
|
|
4942
4955
|
MIXED_AUDIO_FILENAME,
|
|
4943
4956
|
audioFailureDiagnostics,
|
|
4944
4957
|
processCompositionAudio
|
|
4945
4958
|
} from "@hyperframes/engine";
|
|
4946
|
-
|
|
4947
|
-
|
|
4959
|
+
|
|
4960
|
+
// src/services/render/audioPadTrim.ts
|
|
4961
|
+
import { spawn } from "node:child_process";
|
|
4962
|
+
import { mkdtempSync as mkdtempSync2, renameSync as renameSync2, rmSync as rmSync4 } from "node:fs";
|
|
4963
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
4964
|
+
import {
|
|
4965
|
+
DEFAULT_CONFIG,
|
|
4966
|
+
describeFfmpegFailure as describeFfmpegFailure2,
|
|
4967
|
+
encodeAac,
|
|
4968
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
4969
|
+
getFfprobeBinary,
|
|
4970
|
+
ManagedChildProcess,
|
|
4971
|
+
runFfmpeg as runFfmpeg4,
|
|
4972
|
+
trackChildProcess
|
|
4973
|
+
} from "@hyperframes/engine";
|
|
4974
|
+
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
4975
|
+
|
|
4976
|
+
// src/services/render/audioTruePeak.ts
|
|
4977
|
+
import {
|
|
4978
|
+
aacSegmentCount,
|
|
4979
|
+
describeFfmpegFailure,
|
|
4980
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
4981
|
+
runFfmpeg as runFfmpeg3
|
|
4982
|
+
} from "@hyperframes/engine";
|
|
4983
|
+
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
4984
|
+
function checkedDbfs(value) {
|
|
4985
|
+
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
4986
|
+
throw new Error("[audioTruePeak] FFmpeg did not report AAC true peak");
|
|
4987
|
+
}
|
|
4988
|
+
return value;
|
|
4948
4989
|
}
|
|
4949
|
-
async function
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
assertNotAborted,
|
|
4963
|
-
log
|
|
4964
|
-
} = input;
|
|
4965
|
-
const stage3Start = Date.now();
|
|
4966
|
-
const audioOutputPath = join9(workDir, MIXED_AUDIO_FILENAME);
|
|
4967
|
-
let hasAudio = false;
|
|
4968
|
-
let audioError;
|
|
4969
|
-
let audioFailures;
|
|
4970
|
-
if (audios.length > 0) {
|
|
4971
|
-
let audioResult;
|
|
4972
|
-
try {
|
|
4973
|
-
audioResult = await processCompositionAudio(
|
|
4974
|
-
audios,
|
|
4975
|
-
projectDir,
|
|
4976
|
-
join9(workDir, "audio-work"),
|
|
4977
|
-
audioOutputPath,
|
|
4978
|
-
duration,
|
|
4979
|
-
abortSignal,
|
|
4980
|
-
{
|
|
4981
|
-
ffmpegProcessTimeout,
|
|
4982
|
-
audioGain,
|
|
4983
|
-
masterFxChain,
|
|
4984
|
-
urlByAbsentLocalPath,
|
|
4985
|
-
keepEncoderInputAt
|
|
4986
|
-
},
|
|
4987
|
-
compiledDir
|
|
4988
|
-
);
|
|
4989
|
-
} catch (err) {
|
|
4990
|
-
assertNotAborted();
|
|
4991
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
4992
|
-
return {
|
|
4993
|
-
audioOutputPath,
|
|
4994
|
-
hasAudio: false,
|
|
4995
|
-
audioProcessMs: Date.now() - stage3Start,
|
|
4996
|
-
audioError: detail,
|
|
4997
|
-
// Synthesised rather than left undefined. The warning policy reads
|
|
4998
|
-
// owner, retryability, reason and stage off this list, so reporting the
|
|
4999
|
-
// FATAL failure — the one that took the whole mix down — with an empty
|
|
5000
|
-
// one gave it strictly less classification than a single dropped track
|
|
5001
|
-
// gets, which is the opposite of what this stage exists to do.
|
|
5002
|
-
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
|
5003
|
-
// and this is the FX render, which is none of them.
|
|
5004
|
-
audioFailures: [
|
|
5005
|
-
{
|
|
5006
|
-
stage: "internal",
|
|
5007
|
-
reason: "internal",
|
|
5008
|
-
owner: "system",
|
|
5009
|
-
retryable: false,
|
|
5010
|
-
detail: detail.slice(0, 2e3)
|
|
5011
|
-
}
|
|
5012
|
-
]
|
|
5013
|
-
};
|
|
5014
|
-
}
|
|
5015
|
-
assertNotAborted();
|
|
5016
|
-
const interrupted = audioResult.failures?.find(
|
|
5017
|
-
(failure) => failure.reason === "external_interruption"
|
|
5018
|
-
);
|
|
5019
|
-
if (interrupted) {
|
|
5020
|
-
throw encoderFailureError("Audio processing failed", {
|
|
5021
|
-
error: interrupted.detail,
|
|
5022
|
-
failureReason: "external_interruption",
|
|
5023
|
-
audioFailures: audioFailureDiagnostics(triggerFirst(audioResult.failures, interrupted))
|
|
5024
|
-
});
|
|
5025
|
-
}
|
|
5026
|
-
hasAudio = audioResult.success;
|
|
5027
|
-
audioFailures = audioResult.failures;
|
|
5028
|
-
if (!hasAudio) {
|
|
5029
|
-
audioError = audioResult.error ?? "audio mix failed for an unknown reason";
|
|
5030
|
-
for (const failure of audioResult.failures ?? []) {
|
|
5031
|
-
log?.warn("Audio track failed", {
|
|
5032
|
-
elementId: failure.elementId,
|
|
5033
|
-
stage: failure.stage,
|
|
5034
|
-
reason: failure.reason,
|
|
5035
|
-
owner: failure.owner,
|
|
5036
|
-
retryable: failure.retryable,
|
|
5037
|
-
detail: failure.detail
|
|
5038
|
-
});
|
|
5039
|
-
}
|
|
4990
|
+
async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
4991
|
+
let firstBlock;
|
|
4992
|
+
let laterBlocks = Number.NEGATIVE_INFINITY;
|
|
4993
|
+
let partialLine = "";
|
|
4994
|
+
const readLines = (chunk) => {
|
|
4995
|
+
const lines = (partialLine + chunk).split(/\r|\n/);
|
|
4996
|
+
partialLine = lines.pop() ?? "";
|
|
4997
|
+
for (const line of lines) {
|
|
4998
|
+
const ftpk = /FTPK:([^d]*)dBFS/.exec(line)?.[1];
|
|
4999
|
+
if (!ftpk) continue;
|
|
5000
|
+
const block = Math.max(...ftpk.trim().split(/\s+/).map(toDbfs));
|
|
5001
|
+
if (firstBlock === void 0) firstBlock = block;
|
|
5002
|
+
else laterBlocks = Math.max(laterBlocks, block);
|
|
5040
5003
|
}
|
|
5004
|
+
};
|
|
5005
|
+
const result = await runFfmpeg3(
|
|
5006
|
+
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
5007
|
+
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
5008
|
+
);
|
|
5009
|
+
if (!result.success) {
|
|
5010
|
+
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
5041
5011
|
}
|
|
5042
|
-
const
|
|
5012
|
+
const summary = [...result.stderr.matchAll(/^\s*Peak:\s*([+-]?(?:[\d.]+|inf)) dBFS$/gim)];
|
|
5043
5013
|
return {
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
audioError,
|
|
5048
|
-
audioFailures
|
|
5014
|
+
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
5015
|
+
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
5016
|
+
laterBlocks: checkedDbfs(laterBlocks)
|
|
5049
5017
|
};
|
|
5050
5018
|
}
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
}
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
const
|
|
5077
|
-
const
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
return frameFileExtension(captureOptions.format) === "jpg" ? Math.ceil(
|
|
5081
|
-
rawBytes / ((captureOptions.quality ?? 80) > 80 ? HIGH_QUALITY_JPEG_RAW_RGBA_DIVISOR : JPEG_RAW_RGBA_DIVISOR)
|
|
5082
|
-
) : rawBytes;
|
|
5083
|
-
}
|
|
5084
|
-
function freeDiskBytesAt(path) {
|
|
5085
|
-
try {
|
|
5086
|
-
const stat = statfsSync2(path);
|
|
5087
|
-
return stat.bavail * stat.bsize;
|
|
5088
|
-
} catch {
|
|
5089
|
-
return null;
|
|
5090
|
-
}
|
|
5019
|
+
async function measureTruePeakDbfs(audioPath, options) {
|
|
5020
|
+
const { timeoutMs, signal } = options;
|
|
5021
|
+
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
5022
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
5023
|
+
const block = 4800;
|
|
5024
|
+
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
5025
|
+
if (count === 1) return wholeFile();
|
|
5026
|
+
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
5027
|
+
const peers = new AbortController();
|
|
5028
|
+
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
5029
|
+
const scans = await Promise.all(
|
|
5030
|
+
Array.from({ length: count }, (_, k) => {
|
|
5031
|
+
const start = k === 0 ? 0 : cut(k) - block;
|
|
5032
|
+
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
5033
|
+
return scanTruePeak(
|
|
5034
|
+
audioPath,
|
|
5035
|
+
`atrim=start_sample=${start}${end},`,
|
|
5036
|
+
timeoutMs,
|
|
5037
|
+
segmentSignal
|
|
5038
|
+
);
|
|
5039
|
+
})
|
|
5040
|
+
).catch((error) => {
|
|
5041
|
+
peers.abort();
|
|
5042
|
+
throw error;
|
|
5043
|
+
});
|
|
5044
|
+
const last = scans[count - 1];
|
|
5045
|
+
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
5046
|
+
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
5047
|
+
return wholeFile();
|
|
5091
5048
|
}
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5049
|
+
|
|
5050
|
+
// src/services/render/audioPadTrim.ts
|
|
5051
|
+
var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
5052
|
+
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
5053
|
+
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
5054
|
+
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
5055
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
5056
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
5099
5057
|
}
|
|
5100
|
-
function
|
|
5101
|
-
const
|
|
5102
|
-
|
|
5103
|
-
|
|
5058
|
+
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
5059
|
+
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
5060
|
+
const targetSec = formatSeconds(targetDurationSeconds);
|
|
5061
|
+
if (Math.abs(delta) < AUDIO_DURATION_TOLERANCE_SECONDS) {
|
|
5062
|
+
return {
|
|
5063
|
+
operation: "copy",
|
|
5064
|
+
steps: [{ kind: "copy", args: ["-i", audioPath, "-c:a", "copy", "-y", outputPath] }],
|
|
5065
|
+
cleanupPaths: []
|
|
5066
|
+
};
|
|
5104
5067
|
}
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
return "The streaming duration cap (PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED=true) sends renders longer than PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS to disk at any worker count. Free up disk space, unset the cap, or use --low-memory-mode, which streams past it on one worker.";
|
|
5112
|
-
default:
|
|
5113
|
-
return "Free up disk space.";
|
|
5068
|
+
if (delta > 0) {
|
|
5069
|
+
return {
|
|
5070
|
+
operation: "pad",
|
|
5071
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
5072
|
+
cleanupPaths: []
|
|
5073
|
+
};
|
|
5114
5074
|
}
|
|
5075
|
+
return {
|
|
5076
|
+
operation: "trim",
|
|
5077
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
5078
|
+
cleanupPaths: []
|
|
5079
|
+
};
|
|
5115
5080
|
}
|
|
5116
|
-
function
|
|
5117
|
-
|
|
5118
|
-
return new Error(
|
|
5119
|
-
`Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha). ${remedy}`
|
|
5120
|
-
);
|
|
5081
|
+
function formatSeconds(sec) {
|
|
5082
|
+
return sec.toFixed(6);
|
|
5121
5083
|
}
|
|
5122
|
-
function
|
|
5123
|
-
const
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
captureOptions,
|
|
5127
|
-
freeDiskBytes
|
|
5128
|
-
);
|
|
5129
|
-
if (headroom.available) return;
|
|
5130
|
-
throw diskCaptureShortfallError(
|
|
5131
|
-
headroom.estimatedBytes,
|
|
5132
|
-
headroom.freeBytes,
|
|
5133
|
-
framesDir,
|
|
5134
|
-
"estimate",
|
|
5135
|
-
remedy
|
|
5136
|
-
);
|
|
5084
|
+
function sanitizeProbeFailure(reason, paths) {
|
|
5085
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
5086
|
+
const keepFfmpegsLastLineUncapped = Number.POSITIVE_INFINITY;
|
|
5087
|
+
return redactTelemetryString3(redactKnownPaths(message, paths), keepFfmpegsLastLineUncapped);
|
|
5137
5088
|
}
|
|
5138
|
-
function
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5089
|
+
async function padOrTrimAudioToVideoFrameCount(input) {
|
|
5090
|
+
const probeVideo = input.probeVideoFrameInfo ?? ((videoPath) => defaultProbeVideoFrameInfo(videoPath, input.signal));
|
|
5091
|
+
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
5092
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
5093
|
+
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
5094
|
+
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
5095
|
+
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
5096
|
+
const [videoResult, audioResult] = await Promise.allSettled([
|
|
5097
|
+
probeVideo(input.videoPath),
|
|
5098
|
+
probeAudio(input.audioPath, input.signal)
|
|
5099
|
+
]);
|
|
5100
|
+
const probePaths = [input.videoPath, input.audioPath, input.outputPath];
|
|
5101
|
+
if (videoResult.status === "rejected") {
|
|
5102
|
+
return failResult(
|
|
5103
|
+
input.outputPath,
|
|
5104
|
+
0,
|
|
5105
|
+
audioResult.status === "fulfilled" ? audioResult.value.durationSeconds : 0,
|
|
5106
|
+
`audioPadTrim: failed to probe video: ${sanitizeProbeFailure(
|
|
5107
|
+
videoResult.reason,
|
|
5108
|
+
probePaths
|
|
5109
|
+
)}`
|
|
5110
|
+
);
|
|
5151
5111
|
}
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
}
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
5195
|
-
if (frameRange !== void 0) {
|
|
5196
|
-
if (!Number.isFinite(frameRange.startFrame) || !Number.isFinite(frameRange.endFrame) || frameRange.startFrame < 0 || frameRange.endFrame <= frameRange.startFrame) {
|
|
5197
|
-
throw new Error(
|
|
5198
|
-
`[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
|
|
5199
|
-
);
|
|
5200
|
-
}
|
|
5201
|
-
const rangeFrames = frameRange.endFrame - frameRange.startFrame;
|
|
5202
|
-
if (rangeFrames !== totalFrames) {
|
|
5203
|
-
throw new Error(
|
|
5204
|
-
`[captureStage] frameRange size (${rangeFrames}) must equal totalFrames (${totalFrames}). Received frameRange=${JSON.stringify(frameRange)}.`
|
|
5205
|
-
);
|
|
5112
|
+
if (audioResult.status === "rejected") {
|
|
5113
|
+
return failResult(
|
|
5114
|
+
input.outputPath,
|
|
5115
|
+
0,
|
|
5116
|
+
0,
|
|
5117
|
+
`audioPadTrim: failed to probe audio: ${sanitizeProbeFailure(
|
|
5118
|
+
audioResult.reason,
|
|
5119
|
+
probePaths
|
|
5120
|
+
)}`
|
|
5121
|
+
);
|
|
5122
|
+
}
|
|
5123
|
+
const videoInfo = videoResult.value;
|
|
5124
|
+
const audioInfo = audioResult.value;
|
|
5125
|
+
if (!Number.isFinite(videoInfo.frameCount) || videoInfo.frameCount <= 0 || !Number.isFinite(videoInfo.fpsNum) || videoInfo.fpsNum <= 0 || !Number.isFinite(videoInfo.fpsDen) || videoInfo.fpsDen <= 0) {
|
|
5126
|
+
return failResult(
|
|
5127
|
+
input.outputPath,
|
|
5128
|
+
0,
|
|
5129
|
+
audioInfo.durationSeconds,
|
|
5130
|
+
`audioPadTrim: invalid video frame info: ${JSON.stringify(videoInfo)}`
|
|
5131
|
+
);
|
|
5132
|
+
}
|
|
5133
|
+
const targetDurationSeconds = videoInfo.frameCount * videoInfo.fpsDen / videoInfo.fpsNum;
|
|
5134
|
+
const plan2 = buildPadTrimAudioPlan(
|
|
5135
|
+
input.audioPath,
|
|
5136
|
+
input.outputPath,
|
|
5137
|
+
audioInfo.durationSeconds,
|
|
5138
|
+
targetDurationSeconds
|
|
5139
|
+
);
|
|
5140
|
+
try {
|
|
5141
|
+
for (const step of plan2.steps) {
|
|
5142
|
+
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
5143
|
+
if (!ffmpegResult.success) {
|
|
5144
|
+
return {
|
|
5145
|
+
success: false,
|
|
5146
|
+
outputPath: input.outputPath,
|
|
5147
|
+
targetDurationSeconds,
|
|
5148
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5149
|
+
operation: plan2.operation,
|
|
5150
|
+
error: ffmpegResult.error,
|
|
5151
|
+
failureReason: ffmpegResult.failureReason
|
|
5152
|
+
};
|
|
5153
|
+
}
|
|
5206
5154
|
}
|
|
5155
|
+
} catch (err) {
|
|
5156
|
+
return {
|
|
5157
|
+
success: false,
|
|
5158
|
+
outputPath: input.outputPath,
|
|
5159
|
+
targetDurationSeconds,
|
|
5160
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5161
|
+
operation: plan2.operation,
|
|
5162
|
+
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
5163
|
+
};
|
|
5164
|
+
} finally {
|
|
5165
|
+
for (const path of plan2.cleanupPaths) rmSync4(path, { force: true });
|
|
5207
5166
|
}
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
});
|
|
5217
|
-
if (workerCount > 1) {
|
|
5218
|
-
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
5219
|
-
serverUrl: fileServer.url,
|
|
5220
|
-
workDir,
|
|
5221
|
-
framesDir,
|
|
5222
|
-
totalFrames,
|
|
5223
|
-
initialWorkerCount: workerCount,
|
|
5224
|
-
allowRetry: shouldAllowAdaptiveCaptureRetry(workerCount, job.config.workers !== void 0),
|
|
5225
|
-
frameExt: frameFileExtension(captureOptions.format),
|
|
5226
|
-
captureOptions,
|
|
5227
|
-
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
5228
|
-
abortSignal,
|
|
5229
|
-
frameRangeStart: frameRange?.startFrame,
|
|
5230
|
-
dedupPerfs,
|
|
5231
|
-
onProgress: (progress) => {
|
|
5232
|
-
if (progress.latestWorkerPhase) {
|
|
5233
|
-
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
5234
|
-
return;
|
|
5235
|
-
}
|
|
5236
|
-
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
5237
|
-
job.framesRendered = progress.capturedFrames;
|
|
5238
|
-
checkDiskProjection(progress.capturedFrames);
|
|
5239
|
-
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
5240
|
-
reportFrameProgress(
|
|
5241
|
-
job,
|
|
5242
|
-
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
|
|
5243
|
-
Math.round(25 + frameProgress * 45),
|
|
5244
|
-
onProgress,
|
|
5245
|
-
progress.capturedFrames,
|
|
5246
|
-
progress.totalFrames
|
|
5247
|
-
);
|
|
5248
|
-
},
|
|
5249
|
-
cfg: captureCfg,
|
|
5250
|
-
log
|
|
5167
|
+
let loweredDb = 0;
|
|
5168
|
+
if (probeTruePeak) {
|
|
5169
|
+
const correction = await enforceAacTruePeak({
|
|
5170
|
+
audioPath: input.outputPath,
|
|
5171
|
+
targetDurationSeconds,
|
|
5172
|
+
signal: input.signal,
|
|
5173
|
+
probeTruePeak,
|
|
5174
|
+
encode
|
|
5251
5175
|
});
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5176
|
+
if (!correction.success) {
|
|
5177
|
+
return {
|
|
5178
|
+
success: false,
|
|
5179
|
+
outputPath: input.outputPath,
|
|
5180
|
+
targetDurationSeconds,
|
|
5181
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5182
|
+
operation: plan2.operation,
|
|
5183
|
+
error: correction.error,
|
|
5184
|
+
failureReason: correction.failureReason
|
|
5185
|
+
};
|
|
5262
5186
|
}
|
|
5263
|
-
|
|
5264
|
-
const sequential = await runSequentialDiskCapture(
|
|
5265
|
-
input,
|
|
5266
|
-
captureOptions,
|
|
5267
|
-
captureCfg,
|
|
5268
|
-
checkDiskProjection
|
|
5269
|
-
);
|
|
5270
|
-
probeSession = null;
|
|
5271
|
-
lastBrowserConsole = sequential.lastBrowserConsole;
|
|
5272
|
-
captureBeyondViewport = sequential.captureBeyondViewport;
|
|
5187
|
+
loweredDb = correction.loweredDb ?? 0;
|
|
5273
5188
|
}
|
|
5274
|
-
return {
|
|
5189
|
+
return {
|
|
5190
|
+
success: true,
|
|
5191
|
+
outputPath: input.outputPath,
|
|
5192
|
+
targetDurationSeconds,
|
|
5193
|
+
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5194
|
+
operation: plan2.operation,
|
|
5195
|
+
...loweredDb > 0 ? { audioLoweredDb: loweredDb } : {}
|
|
5196
|
+
};
|
|
5275
5197
|
}
|
|
5276
|
-
async function
|
|
5277
|
-
const
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
let
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
videoInjector,
|
|
5302
|
-
captureCfg
|
|
5303
|
-
);
|
|
5304
|
-
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
5305
|
-
try {
|
|
5306
|
-
if (probeSession) {
|
|
5307
|
-
prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
|
|
5308
|
-
probeSession = null;
|
|
5198
|
+
async function constrainAacTruePeak(input) {
|
|
5199
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
5200
|
+
return enforceAacTruePeak({
|
|
5201
|
+
audioPath: input.audioPath,
|
|
5202
|
+
targetDurationSeconds: input.targetDurationSeconds,
|
|
5203
|
+
signal: input.signal,
|
|
5204
|
+
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
5205
|
+
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
5206
|
+
});
|
|
5207
|
+
}
|
|
5208
|
+
async function enforceAacTruePeak(input) {
|
|
5209
|
+
let scratchDir;
|
|
5210
|
+
try {
|
|
5211
|
+
scratchDir = mkdtempSync2(join9(dirname5(input.audioPath), ".true-peak-"));
|
|
5212
|
+
const correctedPath = join9(scratchDir, "audio.m4a");
|
|
5213
|
+
const measurements = [];
|
|
5214
|
+
let attenuationDb = 0;
|
|
5215
|
+
let measuredPath = input.audioPath;
|
|
5216
|
+
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
5217
|
+
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
5218
|
+
measurements.push(
|
|
5219
|
+
`pass ${pass}: ${truePeakDbfs.toFixed(3)} dBFS at ${attenuationDb.toFixed(3)} dB attenuation`
|
|
5220
|
+
);
|
|
5221
|
+
if (Number.isNaN(truePeakDbfs) || truePeakDbfs === Number.POSITIVE_INFINITY) {
|
|
5222
|
+
return { success: false, error: "audioPadTrim: FFmpeg reported an invalid true peak" };
|
|
5309
5223
|
}
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
input,
|
|
5315
|
-
rangeStart,
|
|
5316
|
-
rangeEnd,
|
|
5317
|
-
resumeFrom,
|
|
5318
|
-
checkDiskProjection
|
|
5319
|
-
});
|
|
5320
|
-
break;
|
|
5321
|
-
} catch (error) {
|
|
5322
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
5323
|
-
const resume = planTransientResume(error, {
|
|
5324
|
-
abortSignal,
|
|
5325
|
-
rangeFrames,
|
|
5326
|
-
framesDir,
|
|
5327
|
-
frameExt,
|
|
5328
|
-
retriesUsed: transientRetriesUsed
|
|
5329
|
-
});
|
|
5330
|
-
if (resume !== null) {
|
|
5331
|
-
transientRetriesUsed++;
|
|
5332
|
-
resumeFrom = resume;
|
|
5333
|
-
log.warn(
|
|
5334
|
-
"[Render] Transient browser failure during sequential capture; retrying once with a fresh session.",
|
|
5335
|
-
{
|
|
5336
|
-
resumeFrom,
|
|
5337
|
-
rangeFrames,
|
|
5338
|
-
transientRetriesUsed,
|
|
5339
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5340
|
-
}
|
|
5341
|
-
);
|
|
5342
|
-
continue;
|
|
5224
|
+
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
5225
|
+
if (measuredPath !== correctedPath) return { success: true };
|
|
5226
|
+
renameSync2(correctedPath, input.audioPath);
|
|
5227
|
+
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
5343
5228
|
}
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
await
|
|
5229
|
+
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
5230
|
+
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
5231
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
|
|
5232
|
+
const result = await input.encode(
|
|
5233
|
+
input.audioPath,
|
|
5234
|
+
correctedPath,
|
|
5235
|
+
input.targetDurationSeconds,
|
|
5236
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
5237
|
+
);
|
|
5238
|
+
if (!result.success) {
|
|
5239
|
+
return {
|
|
5240
|
+
success: false,
|
|
5241
|
+
error: sanitizeProbeFailure(
|
|
5242
|
+
result.error ?? "audioPadTrim: failed to constrain AAC true peak",
|
|
5243
|
+
[input.audioPath, correctedPath]
|
|
5244
|
+
),
|
|
5245
|
+
failureReason: result.failureReason
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
measuredPath = correctedPath;
|
|
5348
5249
|
}
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
}
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
}
|
|
5362
|
-
|
|
5363
|
-
if (!session.isInitialized) {
|
|
5364
|
-
await initializeSession5(session);
|
|
5365
|
-
return;
|
|
5366
|
-
}
|
|
5367
|
-
if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
|
|
5368
|
-
await completeDeferredDrawElementInit3(session);
|
|
5250
|
+
return {
|
|
5251
|
+
success: false,
|
|
5252
|
+
error: `audioPadTrim: AAC true peak remained above ${AAC_DELIVERY_TRUE_PEAK_DBFS} dBFS after ${MAX_TRUE_PEAK_CORRECTION_PASSES} correction passes; ${measurements.join("; ")}`
|
|
5253
|
+
};
|
|
5254
|
+
} catch (err) {
|
|
5255
|
+
return {
|
|
5256
|
+
success: false,
|
|
5257
|
+
error: `audioPadTrim: failed to validate AAC true peak: ${sanitizeProbeFailure(
|
|
5258
|
+
err,
|
|
5259
|
+
scratchDir ? [input.audioPath, scratchDir] : [input.audioPath]
|
|
5260
|
+
)}`
|
|
5261
|
+
};
|
|
5262
|
+
} finally {
|
|
5263
|
+
if (scratchDir) rmSync4(scratchDir, { recursive: true, force: true });
|
|
5369
5264
|
}
|
|
5370
5265
|
}
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
`Capturing frame ${fileIndex + 1}/${rangeFrames}`,
|
|
5380
|
-
Math.round(25 + (fileIndex + 1) / rangeFrames * 45),
|
|
5381
|
-
onProgress,
|
|
5382
|
-
fileIndex + 1,
|
|
5383
|
-
rangeFrames
|
|
5384
|
-
);
|
|
5266
|
+
function failResult(outputPath, target, source, error) {
|
|
5267
|
+
return {
|
|
5268
|
+
success: false,
|
|
5269
|
+
outputPath,
|
|
5270
|
+
targetDurationSeconds: target,
|
|
5271
|
+
sourceDurationSeconds: source,
|
|
5272
|
+
operation: "copy",
|
|
5273
|
+
error
|
|
5385
5274
|
};
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
const absoluteIdx = rangeStart + i;
|
|
5408
|
-
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
5409
|
-
await captureFrame(session, i, time);
|
|
5410
|
-
reportFrame(i);
|
|
5411
|
-
}
|
|
5275
|
+
}
|
|
5276
|
+
async function defaultProbeVideoFrameInfo(videoPath, signal) {
|
|
5277
|
+
const fastInfo = await runFfprobeJson(
|
|
5278
|
+
[
|
|
5279
|
+
"-v",
|
|
5280
|
+
"error",
|
|
5281
|
+
"-select_streams",
|
|
5282
|
+
"v:0",
|
|
5283
|
+
"-show_entries",
|
|
5284
|
+
"stream=nb_frames,r_frame_rate",
|
|
5285
|
+
"-of",
|
|
5286
|
+
"json",
|
|
5287
|
+
"--",
|
|
5288
|
+
videoPath
|
|
5289
|
+
],
|
|
5290
|
+
signal
|
|
5291
|
+
);
|
|
5292
|
+
let stream = fastInfo.streams?.[0];
|
|
5293
|
+
const fastCount = Number(stream?.nb_frames);
|
|
5294
|
+
if (stream && Number.isFinite(fastCount) && fastCount > 0) {
|
|
5295
|
+
return { frameCount: fastCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
5412
5296
|
}
|
|
5413
|
-
await
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5297
|
+
const slowInfo = await runFfprobeJson(
|
|
5298
|
+
[
|
|
5299
|
+
"-v",
|
|
5300
|
+
"error",
|
|
5301
|
+
"-select_streams",
|
|
5302
|
+
"v:0",
|
|
5303
|
+
"-count_packets",
|
|
5304
|
+
"-show_entries",
|
|
5305
|
+
"stream=nb_read_packets,r_frame_rate",
|
|
5306
|
+
"-of",
|
|
5307
|
+
"json",
|
|
5308
|
+
"--",
|
|
5309
|
+
videoPath
|
|
5310
|
+
],
|
|
5311
|
+
signal
|
|
5423
5312
|
);
|
|
5424
|
-
|
|
5313
|
+
stream = slowInfo.streams?.[0];
|
|
5314
|
+
if (!stream) throw new Error(`ffprobe found no video stream in ${videoPath}`);
|
|
5315
|
+
const slowCount = Number(stream.nb_read_packets);
|
|
5316
|
+
if (!Number.isFinite(slowCount) || slowCount <= 0) {
|
|
5317
|
+
throw new Error(`ffprobe returned no frame count: ${JSON.stringify(stream)}`);
|
|
5318
|
+
}
|
|
5319
|
+
return { frameCount: slowCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
5425
5320
|
}
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
5435
|
-
} from "@hyperframes/engine";
|
|
5436
|
-
import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
|
|
5437
|
-
|
|
5438
|
-
// src/services/hdrImageTransferCache.ts
|
|
5439
|
-
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
5440
|
-
var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
|
|
5441
|
-
|
|
5442
|
-
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
5443
|
-
import {
|
|
5444
|
-
TRANSITIONS,
|
|
5445
|
-
crossfade
|
|
5446
|
-
} from "@hyperframes/engine";
|
|
5447
|
-
|
|
5448
|
-
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
5449
|
-
import {
|
|
5450
|
-
TRANSITIONS as TRANSITIONS2,
|
|
5451
|
-
closeCaptureSession as closeCaptureSession6,
|
|
5452
|
-
createCaptureSession as createCaptureSession6,
|
|
5453
|
-
createFrameReorderBuffer as createFrameReorderBuffer2,
|
|
5454
|
-
crossfade as crossfade2,
|
|
5455
|
-
initTransparentBackground,
|
|
5456
|
-
initializeSession as initializeSession6
|
|
5457
|
-
} from "@hyperframes/engine";
|
|
5458
|
-
|
|
5459
|
-
// src/services/render/stages/encodeStage.ts
|
|
5460
|
-
import {
|
|
5461
|
-
copyFileSync as copyFileSync3,
|
|
5462
|
-
existsSync as existsSync9,
|
|
5463
|
-
mkdirSync as mkdirSync5,
|
|
5464
|
-
readdirSync as readdirSync2,
|
|
5465
|
-
readFileSync as readFileSync5,
|
|
5466
|
-
rmSync as rmSync4,
|
|
5467
|
-
statSync as statSync3,
|
|
5468
|
-
writeFileSync as writeFileSync2
|
|
5469
|
-
} from "node:fs";
|
|
5470
|
-
import { dirname as dirname5, join as join12 } from "node:path";
|
|
5471
|
-
import {
|
|
5472
|
-
encodeFramesChunkedConcat,
|
|
5473
|
-
encodeFramesFromDir,
|
|
5474
|
-
formatFfmpegError,
|
|
5475
|
-
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
5476
|
-
resolveConfig,
|
|
5477
|
-
runFfmpeg as runFfmpeg3,
|
|
5478
|
-
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
5479
|
-
} from "@hyperframes/engine";
|
|
5480
|
-
import {
|
|
5481
|
-
clearGifFramesBeforeNext,
|
|
5482
|
-
gifClearsAfterLeavingFrameInPlace
|
|
5483
|
-
} from "@hyperframes/core";
|
|
5484
|
-
|
|
5485
|
-
// src/services/render/stages/gifEncodeArgs.ts
|
|
5486
|
-
import { join as join11 } from "node:path";
|
|
5487
|
-
function fpsToFfmpegArg2(fps) {
|
|
5488
|
-
return fps.den === 1 ? String(fps.num) : `${fps.num}/${fps.den}`;
|
|
5321
|
+
function parseFrameRate(rate) {
|
|
5322
|
+
const [n, d] = rate.split("/");
|
|
5323
|
+
const fpsNum = Number(n);
|
|
5324
|
+
const fpsDen = d === void 0 ? 1 : Number(d);
|
|
5325
|
+
if (!Number.isFinite(fpsNum) || !Number.isFinite(fpsDen) || fpsNum <= 0 || fpsDen <= 0) {
|
|
5326
|
+
throw new Error(`Invalid r_frame_rate: ${JSON.stringify(rate)}`);
|
|
5327
|
+
}
|
|
5328
|
+
return { fpsNum, fpsDen };
|
|
5489
5329
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
5499
|
-
"-i",
|
|
5500
|
-
join11(input.framesDir, input.framePattern)
|
|
5501
|
-
];
|
|
5330
|
+
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
5331
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
5332
|
+
return {
|
|
5333
|
+
durationSeconds: metadata.durationSeconds,
|
|
5334
|
+
sampleRate: metadata.sampleRate,
|
|
5335
|
+
channels: metadata.channels,
|
|
5336
|
+
audioCodec: metadata.audioCodec
|
|
5337
|
+
};
|
|
5502
5338
|
}
|
|
5503
|
-
function
|
|
5504
|
-
|
|
5505
|
-
const transparency = input.preserveAlpha ? ":reserve_transparent=1" : "";
|
|
5506
|
-
return [
|
|
5507
|
-
"-y",
|
|
5508
|
-
...framesInput(input, fpsArg),
|
|
5509
|
-
"-vf",
|
|
5510
|
-
`fps=${fpsArg},palettegen=stats_mode=diff${transparency}`,
|
|
5511
|
-
input.palettePath
|
|
5512
|
-
];
|
|
5339
|
+
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
5340
|
+
return toStepResult(await runFfmpeg4(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
5513
5341
|
}
|
|
5514
|
-
function
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5342
|
+
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
5343
|
+
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
5344
|
+
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
5345
|
+
return toStepResult(
|
|
5346
|
+
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
5347
|
+
timeoutMs
|
|
5348
|
+
);
|
|
5349
|
+
};
|
|
5350
|
+
}
|
|
5351
|
+
function toStepResult(result, timeoutMs) {
|
|
5352
|
+
if (result.success) return { success: true };
|
|
5353
|
+
return {
|
|
5354
|
+
success: false,
|
|
5355
|
+
error: `[audioPadTrim] ${describeFfmpegFailure2(result, timeoutMs)}`,
|
|
5356
|
+
failureReason: result.failureReason
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
async function runFfprobeJson(args, signal) {
|
|
5360
|
+
if (!args.includes("--")) {
|
|
5361
|
+
throw new Error('[audioPadTrim] ffprobe args must terminate options with "--".');
|
|
5362
|
+
}
|
|
5363
|
+
const proc = spawn(getFfprobeBinary(), args, {
|
|
5364
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5365
|
+
windowsHide: true
|
|
5366
|
+
});
|
|
5367
|
+
trackChildProcess(proc);
|
|
5368
|
+
let stdout = "";
|
|
5369
|
+
proc.stdout.on("data", (data) => {
|
|
5370
|
+
stdout += data.toString();
|
|
5371
|
+
});
|
|
5372
|
+
const managed = new ManagedChildProcess(proc, {
|
|
5373
|
+
signal,
|
|
5374
|
+
deadlineAtMs: Date.now() + 3e4
|
|
5375
|
+
});
|
|
5376
|
+
const outcome = await managed.wait();
|
|
5377
|
+
if (outcome.reason === "spawn_error") {
|
|
5378
|
+
if (outcome.error?.code === "ENOENT") {
|
|
5379
|
+
throw new Error("[audioPadTrim] ffprobe not found. Please install FFmpeg.");
|
|
5380
|
+
}
|
|
5381
|
+
throw outcome.error ?? new Error(outcome.stderr);
|
|
5382
|
+
}
|
|
5383
|
+
if (outcome.reason !== "exit" || outcome.exitCode !== 0) {
|
|
5384
|
+
const probed = args[args.length - 1];
|
|
5385
|
+
const scrubbed = redactKnownPaths(outcome.stderr, probed === void 0 ? [] : [probed]);
|
|
5386
|
+
throw new Error(`ffprobe ${outcome.reason}: ${redactTelemetryString3(scrubbed, 2e3)}`);
|
|
5387
|
+
}
|
|
5388
|
+
try {
|
|
5389
|
+
return JSON.parse(stdout);
|
|
5390
|
+
} catch (err) {
|
|
5391
|
+
throw new Error(`Failed to parse ffprobe output: ${err.message}`);
|
|
5392
|
+
}
|
|
5529
5393
|
}
|
|
5530
5394
|
|
|
5531
|
-
// src/services/render/stages/
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5395
|
+
// src/services/render/stages/audioStage.ts
|
|
5396
|
+
function assertAudioNotInterrupted(failures) {
|
|
5397
|
+
const interrupted = failures?.find((failure) => failure.reason === "external_interruption");
|
|
5398
|
+
if (!interrupted) return;
|
|
5399
|
+
throw encoderFailureError("Audio processing failed", {
|
|
5400
|
+
error: interrupted.detail,
|
|
5401
|
+
failureReason: "external_interruption",
|
|
5402
|
+
audioFailures: audioFailureDiagnostics([
|
|
5403
|
+
interrupted,
|
|
5404
|
+
...(failures ?? []).filter((failure) => failure !== interrupted)
|
|
5405
|
+
])
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
async function runAudioStage(input) {
|
|
5409
|
+
const stageStart = Date.now();
|
|
5410
|
+
const pcmSourcePath = join10(input.workDir, "audio-source.wav");
|
|
5411
|
+
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
5412
|
+
try {
|
|
5413
|
+
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
5414
|
+
let audioLoweredDb;
|
|
5415
|
+
if (result.hasAudio) {
|
|
5416
|
+
const peak = await constrainAacTruePeak({
|
|
5417
|
+
audioPath: result.audioOutputPath,
|
|
5418
|
+
sourcePath: pcmSourcePath,
|
|
5419
|
+
targetDurationSeconds: duration,
|
|
5420
|
+
signal: input.abortSignal,
|
|
5421
|
+
timeoutMs: input.ffmpegProcessTimeout
|
|
5422
|
+
});
|
|
5423
|
+
if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
|
|
5424
|
+
audioLoweredDb = peak.loweredDb;
|
|
5425
|
+
}
|
|
5426
|
+
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
5427
|
+
} finally {
|
|
5428
|
+
rmSync5(pcmSourcePath, { force: true });
|
|
5537
5429
|
}
|
|
5538
|
-
return resolved;
|
|
5539
5430
|
}
|
|
5540
|
-
function
|
|
5431
|
+
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
5432
|
+
const {
|
|
5433
|
+
projectDir,
|
|
5434
|
+
workDir,
|
|
5435
|
+
compiledDir,
|
|
5436
|
+
ffmpegProcessTimeout,
|
|
5437
|
+
audioGain,
|
|
5438
|
+
audios,
|
|
5439
|
+
masterFxChain,
|
|
5440
|
+
urlByAbsentLocalPath,
|
|
5441
|
+
abortSignal,
|
|
5442
|
+
assertNotAborted,
|
|
5443
|
+
log
|
|
5444
|
+
} = input;
|
|
5445
|
+
const audioOutputPath = join10(workDir, MIXED_AUDIO_FILENAME);
|
|
5446
|
+
let hasAudio = false;
|
|
5447
|
+
let audioError;
|
|
5448
|
+
let audioFailures;
|
|
5449
|
+
if (audios.length > 0) {
|
|
5450
|
+
let audioResult;
|
|
5451
|
+
try {
|
|
5452
|
+
audioResult = await processCompositionAudio(
|
|
5453
|
+
audios,
|
|
5454
|
+
projectDir,
|
|
5455
|
+
join10(workDir, "audio-work"),
|
|
5456
|
+
audioOutputPath,
|
|
5457
|
+
duration,
|
|
5458
|
+
abortSignal,
|
|
5459
|
+
{
|
|
5460
|
+
ffmpegProcessTimeout,
|
|
5461
|
+
audioGain,
|
|
5462
|
+
masterFxChain,
|
|
5463
|
+
urlByAbsentLocalPath,
|
|
5464
|
+
keepEncoderInputAt: pcmSourcePath
|
|
5465
|
+
},
|
|
5466
|
+
compiledDir
|
|
5467
|
+
);
|
|
5468
|
+
} catch (err) {
|
|
5469
|
+
assertNotAborted();
|
|
5470
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
5471
|
+
return {
|
|
5472
|
+
audioOutputPath,
|
|
5473
|
+
hasAudio: false,
|
|
5474
|
+
audioError: detail,
|
|
5475
|
+
// Synthesised rather than left undefined. The warning policy reads
|
|
5476
|
+
// owner, retryability, reason and stage off this list, so reporting the
|
|
5477
|
+
// FATAL failure — the one that took the whole mix down — with an empty
|
|
5478
|
+
// one gave it strictly less classification than a single dropped track
|
|
5479
|
+
// gets, which is the opposite of what this stage exists to do.
|
|
5480
|
+
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
|
5481
|
+
// and this is the FX render, which is none of them.
|
|
5482
|
+
audioFailures: [
|
|
5483
|
+
{
|
|
5484
|
+
stage: "internal",
|
|
5485
|
+
reason: "internal",
|
|
5486
|
+
owner: "system",
|
|
5487
|
+
retryable: false,
|
|
5488
|
+
detail: detail.slice(0, 2e3)
|
|
5489
|
+
}
|
|
5490
|
+
]
|
|
5491
|
+
};
|
|
5492
|
+
}
|
|
5493
|
+
assertNotAborted();
|
|
5494
|
+
assertAudioNotInterrupted(audioResult.failures);
|
|
5495
|
+
hasAudio = audioResult.success;
|
|
5496
|
+
audioFailures = audioResult.failures;
|
|
5497
|
+
if (!hasAudio) {
|
|
5498
|
+
audioError = audioResult.error ?? "audio mix failed for an unknown reason";
|
|
5499
|
+
for (const failure of audioResult.failures ?? []) {
|
|
5500
|
+
log?.warn("Audio track failed", {
|
|
5501
|
+
elementId: failure.elementId,
|
|
5502
|
+
stage: failure.stage,
|
|
5503
|
+
reason: failure.reason,
|
|
5504
|
+
owner: failure.owner,
|
|
5505
|
+
retryable: failure.retryable,
|
|
5506
|
+
detail: failure.detail
|
|
5507
|
+
});
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5541
5511
|
return {
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
fileSize: 0,
|
|
5547
|
-
error,
|
|
5548
|
-
failureReason
|
|
5512
|
+
audioOutputPath,
|
|
5513
|
+
hasAudio,
|
|
5514
|
+
audioError,
|
|
5515
|
+
audioFailures
|
|
5549
5516
|
};
|
|
5550
5517
|
}
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5518
|
+
|
|
5519
|
+
// src/services/render/stages/captureStage.ts
|
|
5520
|
+
import { existsSync as existsSync8, readdirSync, statfsSync as statfsSync2 } from "node:fs";
|
|
5521
|
+
import { join as join11 } from "node:path";
|
|
5522
|
+
import {
|
|
5523
|
+
classifyCaptureFailure as classifyCaptureFailure2,
|
|
5524
|
+
frameFileExtension,
|
|
5525
|
+
captureFrame,
|
|
5526
|
+
captureFrameToBufferPipelined as captureFrameToBufferPipelined2,
|
|
5527
|
+
verifyDiskDrawElementSamples,
|
|
5528
|
+
writeCapturedFrame,
|
|
5529
|
+
closeCaptureSession as closeCaptureSession5,
|
|
5530
|
+
completeDeferredDrawElementInit as completeDeferredDrawElementInit3,
|
|
5531
|
+
createCaptureSession as createCaptureSession5,
|
|
5532
|
+
getCapturePerfSummary as getCapturePerfSummary3,
|
|
5533
|
+
initializeSession as initializeSession5,
|
|
5534
|
+
prepareCaptureSessionForReuse as prepareCaptureSessionForReuse3
|
|
5535
|
+
} from "@hyperframes/engine";
|
|
5536
|
+
function shouldAllowAdaptiveCaptureRetry(workerCount, _explicitlyConfigured) {
|
|
5537
|
+
return workerCount > 1;
|
|
5538
|
+
}
|
|
5539
|
+
var DISK_PROJECTION_SAMPLE_FRAMES = 10;
|
|
5540
|
+
function freeDiskBytesAt(path) {
|
|
5541
|
+
try {
|
|
5542
|
+
const stat = statfsSync2(path);
|
|
5543
|
+
return stat.bavail * stat.bsize;
|
|
5544
|
+
} catch {
|
|
5545
|
+
return null;
|
|
5562
5546
|
}
|
|
5563
|
-
writeFileSync2(outputPath, gif);
|
|
5564
|
-
return null;
|
|
5565
5547
|
}
|
|
5566
|
-
|
|
5567
|
-
const
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
if (frameCount === 0) {
|
|
5571
|
-
return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
|
|
5548
|
+
function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
|
|
5549
|
+
const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
|
|
5550
|
+
if (workerCount > 1 && oneWorker.enabled) {
|
|
5551
|
+
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.`;
|
|
5572
5552
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
try {
|
|
5583
|
-
const run = { signal: input.signal, timeout: input.timeout };
|
|
5584
|
-
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
5585
|
-
const result = await runFfmpeg3(args, run);
|
|
5586
|
-
if (!result.success) {
|
|
5587
|
-
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
5588
|
-
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
5589
|
-
}
|
|
5590
|
-
}
|
|
5591
|
-
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync5(outputPath)) !== false) {
|
|
5592
|
-
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
5593
|
-
if (failure) return failure;
|
|
5594
|
-
}
|
|
5595
|
-
const fileSize = existsSync9(outputPath) ? statSync3(outputPath).size : 0;
|
|
5596
|
-
return {
|
|
5597
|
-
success: true,
|
|
5598
|
-
outputPath,
|
|
5599
|
-
durationMs: Date.now() - startTime,
|
|
5600
|
-
framesEncoded: frameCount,
|
|
5601
|
-
fileSize
|
|
5602
|
-
};
|
|
5603
|
-
} finally {
|
|
5604
|
-
rmSync4(input.palettePath, { force: true });
|
|
5553
|
+
switch (oneWorker.reason) {
|
|
5554
|
+
case "format_excluded":
|
|
5555
|
+
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
5556
|
+
case "disabled_by_config":
|
|
5557
|
+
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
5558
|
+
case "duration_cap":
|
|
5559
|
+
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.";
|
|
5560
|
+
default:
|
|
5561
|
+
return "Free up disk space.";
|
|
5605
5562
|
}
|
|
5606
5563
|
}
|
|
5607
|
-
function
|
|
5608
|
-
const
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
});
|
|
5613
|
-
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
5564
|
+
function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
|
|
5565
|
+
const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
|
|
5566
|
+
return new Error(
|
|
5567
|
+
`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}`
|
|
5568
|
+
);
|
|
5614
5569
|
}
|
|
5615
|
-
function
|
|
5616
|
-
|
|
5570
|
+
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
5571
|
+
let total = sampleDirectoryBytes(framesDir);
|
|
5572
|
+
if (!existsSync8(workDir)) return total;
|
|
5573
|
+
for (const attempt of readdirSync(workDir)) {
|
|
5574
|
+
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
5575
|
+
total += sampleDirectoryBytes(join11(workDir, attempt));
|
|
5576
|
+
continue;
|
|
5577
|
+
}
|
|
5578
|
+
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
5579
|
+
const attemptDir = join11(workDir, attempt);
|
|
5580
|
+
for (const name of readdirSync(attemptDir)) {
|
|
5581
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join11(attemptDir, name));
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
return total;
|
|
5617
5585
|
}
|
|
5618
|
-
function
|
|
5619
|
-
const {
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
5634
|
-
// renders leave both undefined → preserves the existing open-GOP output.
|
|
5635
|
-
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
5636
|
-
gopSize: input.gopSize
|
|
5586
|
+
function createDiskCaptureProjection(input) {
|
|
5587
|
+
const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
|
|
5588
|
+
let checked = false;
|
|
5589
|
+
return (capturedFrames) => {
|
|
5590
|
+
if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
|
|
5591
|
+
const measured = measureBytes();
|
|
5592
|
+
if (measured <= 0) return;
|
|
5593
|
+
checked = true;
|
|
5594
|
+
const remaining = totalFrames - capturedFrames;
|
|
5595
|
+
const freeBytes = freeDiskBytes(framesDir);
|
|
5596
|
+
if (remaining <= 0 || freeBytes === null) return;
|
|
5597
|
+
const needBytes = Math.ceil(measured / capturedFrames * remaining);
|
|
5598
|
+
if (needBytes > freeBytes * 0.9) {
|
|
5599
|
+
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
|
|
5600
|
+
}
|
|
5637
5601
|
};
|
|
5638
5602
|
}
|
|
5639
|
-
async function
|
|
5603
|
+
async function runCaptureStage(input) {
|
|
5640
5604
|
const {
|
|
5605
|
+
fileServer,
|
|
5606
|
+
workDir,
|
|
5607
|
+
framesDir,
|
|
5641
5608
|
job,
|
|
5609
|
+
totalFrames,
|
|
5610
|
+
cfg,
|
|
5611
|
+
plan: plan2,
|
|
5642
5612
|
log,
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
needsAlpha,
|
|
5647
|
-
hasAudio,
|
|
5648
|
-
audioOutputPath,
|
|
5649
|
-
isPngSequence,
|
|
5650
|
-
isGif,
|
|
5651
|
-
enableChunkedEncode,
|
|
5652
|
-
chunkedEncodeSize,
|
|
5613
|
+
captureAttempts,
|
|
5614
|
+
buildCaptureOptions,
|
|
5615
|
+
createRenderVideoFrameInjector,
|
|
5653
5616
|
abortSignal,
|
|
5654
|
-
|
|
5655
|
-
|
|
5617
|
+
onProgress,
|
|
5618
|
+
frameRange,
|
|
5619
|
+
dedupPerfs
|
|
5656
5620
|
} = input;
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5621
|
+
let { probeSession } = input;
|
|
5622
|
+
let { workerCount } = plan2;
|
|
5623
|
+
const { forceScreenshot } = plan2;
|
|
5624
|
+
let lastBrowserConsole = [];
|
|
5625
|
+
let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
|
|
5626
|
+
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
5627
|
+
if (frameRange !== void 0) {
|
|
5628
|
+
if (!Number.isFinite(frameRange.startFrame) || !Number.isFinite(frameRange.endFrame) || frameRange.startFrame < 0 || frameRange.endFrame <= frameRange.startFrame) {
|
|
5663
5629
|
throw new Error(
|
|
5664
|
-
`[
|
|
5630
|
+
`[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
|
|
5665
5631
|
);
|
|
5666
5632
|
}
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
if (hasAudio && audioOutputPath && existsSync9(audioOutputPath)) {
|
|
5672
|
-
copyFileSync3(audioOutputPath, join12(outputPath, MIXED_AUDIO_FILENAME2));
|
|
5673
|
-
log.info(
|
|
5674
|
-
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
5633
|
+
const rangeFrames = frameRange.endFrame - frameRange.startFrame;
|
|
5634
|
+
if (rangeFrames !== totalFrames) {
|
|
5635
|
+
throw new Error(
|
|
5636
|
+
`[captureStage] frameRange size (${rangeFrames}) must equal totalFrames (${totalFrames}). Received frameRange=${JSON.stringify(frameRange)}.`
|
|
5675
5637
|
);
|
|
5676
5638
|
}
|
|
5677
|
-
encoded();
|
|
5678
|
-
return { encodeMs: Date.now() - stage5Start };
|
|
5679
5639
|
}
|
|
5680
|
-
const
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
const
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5640
|
+
const captureOptions = buildCaptureOptions();
|
|
5641
|
+
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
5642
|
+
const checkDiskProjection = createDiskCaptureProjection({
|
|
5643
|
+
framesDir,
|
|
5644
|
+
totalFrames,
|
|
5645
|
+
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
|
|
5646
|
+
remedy
|
|
5647
|
+
});
|
|
5648
|
+
if (workerCount > 1) {
|
|
5649
|
+
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
5650
|
+
serverUrl: fileServer.url,
|
|
5651
|
+
workDir,
|
|
5652
|
+
framesDir,
|
|
5653
|
+
totalFrames,
|
|
5654
|
+
initialWorkerCount: workerCount,
|
|
5655
|
+
allowRetry: shouldAllowAdaptiveCaptureRetry(workerCount, job.config.workers !== void 0),
|
|
5656
|
+
frameExt: frameFileExtension(captureOptions.format),
|
|
5657
|
+
captureOptions,
|
|
5658
|
+
createBeforeCaptureHook: createRenderVideoFrameInjector,
|
|
5659
|
+
abortSignal,
|
|
5660
|
+
frameRangeStart: frameRange?.startFrame,
|
|
5661
|
+
dedupPerfs,
|
|
5662
|
+
onProgress: (progress) => {
|
|
5663
|
+
if (progress.latestWorkerPhase) {
|
|
5664
|
+
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
5668
|
+
job.framesRendered = progress.capturedFrames;
|
|
5669
|
+
checkDiskProjection(progress.capturedFrames);
|
|
5670
|
+
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
5671
|
+
reportFrameProgress(
|
|
5672
|
+
job,
|
|
5673
|
+
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${progress.activeWorkers} workers)`,
|
|
5674
|
+
Math.round(25 + frameProgress * 45),
|
|
5675
|
+
onProgress,
|
|
5676
|
+
progress.capturedFrames,
|
|
5677
|
+
progress.totalFrames
|
|
5678
|
+
);
|
|
5679
|
+
},
|
|
5680
|
+
cfg: captureCfg,
|
|
5681
|
+
log
|
|
5696
5682
|
});
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5683
|
+
captureAttempts.push(...attempts);
|
|
5684
|
+
const lastAttempt = attempts[attempts.length - 1];
|
|
5685
|
+
if (lastAttempt) {
|
|
5686
|
+
workerCount = lastAttempt.workers;
|
|
5700
5687
|
}
|
|
5701
|
-
|
|
5702
|
-
|
|
5688
|
+
if (probeSession) {
|
|
5689
|
+
captureBeyondViewport = probeSession.options.captureBeyondViewport;
|
|
5690
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
5691
|
+
await closeCaptureSession5(probeSession);
|
|
5692
|
+
probeSession = null;
|
|
5693
|
+
}
|
|
5694
|
+
} else {
|
|
5695
|
+
const sequential = await runSequentialDiskCapture(
|
|
5696
|
+
input,
|
|
5697
|
+
captureOptions,
|
|
5698
|
+
captureCfg,
|
|
5699
|
+
checkDiskProjection
|
|
5700
|
+
);
|
|
5701
|
+
probeSession = null;
|
|
5702
|
+
lastBrowserConsole = sequential.lastBrowserConsole;
|
|
5703
|
+
captureBeyondViewport = sequential.captureBeyondViewport;
|
|
5703
5704
|
}
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
const
|
|
5708
|
-
|
|
5709
|
-
const framePattern = `frame_%06d.${frameExt}`;
|
|
5710
|
-
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
5711
|
-
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
5712
|
-
framesDir,
|
|
5713
|
-
framePattern,
|
|
5714
|
-
videoOnlyPath,
|
|
5715
|
-
encoderOpts,
|
|
5716
|
-
chunkedEncodeSize,
|
|
5717
|
-
abortSignal,
|
|
5718
|
-
videoEngineCfg,
|
|
5719
|
-
onFramesEncoded
|
|
5720
|
-
) : await encodeFramesFromDir(
|
|
5705
|
+
return { workerCount, probeSession, lastBrowserConsole, captureBeyondViewport };
|
|
5706
|
+
}
|
|
5707
|
+
async function runSequentialDiskCapture(input, captureOptions, captureCfg, checkDiskProjection) {
|
|
5708
|
+
const {
|
|
5709
|
+
fileServer,
|
|
5721
5710
|
framesDir,
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5711
|
+
totalFrames,
|
|
5712
|
+
log,
|
|
5713
|
+
createRenderVideoFrameInjector,
|
|
5725
5714
|
abortSignal,
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
}
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5715
|
+
assertNotAborted,
|
|
5716
|
+
frameRange
|
|
5717
|
+
} = input;
|
|
5718
|
+
let { probeSession } = input;
|
|
5719
|
+
let lastBrowserConsole = [];
|
|
5720
|
+
let captureBeyondViewport;
|
|
5721
|
+
const { rangeStart, rangeEnd } = resolveCaptureRange(frameRange, totalFrames);
|
|
5722
|
+
const rangeFrames = rangeEnd - rangeStart;
|
|
5723
|
+
const frameExt = frameFileExtension(captureOptions.format);
|
|
5724
|
+
let transientRetriesUsed = 0;
|
|
5725
|
+
let resumeFrom = 0;
|
|
5726
|
+
while (true) {
|
|
5727
|
+
const videoInjector = createRenderVideoFrameInjector();
|
|
5728
|
+
const session = probeSession ?? await createCaptureSession5(
|
|
5729
|
+
fileServer.url,
|
|
5730
|
+
framesDir,
|
|
5731
|
+
captureOptions,
|
|
5732
|
+
videoInjector,
|
|
5733
|
+
captureCfg
|
|
5734
|
+
);
|
|
5735
|
+
captureBeyondViewport = session.options.captureBeyondViewport;
|
|
5736
|
+
try {
|
|
5737
|
+
if (probeSession) {
|
|
5738
|
+
prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
|
|
5739
|
+
probeSession = null;
|
|
5740
|
+
}
|
|
5741
|
+
await ensureSessionReady(session);
|
|
5742
|
+
assertNotAborted();
|
|
5743
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
5744
|
+
await captureSessionFrames(session, {
|
|
5745
|
+
input,
|
|
5746
|
+
rangeStart,
|
|
5747
|
+
rangeEnd,
|
|
5748
|
+
resumeFrom,
|
|
5749
|
+
checkDiskProjection
|
|
5750
|
+
});
|
|
5751
|
+
break;
|
|
5752
|
+
} catch (error) {
|
|
5753
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
5754
|
+
const resume = planTransientResume(error, {
|
|
5755
|
+
abortSignal,
|
|
5756
|
+
rangeFrames,
|
|
5757
|
+
framesDir,
|
|
5758
|
+
frameExt,
|
|
5759
|
+
retriesUsed: transientRetriesUsed
|
|
5760
|
+
});
|
|
5761
|
+
if (resume !== null) {
|
|
5762
|
+
transientRetriesUsed++;
|
|
5763
|
+
resumeFrom = resume;
|
|
5764
|
+
log.warn(
|
|
5765
|
+
"[Render] Transient browser failure during sequential capture; retrying once with a fresh session.",
|
|
5766
|
+
{
|
|
5767
|
+
resumeFrom,
|
|
5768
|
+
rangeFrames,
|
|
5769
|
+
transientRetriesUsed,
|
|
5770
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5771
|
+
}
|
|
5772
|
+
);
|
|
5773
|
+
continue;
|
|
5774
|
+
}
|
|
5775
|
+
throw wrapCaptureStageError(error, lastBrowserConsole);
|
|
5776
|
+
} finally {
|
|
5777
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
5778
|
+
await closeCaptureSession5(session);
|
|
5783
5779
|
}
|
|
5784
|
-
};
|
|
5785
|
-
const result = await runFfmpeg4(
|
|
5786
|
-
["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
|
|
5787
|
-
{ signal, timeout: timeoutMs, onStderr: readLines }
|
|
5788
|
-
);
|
|
5789
|
-
if (!result.success) {
|
|
5790
|
-
throw new Error(`[audioTruePeak] ${describeFfmpegFailure(result, timeoutMs)}`);
|
|
5791
5780
|
}
|
|
5792
|
-
|
|
5793
|
-
return {
|
|
5794
|
-
peak: checkedDbfs(toDbfs(summary.at(-1)?.[1] ?? "nan")),
|
|
5795
|
-
firstBlock: checkedDbfs(firstBlock ?? Number.NEGATIVE_INFINITY),
|
|
5796
|
-
laterBlocks: checkedDbfs(laterBlocks)
|
|
5797
|
-
};
|
|
5781
|
+
return { lastBrowserConsole, captureBeyondViewport };
|
|
5798
5782
|
}
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
5802
|
-
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
5803
|
-
const block = 4800;
|
|
5804
|
-
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
5805
|
-
if (count === 1) return wholeFile();
|
|
5806
|
-
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
5807
|
-
const peers = new AbortController();
|
|
5808
|
-
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
5809
|
-
const scans = await Promise.all(
|
|
5810
|
-
Array.from({ length: count }, (_, k) => {
|
|
5811
|
-
const start = k === 0 ? 0 : cut(k) - block;
|
|
5812
|
-
const end = k === count - 1 ? "" : `:end_sample=${cut(k + 1)}`;
|
|
5813
|
-
return scanTruePeak(
|
|
5814
|
-
audioPath,
|
|
5815
|
-
`atrim=start_sample=${start}${end},`,
|
|
5816
|
-
timeoutMs,
|
|
5817
|
-
segmentSignal
|
|
5818
|
-
);
|
|
5819
|
-
})
|
|
5820
|
-
).catch((error) => {
|
|
5821
|
-
peers.abort();
|
|
5822
|
-
throw error;
|
|
5823
|
-
});
|
|
5824
|
-
const last = scans[count - 1];
|
|
5825
|
-
const counted = Math.max(scans[0].peak, ...scans.slice(1).map((scan) => scan.laterBlocks));
|
|
5826
|
-
if (last.peak > last.firstBlock || last.peak <= counted) return Math.max(counted, last.peak);
|
|
5827
|
-
return wholeFile();
|
|
5783
|
+
function resolveCaptureRange(frameRange, totalFrames) {
|
|
5784
|
+
return { rangeStart: frameRange?.startFrame ?? 0, rangeEnd: frameRange?.endFrame ?? totalFrames };
|
|
5828
5785
|
}
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
5836
|
-
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
5786
|
+
function planTransientResume(error, ctx) {
|
|
5787
|
+
const failure = classifyCaptureFailure2(error, { signal: ctx.abortSignal });
|
|
5788
|
+
const missing = findMissingFrameRanges(ctx.rangeFrames, ctx.framesDir, ctx.frameExt);
|
|
5789
|
+
const [firstMissing] = missing;
|
|
5790
|
+
if (!firstMissing) return null;
|
|
5791
|
+
return isTransientCaptureRetryEligible(failure.kind, missing, ctx.retriesUsed) ? firstMissing.startFrame : null;
|
|
5837
5792
|
}
|
|
5838
|
-
function
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
return {
|
|
5843
|
-
operation: "copy",
|
|
5844
|
-
steps: [{ kind: "copy", args: ["-i", audioPath, "-c:a", "copy", "-y", outputPath] }],
|
|
5845
|
-
cleanupPaths: []
|
|
5846
|
-
};
|
|
5793
|
+
async function ensureSessionReady(session) {
|
|
5794
|
+
if (!session.isInitialized) {
|
|
5795
|
+
await initializeSession5(session);
|
|
5796
|
+
return;
|
|
5847
5797
|
}
|
|
5848
|
-
if (
|
|
5849
|
-
|
|
5850
|
-
operation: "pad",
|
|
5851
|
-
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
5852
|
-
cleanupPaths: []
|
|
5853
|
-
};
|
|
5798
|
+
if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
|
|
5799
|
+
await completeDeferredDrawElementInit3(session);
|
|
5854
5800
|
}
|
|
5855
|
-
return {
|
|
5856
|
-
operation: "trim",
|
|
5857
|
-
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
5858
|
-
cleanupPaths: []
|
|
5859
|
-
};
|
|
5860
|
-
}
|
|
5861
|
-
function formatSeconds(sec) {
|
|
5862
|
-
return sec.toFixed(6);
|
|
5863
5801
|
}
|
|
5864
|
-
function
|
|
5865
|
-
const
|
|
5866
|
-
const
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
probeVideo(input.videoPath),
|
|
5878
|
-
probeAudio(input.audioPath, input.signal)
|
|
5879
|
-
]);
|
|
5880
|
-
const probePaths = [input.videoPath, input.audioPath, input.outputPath];
|
|
5881
|
-
if (videoResult.status === "rejected") {
|
|
5882
|
-
return failResult(
|
|
5883
|
-
input.outputPath,
|
|
5884
|
-
0,
|
|
5885
|
-
audioResult.status === "fulfilled" ? audioResult.value.durationSeconds : 0,
|
|
5886
|
-
`audioPadTrim: failed to probe video: ${sanitizeProbeFailure(
|
|
5887
|
-
videoResult.reason,
|
|
5888
|
-
probePaths
|
|
5889
|
-
)}`
|
|
5890
|
-
);
|
|
5891
|
-
}
|
|
5892
|
-
if (audioResult.status === "rejected") {
|
|
5893
|
-
return failResult(
|
|
5894
|
-
input.outputPath,
|
|
5895
|
-
0,
|
|
5896
|
-
0,
|
|
5897
|
-
`audioPadTrim: failed to probe audio: ${sanitizeProbeFailure(
|
|
5898
|
-
audioResult.reason,
|
|
5899
|
-
probePaths
|
|
5900
|
-
)}`
|
|
5901
|
-
);
|
|
5902
|
-
}
|
|
5903
|
-
const videoInfo = videoResult.value;
|
|
5904
|
-
const audioInfo = audioResult.value;
|
|
5905
|
-
if (!Number.isFinite(videoInfo.frameCount) || videoInfo.frameCount <= 0 || !Number.isFinite(videoInfo.fpsNum) || videoInfo.fpsNum <= 0 || !Number.isFinite(videoInfo.fpsDen) || videoInfo.fpsDen <= 0) {
|
|
5906
|
-
return failResult(
|
|
5907
|
-
input.outputPath,
|
|
5908
|
-
0,
|
|
5909
|
-
audioInfo.durationSeconds,
|
|
5910
|
-
`audioPadTrim: invalid video frame info: ${JSON.stringify(videoInfo)}`
|
|
5802
|
+
async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resumeFrom, checkDiskProjection }) {
|
|
5803
|
+
const { job, framesDir, dedupPerfs, assertNotAborted, onProgress } = input;
|
|
5804
|
+
const rangeFrames = rangeEnd - rangeStart;
|
|
5805
|
+
const reportFrame = (fileIndex) => {
|
|
5806
|
+
job.framesRendered = fileIndex + 1;
|
|
5807
|
+
checkDiskProjection(fileIndex + 1);
|
|
5808
|
+
reportFrameProgress(
|
|
5809
|
+
job,
|
|
5810
|
+
`Capturing frame ${fileIndex + 1}/${rangeFrames}`,
|
|
5811
|
+
Math.round(25 + (fileIndex + 1) / rangeFrames * 45),
|
|
5812
|
+
onProgress,
|
|
5813
|
+
fileIndex + 1,
|
|
5814
|
+
rangeFrames
|
|
5911
5815
|
);
|
|
5912
|
-
}
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
for (const step of plan2.steps) {
|
|
5922
|
-
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
5923
|
-
if (!ffmpegResult.success) {
|
|
5924
|
-
return {
|
|
5925
|
-
success: false,
|
|
5926
|
-
outputPath: input.outputPath,
|
|
5927
|
-
targetDurationSeconds,
|
|
5928
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5929
|
-
operation: plan2.operation,
|
|
5930
|
-
error: ffmpegResult.error,
|
|
5931
|
-
failureReason: ffmpegResult.failureReason
|
|
5932
|
-
};
|
|
5933
|
-
}
|
|
5934
|
-
}
|
|
5935
|
-
} catch (err) {
|
|
5936
|
-
return {
|
|
5937
|
-
success: false,
|
|
5938
|
-
outputPath: input.outputPath,
|
|
5939
|
-
targetDurationSeconds,
|
|
5940
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5941
|
-
operation: plan2.operation,
|
|
5942
|
-
error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
5816
|
+
};
|
|
5817
|
+
if (session.workerEncodeEnabled) {
|
|
5818
|
+
let prev = null;
|
|
5819
|
+
const drainPrev = async () => {
|
|
5820
|
+
if (!prev) return;
|
|
5821
|
+
assertNotAborted();
|
|
5822
|
+
const buf = await prev.encodeResult;
|
|
5823
|
+
writeCapturedFrame(session, prev.fileIndex, buf);
|
|
5824
|
+
reportFrame(prev.fileIndex);
|
|
5943
5825
|
};
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
targetDurationSeconds,
|
|
5961
|
-
sourceDurationSeconds: audioInfo.durationSeconds,
|
|
5962
|
-
operation: plan2.operation,
|
|
5963
|
-
error: correction.error,
|
|
5964
|
-
failureReason: correction.failureReason
|
|
5965
|
-
};
|
|
5826
|
+
for (let i = resumeFrom; i < rangeFrames; i++) {
|
|
5827
|
+
assertNotAborted();
|
|
5828
|
+
const absoluteIdx = rangeStart + i;
|
|
5829
|
+
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
5830
|
+
const { encodeResult } = await captureFrameToBufferPipelined2(session, i, time);
|
|
5831
|
+
await drainPrev();
|
|
5832
|
+
prev = { fileIndex: i, encodeResult };
|
|
5833
|
+
}
|
|
5834
|
+
await drainPrev();
|
|
5835
|
+
} else {
|
|
5836
|
+
for (let i = resumeFrom; i < rangeFrames; i++) {
|
|
5837
|
+
assertNotAborted();
|
|
5838
|
+
const absoluteIdx = rangeStart + i;
|
|
5839
|
+
const time = absoluteIdx * job.config.fps.den / job.config.fps.num;
|
|
5840
|
+
await captureFrame(session, i, time);
|
|
5841
|
+
reportFrame(i);
|
|
5966
5842
|
}
|
|
5967
|
-
loweredDb = correction.loweredDb ?? 0;
|
|
5968
5843
|
}
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5844
|
+
await verifyDiskDrawElementSamples(
|
|
5845
|
+
session,
|
|
5846
|
+
{
|
|
5847
|
+
workerId: 0,
|
|
5848
|
+
startFrame: rangeStart + resumeFrom,
|
|
5849
|
+
endFrame: rangeEnd,
|
|
5850
|
+
outputDir: framesDir,
|
|
5851
|
+
outputFrameOffset: rangeStart
|
|
5852
|
+
},
|
|
5853
|
+
false
|
|
5854
|
+
);
|
|
5855
|
+
dedupPerfs.push(getCapturePerfSummary3(session));
|
|
5977
5856
|
}
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5857
|
+
|
|
5858
|
+
// src/services/render/stages/captureHdrStage.ts
|
|
5859
|
+
import {
|
|
5860
|
+
closeCaptureSession as closeCaptureSession7,
|
|
5861
|
+
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
5862
|
+
createCaptureSession as createCaptureSession7,
|
|
5863
|
+
initTransparentBackground as initTransparentBackground2,
|
|
5864
|
+
initializeSession as initializeSession7,
|
|
5865
|
+
spawnStreamingEncoder as spawnStreamingEncoder3
|
|
5866
|
+
} from "@hyperframes/engine";
|
|
5867
|
+
import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
|
|
5868
|
+
|
|
5869
|
+
// src/services/hdrImageTransferCache.ts
|
|
5870
|
+
import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
|
|
5871
|
+
var DEFAULT_MAX_BYTES = 200 * 1024 * 1024;
|
|
5872
|
+
|
|
5873
|
+
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
5874
|
+
import {
|
|
5875
|
+
TRANSITIONS,
|
|
5876
|
+
crossfade
|
|
5877
|
+
} from "@hyperframes/engine";
|
|
5878
|
+
|
|
5879
|
+
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
5880
|
+
import {
|
|
5881
|
+
TRANSITIONS as TRANSITIONS2,
|
|
5882
|
+
closeCaptureSession as closeCaptureSession6,
|
|
5883
|
+
createCaptureSession as createCaptureSession6,
|
|
5884
|
+
createFrameReorderBuffer as createFrameReorderBuffer2,
|
|
5885
|
+
crossfade as crossfade2,
|
|
5886
|
+
initTransparentBackground,
|
|
5887
|
+
initializeSession as initializeSession6
|
|
5888
|
+
} from "@hyperframes/engine";
|
|
5889
|
+
|
|
5890
|
+
// src/services/render/stages/encodeStage.ts
|
|
5891
|
+
import {
|
|
5892
|
+
copyFileSync as copyFileSync3,
|
|
5893
|
+
existsSync as existsSync9,
|
|
5894
|
+
mkdirSync as mkdirSync5,
|
|
5895
|
+
readdirSync as readdirSync2,
|
|
5896
|
+
readFileSync as readFileSync5,
|
|
5897
|
+
rmSync as rmSync6,
|
|
5898
|
+
statSync as statSync3,
|
|
5899
|
+
writeFileSync as writeFileSync2
|
|
5900
|
+
} from "node:fs";
|
|
5901
|
+
import { dirname as dirname6, join as join12 } from "node:path";
|
|
5902
|
+
import {
|
|
5903
|
+
encodeFramesChunkedConcat,
|
|
5904
|
+
encodeFramesFromDir,
|
|
5905
|
+
formatFfmpegError,
|
|
5906
|
+
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
5907
|
+
resolveConfig,
|
|
5908
|
+
runFfmpeg as runFfmpeg5,
|
|
5909
|
+
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
5910
|
+
} from "@hyperframes/engine";
|
|
5911
|
+
import {
|
|
5912
|
+
clearGifFramesBeforeNext,
|
|
5913
|
+
gifClearsAfterLeavingFrameInPlace
|
|
5914
|
+
} from "@hyperframes/core";
|
|
5915
|
+
|
|
5916
|
+
// src/services/render/stages/gifEncodeArgs.ts
|
|
5917
|
+
import { frameSequencePath } from "@hyperframes/engine";
|
|
5918
|
+
function fpsToFfmpegArg2(fps) {
|
|
5919
|
+
return fps.den === 1 ? String(fps.num) : `${fps.num}/${fps.den}`;
|
|
5987
5920
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
return {
|
|
6036
|
-
success: false,
|
|
6037
|
-
error: `audioPadTrim: failed to validate AAC true peak: ${sanitizeProbeFailure(
|
|
6038
|
-
err,
|
|
6039
|
-
scratchDir ? [input.audioPath, scratchDir] : [input.audioPath]
|
|
6040
|
-
)}`
|
|
6041
|
-
};
|
|
6042
|
-
} finally {
|
|
6043
|
-
if (scratchDir) rmSync5(scratchDir, { recursive: true, force: true });
|
|
5921
|
+
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
5922
|
+
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
5923
|
+
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
5924
|
+
function framesInput(input, fpsArg) {
|
|
5925
|
+
return [
|
|
5926
|
+
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
5927
|
+
"-framerate",
|
|
5928
|
+
fpsArg,
|
|
5929
|
+
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
5930
|
+
"-i",
|
|
5931
|
+
frameSequencePath(input.framesDir, input.framePattern)
|
|
5932
|
+
];
|
|
5933
|
+
}
|
|
5934
|
+
function buildGifPalettegenArgs(input) {
|
|
5935
|
+
const fpsArg = fpsToFfmpegArg2(input.fps);
|
|
5936
|
+
const transparency = input.preserveAlpha ? ":reserve_transparent=1" : "";
|
|
5937
|
+
return [
|
|
5938
|
+
"-y",
|
|
5939
|
+
...framesInput(input, fpsArg),
|
|
5940
|
+
"-vf",
|
|
5941
|
+
`fps=${fpsArg},palettegen=stats_mode=diff${transparency}`,
|
|
5942
|
+
input.palettePath
|
|
5943
|
+
];
|
|
5944
|
+
}
|
|
5945
|
+
function buildGifPaletteuseArgs(input) {
|
|
5946
|
+
const fpsArg = fpsToFfmpegArg2(input.fps);
|
|
5947
|
+
const transparency = input.preserveAlpha ? ":alpha_threshold=128" : "";
|
|
5948
|
+
return [
|
|
5949
|
+
"-y",
|
|
5950
|
+
...framesInput(input, fpsArg),
|
|
5951
|
+
"-i",
|
|
5952
|
+
input.palettePath,
|
|
5953
|
+
"-lavfi",
|
|
5954
|
+
`fps=${fpsArg} [x]; [x][1:v] paletteuse=dither=sierra2_4a${transparency}`,
|
|
5955
|
+
"-loop",
|
|
5956
|
+
String(input.loop),
|
|
5957
|
+
...input.wholeFrames ? WRITE_EVERY_FRAME_WHOLE : [],
|
|
5958
|
+
input.outputPath
|
|
5959
|
+
];
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5962
|
+
// src/services/render/stages/encodeStage.ts
|
|
5963
|
+
import { frameFileExtension as frameFileExtension2 } from "@hyperframes/engine";
|
|
5964
|
+
function resolveGifLoop(loop) {
|
|
5965
|
+
const resolved = loop ?? 0;
|
|
5966
|
+
if (!Number.isInteger(resolved) || resolved < 0 || resolved > 65535) {
|
|
5967
|
+
throw new Error(`[Render] gifLoop must be an integer between 0 and 65535 (got ${resolved})`);
|
|
6044
5968
|
}
|
|
5969
|
+
return resolved;
|
|
6045
5970
|
}
|
|
6046
|
-
function
|
|
5971
|
+
function gifEncodeFailure(startTime, outputPath, error, failureReason) {
|
|
6047
5972
|
return {
|
|
6048
5973
|
success: false,
|
|
6049
5974
|
outputPath,
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
error
|
|
5975
|
+
durationMs: Date.now() - startTime,
|
|
5976
|
+
framesEncoded: 0,
|
|
5977
|
+
fileSize: 0,
|
|
5978
|
+
error,
|
|
5979
|
+
failureReason
|
|
6054
5980
|
};
|
|
6055
5981
|
}
|
|
6056
|
-
async function
|
|
6057
|
-
const
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
"v:0",
|
|
6063
|
-
"-show_entries",
|
|
6064
|
-
"stream=nb_frames,r_frame_rate",
|
|
6065
|
-
"-of",
|
|
6066
|
-
"json",
|
|
6067
|
-
"--",
|
|
6068
|
-
videoPath
|
|
6069
|
-
],
|
|
6070
|
-
signal
|
|
6071
|
-
);
|
|
6072
|
-
let stream = fastInfo.streams?.[0];
|
|
6073
|
-
const fastCount = Number(stream?.nb_frames);
|
|
6074
|
-
if (stream && Number.isFinite(fastCount) && fastCount > 0) {
|
|
6075
|
-
return { frameCount: fastCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
6076
|
-
}
|
|
6077
|
-
const slowInfo = await runFfprobeJson(
|
|
6078
|
-
[
|
|
6079
|
-
"-v",
|
|
6080
|
-
"error",
|
|
6081
|
-
"-select_streams",
|
|
6082
|
-
"v:0",
|
|
6083
|
-
"-count_packets",
|
|
6084
|
-
"-show_entries",
|
|
6085
|
-
"stream=nb_read_packets,r_frame_rate",
|
|
6086
|
-
"-of",
|
|
6087
|
-
"json",
|
|
6088
|
-
"--",
|
|
6089
|
-
videoPath
|
|
6090
|
-
],
|
|
6091
|
-
signal
|
|
6092
|
-
);
|
|
6093
|
-
stream = slowInfo.streams?.[0];
|
|
6094
|
-
if (!stream) throw new Error(`ffprobe found no video stream in ${videoPath}`);
|
|
6095
|
-
const slowCount = Number(stream.nb_read_packets);
|
|
6096
|
-
if (!Number.isFinite(slowCount) || slowCount <= 0) {
|
|
6097
|
-
throw new Error(`ffprobe returned no frame count: ${JSON.stringify(stream)}`);
|
|
6098
|
-
}
|
|
6099
|
-
return { frameCount: slowCount, ...parseFrameRate(stream.r_frame_rate ?? "") };
|
|
6100
|
-
}
|
|
6101
|
-
function parseFrameRate(rate) {
|
|
6102
|
-
const [n, d] = rate.split("/");
|
|
6103
|
-
const fpsNum = Number(n);
|
|
6104
|
-
const fpsDen = d === void 0 ? 1 : Number(d);
|
|
6105
|
-
if (!Number.isFinite(fpsNum) || !Number.isFinite(fpsDen) || fpsNum <= 0 || fpsDen <= 0) {
|
|
6106
|
-
throw new Error(`Invalid r_frame_rate: ${JSON.stringify(rate)}`);
|
|
5982
|
+
async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
|
|
5983
|
+
const { outputPath } = argsInput;
|
|
5984
|
+
const result = await runFfmpeg5(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
|
|
5985
|
+
if (!result.success) {
|
|
5986
|
+
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
5987
|
+
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
6107
5988
|
}
|
|
6108
|
-
|
|
5989
|
+
const gif = readFileSync5(outputPath);
|
|
5990
|
+
if (!clearGifFramesBeforeNext(gif)) {
|
|
5991
|
+
const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
|
|
5992
|
+
return gifEncodeFailure(startTime, outputPath, error);
|
|
5993
|
+
}
|
|
5994
|
+
writeFileSync2(outputPath, gif);
|
|
5995
|
+
return null;
|
|
6109
5996
|
}
|
|
6110
|
-
async function
|
|
6111
|
-
const
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
5997
|
+
async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
5998
|
+
const startTime = Date.now();
|
|
5999
|
+
const files = readdirSync2(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
|
|
6000
|
+
const frameCount = files.length;
|
|
6001
|
+
if (frameCount === 0) {
|
|
6002
|
+
return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
|
|
6003
|
+
}
|
|
6004
|
+
const argsInput = {
|
|
6005
|
+
framesDir,
|
|
6006
|
+
framePattern,
|
|
6007
|
+
palettePath: input.palettePath,
|
|
6008
|
+
outputPath,
|
|
6009
|
+
fps: input.fps,
|
|
6010
|
+
loop: input.loop,
|
|
6011
|
+
preserveAlpha: input.preserveAlpha
|
|
6117
6012
|
};
|
|
6013
|
+
try {
|
|
6014
|
+
const run = { signal: input.signal, timeout: input.timeout };
|
|
6015
|
+
for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
|
|
6016
|
+
const result = await runFfmpeg5(args, run);
|
|
6017
|
+
if (!result.success) {
|
|
6018
|
+
const error = formatFfmpegError(result.exitCode, result.stderr);
|
|
6019
|
+
return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync5(outputPath)) !== false) {
|
|
6023
|
+
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
6024
|
+
if (failure) return failure;
|
|
6025
|
+
}
|
|
6026
|
+
const fileSize = existsSync9(outputPath) ? statSync3(outputPath).size : 0;
|
|
6027
|
+
return {
|
|
6028
|
+
success: true,
|
|
6029
|
+
outputPath,
|
|
6030
|
+
durationMs: Date.now() - startTime,
|
|
6031
|
+
framesEncoded: frameCount,
|
|
6032
|
+
fileSize
|
|
6033
|
+
};
|
|
6034
|
+
} finally {
|
|
6035
|
+
rmSync6(input.palettePath, { force: true });
|
|
6036
|
+
}
|
|
6118
6037
|
}
|
|
6119
|
-
|
|
6120
|
-
|
|
6038
|
+
function startEncodeProgress(job, stage, onProgress) {
|
|
6039
|
+
const total = job.totalFrames ?? 0;
|
|
6040
|
+
updateJobStatus(job, "encoding", stage, 75, onProgress, {
|
|
6041
|
+
code: "encode",
|
|
6042
|
+
...total > 0 && { done: 0, total }
|
|
6043
|
+
});
|
|
6044
|
+
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
6121
6045
|
}
|
|
6122
|
-
function
|
|
6123
|
-
return
|
|
6124
|
-
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
6125
|
-
return toStepResult(
|
|
6126
|
-
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
6127
|
-
timeoutMs
|
|
6128
|
-
);
|
|
6129
|
-
};
|
|
6046
|
+
function resolveEngineConfig(input) {
|
|
6047
|
+
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
6130
6048
|
}
|
|
6131
|
-
function
|
|
6132
|
-
|
|
6049
|
+
function videoEncoderOptions(input, engineCfg) {
|
|
6050
|
+
const { job, preset } = input;
|
|
6133
6051
|
return {
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6052
|
+
fps: job.config.fps,
|
|
6053
|
+
width: input.width,
|
|
6054
|
+
height: input.height,
|
|
6055
|
+
codec: preset.codec,
|
|
6056
|
+
preset: preset.preset,
|
|
6057
|
+
quality: input.effectiveQuality,
|
|
6058
|
+
bitrate: input.effectiveBitrate,
|
|
6059
|
+
pixelFormat: preset.pixelFormat,
|
|
6060
|
+
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
6061
|
+
useGpu: job.config.useGpu,
|
|
6062
|
+
hdr: preset.hdr,
|
|
6063
|
+
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
6064
|
+
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
6065
|
+
// renders leave both undefined → preserves the existing open-GOP output.
|
|
6066
|
+
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
6067
|
+
gopSize: input.gopSize
|
|
6137
6068
|
};
|
|
6138
6069
|
}
|
|
6139
|
-
async function
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6070
|
+
async function runEncodeStage(input) {
|
|
6071
|
+
const {
|
|
6072
|
+
job,
|
|
6073
|
+
log,
|
|
6074
|
+
outputPath,
|
|
6075
|
+
framesDir,
|
|
6076
|
+
videoOnlyPath,
|
|
6077
|
+
needsAlpha,
|
|
6078
|
+
hasAudio,
|
|
6079
|
+
audioOutputPath,
|
|
6080
|
+
isPngSequence,
|
|
6081
|
+
isGif,
|
|
6082
|
+
enableChunkedEncode,
|
|
6083
|
+
chunkedEncodeSize,
|
|
6084
|
+
abortSignal,
|
|
6085
|
+
assertNotAborted,
|
|
6086
|
+
onProgress
|
|
6087
|
+
} = input;
|
|
6088
|
+
const stage5Start = Date.now();
|
|
6089
|
+
if (isPngSequence) {
|
|
6090
|
+
const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
|
|
6091
|
+
if (!existsSync9(outputPath)) mkdirSync5(outputPath, { recursive: true });
|
|
6092
|
+
const captured = readdirSync2(framesDir).filter((name) => name.endsWith(".png")).sort();
|
|
6093
|
+
if (captured.length === 0) {
|
|
6094
|
+
throw new Error(
|
|
6095
|
+
`[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
|
|
6096
|
+
);
|
|
6160
6097
|
}
|
|
6161
|
-
|
|
6098
|
+
captured.forEach((name, i) => {
|
|
6099
|
+
const dst = join12(outputPath, formatExportFrameName(i, "png"));
|
|
6100
|
+
copyFileSync3(join12(framesDir, name), dst);
|
|
6101
|
+
});
|
|
6102
|
+
if (hasAudio && audioOutputPath && existsSync9(audioOutputPath)) {
|
|
6103
|
+
copyFileSync3(audioOutputPath, join12(outputPath, MIXED_AUDIO_FILENAME2));
|
|
6104
|
+
log.info(
|
|
6105
|
+
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
6106
|
+
);
|
|
6107
|
+
}
|
|
6108
|
+
encoded();
|
|
6109
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
6162
6110
|
}
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
const
|
|
6166
|
-
|
|
6111
|
+
const engineCfg = resolveEngineConfig(input);
|
|
6112
|
+
if (isGif) {
|
|
6113
|
+
const encoded = startEncodeProgress(job, "Encoding GIF", onProgress);
|
|
6114
|
+
if (hasAudio) {
|
|
6115
|
+
log.warn("[Render] GIF output does not support audio; audio tracks will be ignored.");
|
|
6116
|
+
}
|
|
6117
|
+
const frameExt2 = frameFileExtension2(input.captureImageFormat);
|
|
6118
|
+
const framePattern2 = `frame_%06d.${frameExt2}`;
|
|
6119
|
+
const loop = resolveGifLoop(job.config.gifLoop);
|
|
6120
|
+
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
6121
|
+
fps: job.config.fps,
|
|
6122
|
+
loop,
|
|
6123
|
+
palettePath: join12(dirname6(videoOnlyPath), "gif-palette.png"),
|
|
6124
|
+
preserveAlpha: needsAlpha,
|
|
6125
|
+
signal: abortSignal,
|
|
6126
|
+
timeout: engineCfg.ffmpegEncodeTimeout
|
|
6127
|
+
});
|
|
6128
|
+
assertNotAborted();
|
|
6129
|
+
if (!encodeResult2.success) {
|
|
6130
|
+
throw encoderFailureError("Encoding failed", encodeResult2);
|
|
6131
|
+
}
|
|
6132
|
+
encoded();
|
|
6133
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
6167
6134
|
}
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6135
|
+
const onFramesEncoded = startEncodeProgress(job, "Encoding video", onProgress);
|
|
6136
|
+
const baseScaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
|
|
6137
|
+
const scaledEncodeTimeout = baseScaledEncodeTimeout * (job.config.quality === "high" ? 6 : 1);
|
|
6138
|
+
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
6139
|
+
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
6140
|
+
const framePattern = `frame_%06d.${frameExt}`;
|
|
6141
|
+
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
6142
|
+
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
6143
|
+
framesDir,
|
|
6144
|
+
framePattern,
|
|
6145
|
+
videoOnlyPath,
|
|
6146
|
+
encoderOpts,
|
|
6147
|
+
chunkedEncodeSize,
|
|
6148
|
+
abortSignal,
|
|
6149
|
+
videoEngineCfg,
|
|
6150
|
+
onFramesEncoded
|
|
6151
|
+
) : await encodeFramesFromDir(
|
|
6152
|
+
framesDir,
|
|
6153
|
+
framePattern,
|
|
6154
|
+
videoOnlyPath,
|
|
6155
|
+
encoderOpts,
|
|
6156
|
+
abortSignal,
|
|
6157
|
+
videoEngineCfg,
|
|
6158
|
+
onFramesEncoded
|
|
6159
|
+
);
|
|
6160
|
+
assertNotAborted();
|
|
6161
|
+
if (!encodeResult.success) {
|
|
6162
|
+
throw encoderFailureError("Encoding failed", encodeResult);
|
|
6172
6163
|
}
|
|
6164
|
+
onFramesEncoded();
|
|
6165
|
+
return { encodeMs: Date.now() - stage5Start };
|
|
6173
6166
|
}
|
|
6174
6167
|
|
|
6168
|
+
// src/services/render/stages/assembleStage.ts
|
|
6169
|
+
import { applyFaststart, muxVideoWithAudio, packageHls } from "@hyperframes/engine";
|
|
6170
|
+
|
|
6175
6171
|
// src/services/renderOrchestrator.ts
|
|
6176
6172
|
function sampleDirectoryBytes(dir) {
|
|
6177
6173
|
let total = 0;
|
|
@@ -6186,7 +6182,7 @@ function sampleDirectoryBytes(dir) {
|
|
|
6186
6182
|
continue;
|
|
6187
6183
|
}
|
|
6188
6184
|
for (const name of entries) {
|
|
6189
|
-
const full =
|
|
6185
|
+
const full = join13(current, name);
|
|
6190
6186
|
try {
|
|
6191
6187
|
const st = statSync4(full);
|
|
6192
6188
|
if (st.isDirectory()) {
|
|
@@ -6271,7 +6267,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
6271
6267
|
const ranges = [];
|
|
6272
6268
|
let rangeStart = null;
|
|
6273
6269
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
6274
|
-
const framePath =
|
|
6270
|
+
const framePath = join13(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
6275
6271
|
const missing = !existsSync10(framePath) || statSync4(framePath).size <= 8;
|
|
6276
6272
|
if (missing && rangeStart === null) {
|
|
6277
6273
|
rangeStart = frameIndex;
|
|
@@ -6294,7 +6290,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
|
|
|
6294
6290
|
workerId,
|
|
6295
6291
|
startFrame: rangeStart + range.startFrame,
|
|
6296
6292
|
endFrame: rangeStart + range.endFrame,
|
|
6297
|
-
outputDir:
|
|
6293
|
+
outputDir: join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
|
|
6298
6294
|
outputFrameOffset: rangeStart
|
|
6299
6295
|
}));
|
|
6300
6296
|
batches.push(batch);
|
|
@@ -6320,7 +6316,7 @@ function isRecoverableParallelCaptureError(error) {
|
|
|
6320
6316
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
6321
6317
|
let captured = 0;
|
|
6322
6318
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
6323
|
-
const framePath =
|
|
6319
|
+
const framePath = join13(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
6324
6320
|
if (existsSync10(framePath)) captured++;
|
|
6325
6321
|
}
|
|
6326
6322
|
return captured;
|
|
@@ -6346,7 +6342,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
6346
6342
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
6347
6343
|
});
|
|
6348
6344
|
pendingTransientRetry = false;
|
|
6349
|
-
const attemptWorkDir =
|
|
6345
|
+
const attemptWorkDir = join13(options.workDir, `capture-attempt-${attempt}`);
|
|
6350
6346
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
6351
6347
|
missingRanges,
|
|
6352
6348
|
currentWorkers,
|
|
@@ -6525,7 +6521,7 @@ function explainStreamingEncodeGate(cfg, outputFormat, workerCount, durationSeco
|
|
|
6525
6521
|
var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
|
|
6526
6522
|
|
|
6527
6523
|
// src/services/distributed/planFrameCost.ts
|
|
6528
|
-
import { join as
|
|
6524
|
+
import { join as join16 } from "node:path";
|
|
6529
6525
|
import {
|
|
6530
6526
|
closeCaptureSession as closeCaptureSession9,
|
|
6531
6527
|
createCaptureSession as createCaptureSession9,
|
|
@@ -6535,7 +6531,7 @@ import {
|
|
|
6535
6531
|
|
|
6536
6532
|
// src/services/distributed/chunkFrames.ts
|
|
6537
6533
|
import { existsSync as existsSync12, readdirSync as readdirSync4 } from "node:fs";
|
|
6538
|
-
import { extname as extname2, join as
|
|
6534
|
+
import { extname as extname2, join as join15 } from "node:path";
|
|
6539
6535
|
import {
|
|
6540
6536
|
compositionRequiresWebGpu as compositionRequiresWebGpu3,
|
|
6541
6537
|
createFrameLookupTable as createFrameLookupTable2,
|
|
@@ -6545,7 +6541,7 @@ import {
|
|
|
6545
6541
|
|
|
6546
6542
|
// src/services/distributed/shared.ts
|
|
6547
6543
|
import { execFile as execFileCallback } from "node:child_process";
|
|
6548
|
-
import { dirname as dirname8, join as
|
|
6544
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
6549
6545
|
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
|
|
6550
6546
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6551
6547
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -6561,7 +6557,7 @@ function isPlanAudioArtifactPath(path) {
|
|
|
6561
6557
|
}
|
|
6562
6558
|
function resolvePlanAudioPath(planDir) {
|
|
6563
6559
|
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
6564
|
-
const candidate =
|
|
6560
|
+
const candidate = join14(planDir, name);
|
|
6565
6561
|
if (existsSync11(candidate)) return candidate;
|
|
6566
6562
|
}
|
|
6567
6563
|
return null;
|
|
@@ -6806,7 +6802,8 @@ function buildSyntheticRenderJob(input) {
|
|
|
6806
6802
|
hdrMode: input.hdrMode,
|
|
6807
6803
|
strictness: input.strictness,
|
|
6808
6804
|
producerConfig: input.producerConfig,
|
|
6809
|
-
variables: input.variables
|
|
6805
|
+
variables: input.variables,
|
|
6806
|
+
pageMotionBlur: "off"
|
|
6810
6807
|
};
|
|
6811
6808
|
return createRenderJob(renderConfig);
|
|
6812
6809
|
}
|
|
@@ -6816,7 +6813,7 @@ function readProducerVersion() {
|
|
|
6816
6813
|
const startDir = dirname8(fileURLToPath3(import.meta.url));
|
|
6817
6814
|
let current = startDir;
|
|
6818
6815
|
for (let i = 0; i < 10; i++) {
|
|
6819
|
-
const candidate =
|
|
6816
|
+
const candidate = join14(current, "package.json");
|
|
6820
6817
|
if (existsSync11(candidate)) {
|
|
6821
6818
|
try {
|
|
6822
6819
|
const pkg = JSON.parse(readFileSync7(candidate, "utf-8"));
|
|
@@ -6869,7 +6866,7 @@ function frameNumberFromFileName(name) {
|
|
|
6869
6866
|
function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
|
|
6870
6867
|
const result = [];
|
|
6871
6868
|
for (const v of videos) {
|
|
6872
|
-
const outputDir =
|
|
6869
|
+
const outputDir = join15(planDir, "video-frames", v.videoId);
|
|
6873
6870
|
if (!existsSync12(outputDir)) {
|
|
6874
6871
|
throw new Error(
|
|
6875
6872
|
`[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
|
|
@@ -6888,7 +6885,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
|
|
|
6888
6885
|
if (!frameName) continue;
|
|
6889
6886
|
const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
|
|
6890
6887
|
const frameIndex = frameNumber === null ? i : frameNumber - 1;
|
|
6891
|
-
framePaths.set(frameIndex,
|
|
6888
|
+
framePaths.set(frameIndex, join15(outputDir, frameName));
|
|
6892
6889
|
}
|
|
6893
6890
|
result.push({
|
|
6894
6891
|
videoId: v.videoId,
|
|
@@ -6930,6 +6927,7 @@ function buildChunkCaptureOptions(input) {
|
|
|
6930
6927
|
skipReadinessVideoIds: input.skipReadinessVideoIds,
|
|
6931
6928
|
videoMetadataHints: input.videoMetadataHints,
|
|
6932
6929
|
...videoCaptureBeyondViewport !== void 0 ? { captureBeyondViewport: videoCaptureBeyondViewport } : {},
|
|
6930
|
+
pageMotionBlur: encoder.pageMotionBlur ?? "distributed-render",
|
|
6933
6931
|
// A fixed warmup count keeps `beginFrameTimeTicks` host-independent.
|
|
6934
6932
|
lockWarmupTicks: true,
|
|
6935
6933
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
@@ -6977,14 +6975,14 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
6977
6975
|
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
6978
6976
|
return Promise.all(
|
|
6979
6977
|
deferred.map(async (video) => {
|
|
6980
|
-
const outputDir =
|
|
6978
|
+
const outputDir = join15(input.outputRoot, video.videoId);
|
|
6981
6979
|
const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
|
|
6982
6980
|
if (indexes.length === 0) {
|
|
6983
6981
|
return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
|
|
6984
6982
|
}
|
|
6985
6983
|
const range = video.deferredRange;
|
|
6986
6984
|
const remoteUrl = remoteSources[range.sourcePath];
|
|
6987
|
-
const sourcePath = remoteUrl ??
|
|
6985
|
+
const sourcePath = remoteUrl ?? join15(input.planDir, ...range.sourcePath.split("/"));
|
|
6988
6986
|
if (remoteUrl === void 0 && !existsSync12(sourcePath)) {
|
|
6989
6987
|
throw new RenderChunkValidationError(
|
|
6990
6988
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -7033,12 +7031,12 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7033
7031
|
planVideos,
|
|
7034
7032
|
frames: sampled,
|
|
7035
7033
|
fps: fpsRatio,
|
|
7036
|
-
outputRoot:
|
|
7034
|
+
outputRoot: join16(workDir, "frame-cost-video-frames"),
|
|
7037
7035
|
cfg: input.cfg
|
|
7038
7036
|
})
|
|
7039
7037
|
];
|
|
7040
7038
|
const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
|
|
7041
|
-
const compiledDir =
|
|
7039
|
+
const compiledDir = join16(planDir, "compiled");
|
|
7042
7040
|
const fileServer = await createFileServer({
|
|
7043
7041
|
projectDir: compiledDir,
|
|
7044
7042
|
compiledDir,
|
|
@@ -7050,7 +7048,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7050
7048
|
try {
|
|
7051
7049
|
session = await createCaptureSession9(
|
|
7052
7050
|
fileServer.url,
|
|
7053
|
-
|
|
7051
|
+
join16(workDir, "frame-cost"),
|
|
7054
7052
|
buildChunkCaptureOptions({
|
|
7055
7053
|
dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
|
|
7056
7054
|
encoder: input,
|
|
@@ -7072,7 +7070,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7072
7070
|
|
|
7073
7071
|
// src/services/render/stages/freezePlan.ts
|
|
7074
7072
|
import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7075
|
-
import { join as
|
|
7073
|
+
import { join as join17, relative as relative3, resolve as resolve7 } from "node:path";
|
|
7076
7074
|
|
|
7077
7075
|
// src/services/distributed/planProtocol.ts
|
|
7078
7076
|
var PLAN_SCHEMA_VERSION = 1;
|
|
@@ -7290,7 +7288,7 @@ function listPlanFiles(planDir) {
|
|
|
7290
7288
|
function walk(dir) {
|
|
7291
7289
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
7292
7290
|
for (const entry of entries) {
|
|
7293
|
-
const full =
|
|
7291
|
+
const full = join17(dir, entry.name);
|
|
7294
7292
|
if (entry.isDirectory()) {
|
|
7295
7293
|
walk(full);
|
|
7296
7294
|
} else if (entry.isFile()) {
|
|
@@ -7331,15 +7329,15 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
|
|
|
7331
7329
|
function writePlanRemoteFiles(planDir, files) {
|
|
7332
7330
|
if (files.length === 0) return;
|
|
7333
7331
|
const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
|
|
7334
|
-
writeFileSync4(
|
|
7332
|
+
writeFileSync4(join17(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
|
|
7335
7333
|
}
|
|
7336
7334
|
function readPlanRemoteFiles(planDir) {
|
|
7337
|
-
const path =
|
|
7335
|
+
const path = join17(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
|
|
7338
7336
|
return existsSync13(path) ? JSON.parse(readFileSync8(path, "utf-8")) : [];
|
|
7339
7337
|
}
|
|
7340
7338
|
function recomputePlanHashFromPlanDir(planDir, digests) {
|
|
7341
|
-
const planJsonPath =
|
|
7342
|
-
const encoderJsonPath =
|
|
7339
|
+
const planJsonPath = join17(planDir, "plan.json");
|
|
7340
|
+
const encoderJsonPath = join17(planDir, "meta", "encoder.json");
|
|
7343
7341
|
if (!existsSync13(planJsonPath)) {
|
|
7344
7342
|
throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
|
|
7345
7343
|
}
|
|
@@ -7376,18 +7374,18 @@ async function freezePlan(input) {
|
|
|
7376
7374
|
if (!existsSync13(planDir)) {
|
|
7377
7375
|
throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
|
|
7378
7376
|
}
|
|
7379
|
-
const metaDir =
|
|
7377
|
+
const metaDir = join17(planDir, "meta");
|
|
7380
7378
|
if (!existsSync13(metaDir)) mkdirSync7(metaDir, { recursive: true });
|
|
7381
7379
|
writeFileSync4(
|
|
7382
|
-
|
|
7380
|
+
join17(metaDir, "composition.json"),
|
|
7383
7381
|
`${JSON.stringify(composition, null, 2)}
|
|
7384
7382
|
`,
|
|
7385
7383
|
"utf-8"
|
|
7386
7384
|
);
|
|
7387
7385
|
const encoderForCanonical = stripUndefined(encoder);
|
|
7388
7386
|
const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
|
|
7389
|
-
writeFileSync4(
|
|
7390
|
-
writeFileSync4(
|
|
7387
|
+
writeFileSync4(join17(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
|
|
7388
|
+
writeFileSync4(join17(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
|
|
7391
7389
|
`, "utf-8");
|
|
7392
7390
|
const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
|
|
7393
7391
|
const planHash = computePlanHash({
|
|
@@ -7411,7 +7409,7 @@ async function freezePlan(input) {
|
|
|
7411
7409
|
duration: durationSeconds,
|
|
7412
7410
|
hasAudio
|
|
7413
7411
|
};
|
|
7414
|
-
const planJsonPath =
|
|
7412
|
+
const planJsonPath = join17(planDir, "plan.json");
|
|
7415
7413
|
writeFileSync4(planJsonPath, `${JSON.stringify(planJson, null, 2)}
|
|
7416
7414
|
`, "utf-8");
|
|
7417
7415
|
return { planJsonPath, planHash };
|
|
@@ -7455,7 +7453,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
|
|
|
7455
7453
|
// src/services/distributed/chunkAudio.ts
|
|
7456
7454
|
import { closeSync as closeSync4, existsSync as existsSync14, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync9 } from "node:fs";
|
|
7457
7455
|
import { writeFileSync as writeFileSync5, writeSync } from "node:fs";
|
|
7458
|
-
import { join as
|
|
7456
|
+
import { join as join19 } from "node:path";
|
|
7459
7457
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
7460
7458
|
|
|
7461
7459
|
// src/services/distributed/mp4Stitch.ts
|
|
@@ -7465,10 +7463,10 @@ import {
|
|
|
7465
7463
|
mkdirSync as mkdirSync8,
|
|
7466
7464
|
mkdtempSync as mkdtempSync3,
|
|
7467
7465
|
renameSync as renameSync3,
|
|
7468
|
-
rmSync as
|
|
7466
|
+
rmSync as rmSync8
|
|
7469
7467
|
} from "node:fs";
|
|
7470
7468
|
import { open } from "node:fs/promises";
|
|
7471
|
-
import { dirname as dirname9, join as
|
|
7469
|
+
import { dirname as dirname9, join as join18 } from "node:path";
|
|
7472
7470
|
import { finished, pipeline } from "node:stream/promises";
|
|
7473
7471
|
function boxAt(src, off, at, limit) {
|
|
7474
7472
|
let size = src.readUInt32BE(off);
|
|
@@ -7834,16 +7832,16 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
7834
7832
|
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
7835
7833
|
mkdirSync8(dirname9(outputPath), { recursive: true });
|
|
7836
7834
|
const workDir = mkdtempSync3(`${outputPath}.stitch-`);
|
|
7837
|
-
const out = createWriteStream(
|
|
7835
|
+
const out = createWriteStream(join18(workDir, "out.mp4"));
|
|
7838
7836
|
try {
|
|
7839
7837
|
out.write(header);
|
|
7840
7838
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
7841
7839
|
out.end();
|
|
7842
7840
|
await finished(out);
|
|
7843
|
-
renameSync3(
|
|
7841
|
+
renameSync3(join18(workDir, "out.mp4"), outputPath);
|
|
7844
7842
|
} finally {
|
|
7845
7843
|
out.destroy();
|
|
7846
|
-
|
|
7844
|
+
rmSync8(workDir, { recursive: true, force: true });
|
|
7847
7845
|
}
|
|
7848
7846
|
}
|
|
7849
7847
|
|
|
@@ -7934,7 +7932,7 @@ async function writeChunkAudioSlices(input) {
|
|
|
7934
7932
|
});
|
|
7935
7933
|
const bytes = readFileSync9(input.audioPath);
|
|
7936
7934
|
const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
|
|
7937
|
-
mkdirSync9(
|
|
7935
|
+
mkdirSync9(join19(input.planDir, SLICE_DIR), { recursive: true });
|
|
7938
7936
|
input.chunks.forEach((_, index) => {
|
|
7939
7937
|
const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
|
|
7940
7938
|
if (!cut) return;
|
|
@@ -7942,17 +7940,17 @@ async function writeChunkAudioSlices(input) {
|
|
|
7942
7940
|
const start = offsets[cut.firstPacket];
|
|
7943
7941
|
layout.partStarts[index] = start;
|
|
7944
7942
|
const end = offsets[last] + track.sizes[last];
|
|
7945
|
-
writeFileSync5(
|
|
7943
|
+
writeFileSync5(join19(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
|
|
7946
7944
|
});
|
|
7947
7945
|
const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
|
|
7948
|
-
writeFileSync5(
|
|
7949
|
-
writeFileSync5(
|
|
7946
|
+
writeFileSync5(join19(input.planDir, SLICE_FRAME), Buffer.concat(frame));
|
|
7947
|
+
writeFileSync5(join19(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
|
|
7950
7948
|
}
|
|
7951
7949
|
function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
7952
|
-
const layoutPath =
|
|
7950
|
+
const layoutPath = join19(planDir, SLICE_LAYOUT);
|
|
7953
7951
|
if (!existsSync14(layoutPath)) return resolvePlanAudioPath(planDir);
|
|
7954
7952
|
const layout = JSON.parse(readFileSync9(layoutPath, "utf-8"));
|
|
7955
|
-
const frame = readFileSync9(
|
|
7953
|
+
const frame = readFileSync9(join19(planDir, SLICE_FRAME));
|
|
7956
7954
|
const fd = openSync4(scratchPath, "w");
|
|
7957
7955
|
try {
|
|
7958
7956
|
writeSync(fd, frame, 0, layout.payloadStart, 0);
|
|
@@ -7965,7 +7963,7 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
|
|
|
7965
7963
|
);
|
|
7966
7964
|
const start = layout.partStarts[chunkIndex];
|
|
7967
7965
|
if (start !== void 0) {
|
|
7968
|
-
const part = readFileSync9(
|
|
7966
|
+
const part = readFileSync9(join19(planDir, chunkAudioSlicePath(chunkIndex)));
|
|
7969
7967
|
writeSync(fd, part, 0, part.length, start);
|
|
7970
7968
|
}
|
|
7971
7969
|
ftruncateSync(fd, layout.sizeBytes);
|
|
@@ -8021,7 +8019,7 @@ async function muxChunkWithAudio(input) {
|
|
|
8021
8019
|
// src/services/distributed/planSize.ts
|
|
8022
8020
|
import { createHash as createHash5 } from "node:crypto";
|
|
8023
8021
|
import { lstatSync, readdirSync as readdirSync6 } from "node:fs";
|
|
8024
|
-
import { extname as extname3, join as
|
|
8022
|
+
import { extname as extname3, join as join20, relative as relative4 } from "node:path";
|
|
8025
8023
|
var MAX_TOP_COMPONENTS = 10;
|
|
8026
8024
|
var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
8027
8025
|
".aac",
|
|
@@ -8133,7 +8131,7 @@ function walkRegularFiles(dir, visit) {
|
|
|
8133
8131
|
return;
|
|
8134
8132
|
}
|
|
8135
8133
|
for (const entry of entries) {
|
|
8136
|
-
const full =
|
|
8134
|
+
const full = join20(dir, entry.name);
|
|
8137
8135
|
if (entry.isDirectory()) {
|
|
8138
8136
|
walkRegularFiles(full, visit);
|
|
8139
8137
|
} else if (entry.isFile()) {
|
|
@@ -8209,7 +8207,6 @@ function chunkSizingUsesFrameCost(config) {
|
|
|
8209
8207
|
}
|
|
8210
8208
|
var CHUNK_EXCEEDS_WORKER_TIMEOUT = "CHUNK_EXCEEDS_WORKER_TIMEOUT";
|
|
8211
8209
|
var ChunkExceedsWorkerTimeoutError = class extends Error {
|
|
8212
|
-
// fallow-ignore-next-line unused-class-member
|
|
8213
8210
|
code = CHUNK_EXCEEDS_WORKER_TIMEOUT;
|
|
8214
8211
|
constructor(predictedChunkSeconds, workerTimeoutSeconds) {
|
|
8215
8212
|
super(
|
|
@@ -8272,7 +8269,6 @@ var PlanTooLargeError = class extends Error {
|
|
|
8272
8269
|
};
|
|
8273
8270
|
var FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED = "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED";
|
|
8274
8271
|
var FormatNotSupportedInDistributedError = class extends Error {
|
|
8275
|
-
// fallow-ignore-next-line unused-class-member
|
|
8276
8272
|
code = FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED;
|
|
8277
8273
|
format;
|
|
8278
8274
|
reason;
|
|
@@ -8347,13 +8343,13 @@ function assertPositiveInteger(name, value) {
|
|
|
8347
8343
|
}
|
|
8348
8344
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
8349
8345
|
"plan.json",
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8346
|
+
join21("meta", "composition.json"),
|
|
8347
|
+
join21("meta", "encoder.json"),
|
|
8348
|
+
join21("meta", "chunks.json")
|
|
8353
8349
|
];
|
|
8354
8350
|
function removeFreezeOwnedPlanFiles(planDir) {
|
|
8355
8351
|
for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
|
|
8356
|
-
|
|
8352
|
+
rmSync9(join21(planDir, relativePath), { force: true });
|
|
8357
8353
|
}
|
|
8358
8354
|
}
|
|
8359
8355
|
function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
|
|
@@ -8412,6 +8408,7 @@ function buildLockedRenderConfig(input) {
|
|
|
8412
8408
|
chunkCount: input.chunkCount,
|
|
8413
8409
|
runtimeEnv: input.runtimeEnv,
|
|
8414
8410
|
variables: config.variables,
|
|
8411
|
+
pageMotionBlur: config.pageMotionBlur,
|
|
8415
8412
|
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
8416
8413
|
...input.separateAudio ? { separateAudio: true } : {}
|
|
8417
8414
|
};
|
|
@@ -8485,19 +8482,19 @@ function createVideoSourceShipper(input) {
|
|
|
8485
8482
|
const known = shipped.get(srcPath);
|
|
8486
8483
|
if (known) return known;
|
|
8487
8484
|
const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
|
|
8488
|
-
const current = stagedRel === null ? srcPath :
|
|
8485
|
+
const current = stagedRel === null ? srcPath : join21(input.planDir, "video-frames", stagedRel);
|
|
8489
8486
|
let result = null;
|
|
8490
8487
|
for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
|
|
8491
8488
|
const rel = pathInside(root, srcPath);
|
|
8492
|
-
if (rel !== null && (existsSync15(
|
|
8489
|
+
if (rel !== null && (existsSync15(join21(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
|
|
8493
8490
|
result = ["compiled", ...rel.split(sep2)].join("/");
|
|
8494
8491
|
break;
|
|
8495
8492
|
}
|
|
8496
8493
|
}
|
|
8497
8494
|
if (result === null) {
|
|
8498
8495
|
result = `video-sources/${shipped.size}${extname4(current)}`;
|
|
8499
|
-
const destination =
|
|
8500
|
-
mkdirSync10(
|
|
8496
|
+
const destination = join21(input.planDir, ...result.split("/"));
|
|
8497
|
+
mkdirSync10(join21(input.planDir, "video-sources"), { recursive: true });
|
|
8501
8498
|
try {
|
|
8502
8499
|
linkSync(current, destination);
|
|
8503
8500
|
} catch {
|
|
@@ -8509,7 +8506,7 @@ function createVideoSourceShipper(input) {
|
|
|
8509
8506
|
};
|
|
8510
8507
|
}
|
|
8511
8508
|
function plansSeparateAudio(config, options) {
|
|
8512
|
-
return options.deferAudio === true && config.format
|
|
8509
|
+
return options.deferAudio === true && config.format !== "png-sequence" && config.cfr !== true;
|
|
8513
8510
|
}
|
|
8514
8511
|
async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
8515
8512
|
rejectUnsupportedDistributedFormat(config);
|
|
@@ -8546,14 +8543,14 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8546
8543
|
variables: config.variables
|
|
8547
8544
|
});
|
|
8548
8545
|
const entryFile = config.entryFile ?? "index.html";
|
|
8549
|
-
const htmlPath =
|
|
8546
|
+
const htmlPath = join21(projectDir, entryFile);
|
|
8550
8547
|
if (!existsSync15(htmlPath)) {
|
|
8551
8548
|
throw new Error(`[plan] entry file not found: ${htmlPath}`);
|
|
8552
8549
|
}
|
|
8553
|
-
const workDir =
|
|
8554
|
-
|
|
8550
|
+
const workDir = join21(planDir, ".plan-work");
|
|
8551
|
+
rmSync9(workDir, { recursive: true, force: true });
|
|
8555
8552
|
mkdirSync10(workDir, { recursive: true });
|
|
8556
|
-
const compiledDir =
|
|
8553
|
+
const compiledDir = join21(workDir, "compiled");
|
|
8557
8554
|
mkdirSync10(compiledDir, { recursive: true });
|
|
8558
8555
|
let stageStarted = Date.now();
|
|
8559
8556
|
cpSync2(projectDir, compiledDir, {
|
|
@@ -8567,10 +8564,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8567
8564
|
}
|
|
8568
8565
|
});
|
|
8569
8566
|
logPlanStage(log, "copy", stageStarted);
|
|
8570
|
-
const finalCompiledDir =
|
|
8567
|
+
const finalCompiledDir = join21(planDir, "compiled");
|
|
8571
8568
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
8572
8569
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
8573
|
-
const remoteLocalPath = (rel) =>
|
|
8570
|
+
const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
|
|
8574
8571
|
const urlByAbsentLocalPath = new Map(
|
|
8575
8572
|
remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
|
|
8576
8573
|
);
|
|
@@ -8596,7 +8593,8 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8596
8593
|
// Distributed renders must not capture host-specific system fonts —
|
|
8597
8594
|
// the Lambda/worker filesystem won't have the same fonts installed.
|
|
8598
8595
|
allowSystemFontCapture: false,
|
|
8599
|
-
variables: config.variables
|
|
8596
|
+
variables: config.variables,
|
|
8597
|
+
pageMotionBlur: job.config.pageMotionBlur
|
|
8600
8598
|
});
|
|
8601
8599
|
logPlanStage(log, "compile", stageStarted);
|
|
8602
8600
|
let compiled = compileResult.compiled;
|
|
@@ -8668,22 +8666,21 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8668
8666
|
forceScreenshot,
|
|
8669
8667
|
width,
|
|
8670
8668
|
height,
|
|
8671
|
-
probeResult,
|
|
8672
8669
|
totalFrames
|
|
8673
8670
|
};
|
|
8674
8671
|
}
|
|
8675
|
-
function mixPlanAudio(c, compiledDir
|
|
8672
|
+
function mixPlanAudio(c, compiledDir) {
|
|
8676
8673
|
return runAudioStage({
|
|
8677
8674
|
projectDir: c.projectDir,
|
|
8678
8675
|
workDir: c.workDir,
|
|
8679
8676
|
compiledDir,
|
|
8680
8677
|
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
8681
|
-
|
|
8678
|
+
totalFrames: c.totalFrames,
|
|
8679
|
+
fps: c.job.config.fps,
|
|
8682
8680
|
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
8683
8681
|
audioGain: c.cfg.audioGain,
|
|
8684
8682
|
audios: c.composition.audios,
|
|
8685
8683
|
masterFxChain: c.composition.masterFxChain,
|
|
8686
|
-
...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
|
|
8687
8684
|
abortSignal: c.abortSignal,
|
|
8688
8685
|
assertNotAborted: c.assertNotAborted
|
|
8689
8686
|
});
|
|
@@ -8707,16 +8704,6 @@ async function extractPlanVideos(c, deferRangeExtraction) {
|
|
|
8707
8704
|
}
|
|
8708
8705
|
return extractResult;
|
|
8709
8706
|
}
|
|
8710
|
-
async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
|
|
8711
|
-
const peak = await constrainAacTruePeak({
|
|
8712
|
-
audioPath,
|
|
8713
|
-
...existsSync15(pcmPath) ? { sourcePath: pcmPath } : {},
|
|
8714
|
-
targetDurationSeconds: seconds,
|
|
8715
|
-
signal: c.abortSignal,
|
|
8716
|
-
timeoutMs: c.cfg.ffmpegProcessTimeout
|
|
8717
|
-
}).finally(() => rmSync8(pcmPath, { force: true }));
|
|
8718
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
8719
|
-
}
|
|
8720
8707
|
async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
|
|
8721
8708
|
const planDir = executionPlanDir;
|
|
8722
8709
|
const c = await compilePlanComposition(projectDir, config, planDir, options);
|
|
@@ -8738,7 +8725,6 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8738
8725
|
forceScreenshot,
|
|
8739
8726
|
width,
|
|
8740
8727
|
height,
|
|
8741
|
-
probeResult,
|
|
8742
8728
|
totalFrames
|
|
8743
8729
|
} = c;
|
|
8744
8730
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
@@ -8754,17 +8740,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8754
8740
|
let stageStarted = Date.now();
|
|
8755
8741
|
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
8756
8742
|
logPlanStage(log, "extract", stageStarted);
|
|
8757
|
-
const stagedVideoFrames =
|
|
8758
|
-
const videoFramesDst =
|
|
8759
|
-
if (existsSync15(videoFramesDst))
|
|
8760
|
-
const videoSourcesDir =
|
|
8761
|
-
|
|
8743
|
+
const stagedVideoFrames = join21(compiledDir, "__hyperframes_video_frames");
|
|
8744
|
+
const videoFramesDst = join21(planDir, "video-frames");
|
|
8745
|
+
if (existsSync15(videoFramesDst)) rmSync9(videoFramesDst, { recursive: true, force: true });
|
|
8746
|
+
const videoSourcesDir = join21(planDir, "video-sources");
|
|
8747
|
+
rmSync9(videoSourcesDir, { recursive: true, force: true });
|
|
8762
8748
|
if (existsSync15(stagedVideoFrames)) {
|
|
8763
8749
|
renameSync4(stagedVideoFrames, videoFramesDst);
|
|
8764
8750
|
} else {
|
|
8765
8751
|
mkdirSync10(videoFramesDst, { recursive: true });
|
|
8766
8752
|
}
|
|
8767
|
-
if (existsSync15(finalCompiledDir))
|
|
8753
|
+
if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
|
|
8768
8754
|
renameSync4(compiledDir, finalCompiledDir);
|
|
8769
8755
|
const shipVideoSource = createVideoSourceShipper({
|
|
8770
8756
|
planDir,
|
|
@@ -8794,7 +8780,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8794
8780
|
};
|
|
8795
8781
|
})
|
|
8796
8782
|
});
|
|
8797
|
-
mkdirSync10(
|
|
8783
|
+
mkdirSync10(join21(planDir, "meta"), { recursive: true });
|
|
8798
8784
|
writePlanRemoteFiles(
|
|
8799
8785
|
planDir,
|
|
8800
8786
|
remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
|
|
@@ -8804,23 +8790,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8804
8790
|
}))
|
|
8805
8791
|
);
|
|
8806
8792
|
writeFileSync6(
|
|
8807
|
-
|
|
8793
|
+
join21(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
|
|
8808
8794
|
JSON.stringify(planVideosJson, null, 2),
|
|
8809
8795
|
"utf-8"
|
|
8810
8796
|
);
|
|
8811
8797
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
8812
8798
|
const separateAudio = plansSeparateAudio(config, options);
|
|
8813
8799
|
const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
|
|
8814
|
-
const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
|
|
8815
|
-
const planMixPcmPath = join22(workDir, "plan-mix.wav");
|
|
8816
8800
|
stageStarted = Date.now();
|
|
8817
8801
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
8818
|
-
separateAudio ? null : mixPlanAudio(
|
|
8819
|
-
c,
|
|
8820
|
-
finalCompiledDir,
|
|
8821
|
-
audioSeconds,
|
|
8822
|
-
chunksCarryAudio ? planMixPcmPath : void 0
|
|
8823
|
-
).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8802
|
+
separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8824
8803
|
(async () => {
|
|
8825
8804
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
8826
8805
|
await prehashPlanFiles(dir, digests);
|
|
@@ -8873,14 +8852,11 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8873
8852
|
if (audioResult?.audioError) {
|
|
8874
8853
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
8875
8854
|
}
|
|
8876
|
-
const planAudioPath =
|
|
8855
|
+
const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
8877
8856
|
if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
|
|
8878
8857
|
renameSync4(audioResult.audioOutputPath, planAudioPath);
|
|
8879
8858
|
}
|
|
8880
8859
|
if (chunksCarryAudio && existsSync15(planAudioPath)) {
|
|
8881
|
-
stageStarted = Date.now();
|
|
8882
|
-
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
8883
|
-
logPlanStage(log, "audio-peak", stageStarted);
|
|
8884
8860
|
if (options.sliceChunkAudio) {
|
|
8885
8861
|
stageStarted = Date.now();
|
|
8886
8862
|
await writeChunkAudioSlices({
|
|
@@ -8925,7 +8901,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8925
8901
|
format: config.format
|
|
8926
8902
|
};
|
|
8927
8903
|
try {
|
|
8928
|
-
|
|
8904
|
+
rmSync9(workDir, { recursive: true, force: true });
|
|
8929
8905
|
} catch (err) {
|
|
8930
8906
|
log.warn("[plan] failed to remove temp work dir", {
|
|
8931
8907
|
workDir,
|
|
@@ -8982,25 +8958,20 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
|
|
|
8982
8958
|
executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER
|
|
8983
8959
|
});
|
|
8984
8960
|
try {
|
|
8985
|
-
const seconds = c.totalFrames / config.fps;
|
|
8986
|
-
const pcmPath = join22(c.workDir, "plan-mix.wav");
|
|
8987
8961
|
let stageStarted = Date.now();
|
|
8988
8962
|
await extractPlanVideos(c, true);
|
|
8989
8963
|
logPlanStage(c.log, "extract", stageStarted);
|
|
8990
8964
|
stageStarted = Date.now();
|
|
8991
|
-
const mixed = await mixPlanAudio(c, c.compiledDir
|
|
8965
|
+
const mixed = await mixPlanAudio(c, c.compiledDir);
|
|
8992
8966
|
logPlanStage(c.log, "audio", stageStarted);
|
|
8993
8967
|
if (mixed.audioError) {
|
|
8994
8968
|
applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
|
|
8995
8969
|
}
|
|
8996
8970
|
if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
|
|
8997
8971
|
renameSync4(mixed.audioOutputPath, outputPath);
|
|
8998
|
-
stageStarted = Date.now();
|
|
8999
|
-
await holdPlanAudioPeak(c, outputPath, pcmPath, seconds);
|
|
9000
|
-
logPlanStage(c.log, "audio-peak", stageStarted);
|
|
9001
8972
|
return { hasAudio: true };
|
|
9002
8973
|
} finally {
|
|
9003
|
-
|
|
8974
|
+
rmSync9(c.workDir, { recursive: true, force: true });
|
|
9004
8975
|
}
|
|
9005
8976
|
}
|
|
9006
8977
|
async function plan(projectDir, config, planDir, options = {}) {
|
|
@@ -9030,12 +9001,12 @@ import {
|
|
|
9030
9001
|
readFileSync as readFileSync10,
|
|
9031
9002
|
readdirSync as readdirSync7,
|
|
9032
9003
|
renameSync as renameSync6,
|
|
9033
|
-
rmSync as
|
|
9004
|
+
rmSync as rmSync11,
|
|
9034
9005
|
statSync as statSync6,
|
|
9035
9006
|
writeFileSync as writeFileSync8
|
|
9036
9007
|
} from "node:fs";
|
|
9037
9008
|
import { tmpdir } from "node:os";
|
|
9038
|
-
import { dirname as dirname11, isAbsolute as isAbsolute4, join as
|
|
9009
|
+
import { dirname as dirname11, isAbsolute as isAbsolute4, join as join24, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
|
|
9039
9010
|
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
9040
9011
|
import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
|
|
9041
9012
|
|
|
@@ -9047,11 +9018,11 @@ import {
|
|
|
9047
9018
|
mkdirSync as mkdirSync11,
|
|
9048
9019
|
mkdtempSync as mkdtempSync4,
|
|
9049
9020
|
renameSync as renameSync5,
|
|
9050
|
-
rmSync as
|
|
9021
|
+
rmSync as rmSync10,
|
|
9051
9022
|
statSync as statSync5,
|
|
9052
9023
|
writeFileSync as writeFileSync7
|
|
9053
9024
|
} from "node:fs";
|
|
9054
|
-
import { dirname as dirname10, join as
|
|
9025
|
+
import { dirname as dirname10, join as join23 } from "node:path";
|
|
9055
9026
|
|
|
9056
9027
|
// src/services/distributed/planV2Errors.ts
|
|
9057
9028
|
var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
|
|
@@ -9066,7 +9037,7 @@ var PlanV2IntegrityError = class extends Error {
|
|
|
9066
9037
|
};
|
|
9067
9038
|
|
|
9068
9039
|
// src/services/distributed/planV2Layout.ts
|
|
9069
|
-
import { join as
|
|
9040
|
+
import { join as join22 } from "node:path";
|
|
9070
9041
|
var SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
9071
9042
|
function assertPlanV2Sha256(value, field) {
|
|
9072
9043
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
@@ -9076,7 +9047,7 @@ function assertPlanV2Sha256(value, field) {
|
|
|
9076
9047
|
}
|
|
9077
9048
|
function planV2BlobPath(root, sha256) {
|
|
9078
9049
|
const digest = assertPlanV2Sha256(sha256, "artifact sha256");
|
|
9079
|
-
return
|
|
9050
|
+
return join22(root, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
9080
9051
|
}
|
|
9081
9052
|
|
|
9082
9053
|
// src/services/distributed/planV2Publisher.ts
|
|
@@ -9117,7 +9088,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9117
9088
|
this.destinationDir = destinationDir;
|
|
9118
9089
|
this.#linkFile = options.linkFile ?? linkSync2;
|
|
9119
9090
|
mkdirSync11(dirname10(destinationDir), { recursive: true });
|
|
9120
|
-
this.temporaryDir = mkdtempSync4(
|
|
9091
|
+
this.temporaryDir = mkdtempSync4(join23(dirname10(destinationDir), ".plan-v2-publish-"));
|
|
9121
9092
|
}
|
|
9122
9093
|
async putBlob(blob) {
|
|
9123
9094
|
const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
|
|
@@ -9130,8 +9101,8 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9130
9101
|
const destinationPath = planV2BlobPath(this.temporaryDir, digest);
|
|
9131
9102
|
if (existsSync16(destinationPath)) return;
|
|
9132
9103
|
mkdirSync11(dirname10(destinationPath), { recursive: true });
|
|
9133
|
-
const stagingDir = mkdtempSync4(
|
|
9134
|
-
const temporaryPath =
|
|
9104
|
+
const stagingDir = mkdtempSync4(join23(dirname10(destinationPath), ".plan-v2-blob-"));
|
|
9105
|
+
const temporaryPath = join23(stagingDir, "blob");
|
|
9135
9106
|
try {
|
|
9136
9107
|
try {
|
|
9137
9108
|
this.#linkFile(blob.sourcePath, temporaryPath);
|
|
@@ -9141,7 +9112,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9141
9112
|
}
|
|
9142
9113
|
renameSync5(temporaryPath, destinationPath);
|
|
9143
9114
|
} finally {
|
|
9144
|
-
|
|
9115
|
+
rmSync10(stagingDir, { recursive: true, force: true });
|
|
9145
9116
|
}
|
|
9146
9117
|
}
|
|
9147
9118
|
async commitManifest(manifestBytes) {
|
|
@@ -9152,13 +9123,13 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9152
9123
|
);
|
|
9153
9124
|
}
|
|
9154
9125
|
}
|
|
9155
|
-
writeFileSync7(
|
|
9126
|
+
writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
9156
9127
|
renameSync5(this.temporaryDir, this.destinationDir);
|
|
9157
9128
|
this.#committed = true;
|
|
9158
9129
|
}
|
|
9159
9130
|
async abort() {
|
|
9160
9131
|
if (!this.#committed) {
|
|
9161
|
-
|
|
9132
|
+
rmSync10(this.temporaryDir, { recursive: true, force: true });
|
|
9162
9133
|
}
|
|
9163
9134
|
}
|
|
9164
9135
|
};
|
|
@@ -9197,7 +9168,7 @@ function listFiles(root) {
|
|
|
9197
9168
|
const rootResolved = resolve8(root);
|
|
9198
9169
|
function walk(dir) {
|
|
9199
9170
|
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
9200
|
-
const absolutePath =
|
|
9171
|
+
const absolutePath = join24(dir, entry.name);
|
|
9201
9172
|
if (entry.isDirectory()) {
|
|
9202
9173
|
walk(absolutePath);
|
|
9203
9174
|
} else if (entry.isFile()) {
|
|
@@ -9224,13 +9195,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
|
|
|
9224
9195
|
}
|
|
9225
9196
|
}
|
|
9226
9197
|
function validateExtractionCacheCompleteSentinels(executionPlanDir) {
|
|
9227
|
-
const videoRoot =
|
|
9198
|
+
const videoRoot = join24(executionPlanDir, "video-frames");
|
|
9228
9199
|
if (!existsSync17(videoRoot)) return;
|
|
9229
9200
|
for (const videoEntry of readdirSync7(videoRoot, { withFileTypes: true })) {
|
|
9230
9201
|
if (!videoEntry.isDirectory()) continue;
|
|
9231
|
-
const videoDir =
|
|
9202
|
+
const videoDir = join24(videoRoot, videoEntry.name);
|
|
9232
9203
|
if (readdirSync7(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
|
|
9233
|
-
const sentinelPath =
|
|
9204
|
+
const sentinelPath = join24(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
|
|
9234
9205
|
assertValidExtractionCacheCompleteSentinel(sentinelPath);
|
|
9235
9206
|
}
|
|
9236
9207
|
}
|
|
@@ -9277,7 +9248,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9277
9248
|
if (video.deferredRange) {
|
|
9278
9249
|
const framePaths2 = /* @__PURE__ */ new Map();
|
|
9279
9250
|
for (let index = 0; index < video.totalFrames; index++) {
|
|
9280
|
-
framePaths2.set(index,
|
|
9251
|
+
framePaths2.set(index, join24(outputDir, String(index)));
|
|
9281
9252
|
}
|
|
9282
9253
|
return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
|
|
9283
9254
|
}
|
|
@@ -9285,7 +9256,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9285
9256
|
const framePaths = /* @__PURE__ */ new Map();
|
|
9286
9257
|
for (const frameName of frameNames) {
|
|
9287
9258
|
if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
|
|
9288
|
-
assertValidExtractionCacheCompleteSentinel(
|
|
9259
|
+
assertValidExtractionCacheCompleteSentinel(join24(outputDir, frameName));
|
|
9289
9260
|
continue;
|
|
9290
9261
|
}
|
|
9291
9262
|
const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
|
|
@@ -9304,7 +9275,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
|
|
|
9304
9275
|
`duplicate extracted frame index ${frameIndex} in ${outputDir}`
|
|
9305
9276
|
);
|
|
9306
9277
|
}
|
|
9307
|
-
framePaths.set(frameIndex,
|
|
9278
|
+
framePaths.set(frameIndex, join24(outputDir, frameName));
|
|
9308
9279
|
}
|
|
9309
9280
|
return {
|
|
9310
9281
|
...video,
|
|
@@ -9325,7 +9296,7 @@ function parsePlanVideosJson2(value) {
|
|
|
9325
9296
|
}
|
|
9326
9297
|
}
|
|
9327
9298
|
function materializeExtractedVideoDirectories(planDir) {
|
|
9328
|
-
const videosPath =
|
|
9299
|
+
const videosPath = join24(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9329
9300
|
if (!existsSync17(videosPath)) return;
|
|
9330
9301
|
const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
|
|
9331
9302
|
for (const video of videos.extracted) {
|
|
@@ -9354,13 +9325,13 @@ function parseChunkSlices(value) {
|
|
|
9354
9325
|
});
|
|
9355
9326
|
}
|
|
9356
9327
|
function buildVideoChunkDependencies(executionPlanDir, dimensions) {
|
|
9357
|
-
const videoRoot =
|
|
9328
|
+
const videoRoot = join24(executionPlanDir, "video-frames");
|
|
9358
9329
|
const hasExtractedFrames = existsSync17(videoRoot) && listFiles(videoRoot).length > 0;
|
|
9359
|
-
const videosPath =
|
|
9330
|
+
const videosPath = join24(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9360
9331
|
if (!existsSync17(videosPath)) {
|
|
9361
9332
|
return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
|
|
9362
9333
|
}
|
|
9363
|
-
const chunksPath =
|
|
9334
|
+
const chunksPath = join24(executionPlanDir, "meta", "chunks.json");
|
|
9364
9335
|
const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
9365
9336
|
const chunks = readJsonFile(chunksPath, "meta/chunks.json");
|
|
9366
9337
|
const parsedVideos = parsePlanVideosJson2(videos);
|
|
@@ -9416,13 +9387,13 @@ function computeManifestHash(manifest) {
|
|
|
9416
9387
|
function writeBlob(sourcePath, destinationPath) {
|
|
9417
9388
|
if (existsSync17(destinationPath)) return;
|
|
9418
9389
|
mkdirSync12(dirname11(destinationPath), { recursive: true });
|
|
9419
|
-
const temporaryDir = mkdtempSync5(
|
|
9420
|
-
const temporaryPath =
|
|
9390
|
+
const temporaryDir = mkdtempSync5(join24(dirname11(destinationPath), ".plan-v2-blob-"));
|
|
9391
|
+
const temporaryPath = join24(temporaryDir, "blob");
|
|
9421
9392
|
try {
|
|
9422
9393
|
copyFileSync7(sourcePath, temporaryPath);
|
|
9423
9394
|
renameSync6(temporaryPath, destinationPath);
|
|
9424
9395
|
} finally {
|
|
9425
|
-
|
|
9396
|
+
rmSync11(temporaryDir, { recursive: true, force: true });
|
|
9426
9397
|
}
|
|
9427
9398
|
}
|
|
9428
9399
|
function publishBlobFor(absolutePath, digests) {
|
|
@@ -9453,7 +9424,7 @@ async function putTasks(tasks) {
|
|
|
9453
9424
|
if (failure) throw failure.reason;
|
|
9454
9425
|
}
|
|
9455
9426
|
function buildPlanV2Publication(executionPlanDir, digests) {
|
|
9456
|
-
const executionPlanPath =
|
|
9427
|
+
const executionPlanPath = join24(executionPlanDir, "plan.json");
|
|
9457
9428
|
if (!existsSync17(executionPlanPath)) {
|
|
9458
9429
|
throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
|
|
9459
9430
|
}
|
|
@@ -9481,7 +9452,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
9481
9452
|
}
|
|
9482
9453
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
9483
9454
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
9484
|
-
const encoder = readJsonFile(
|
|
9455
|
+
const encoder = readJsonFile(join24(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
9485
9456
|
const files = listFiles(executionPlanDir);
|
|
9486
9457
|
const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
|
|
9487
9458
|
for (const file of files) {
|
|
@@ -9542,20 +9513,20 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
9542
9513
|
throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
|
|
9543
9514
|
}
|
|
9544
9515
|
mkdirSync12(dirname11(planV2Dir), { recursive: true });
|
|
9545
|
-
const tempDir = mkdtempSync5(
|
|
9516
|
+
const tempDir = mkdtempSync5(join24(dirname11(planV2Dir), ".plan-v2-build-"));
|
|
9546
9517
|
try {
|
|
9547
9518
|
for (const blob of publication.blobs) {
|
|
9548
9519
|
writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
|
|
9549
9520
|
}
|
|
9550
9521
|
writeFileSync8(
|
|
9551
|
-
|
|
9522
|
+
join24(tempDir, "plan.json"),
|
|
9552
9523
|
canonicalJsonStringify(publication.manifest),
|
|
9553
9524
|
"utf-8"
|
|
9554
9525
|
);
|
|
9555
9526
|
renameSync6(tempDir, planV2Dir);
|
|
9556
9527
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
9557
9528
|
} catch (error) {
|
|
9558
|
-
|
|
9529
|
+
rmSync11(tempDir, { recursive: true, force: true });
|
|
9559
9530
|
throw error;
|
|
9560
9531
|
}
|
|
9561
9532
|
}
|
|
@@ -9595,7 +9566,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
|
|
|
9595
9566
|
return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
|
|
9596
9567
|
}
|
|
9597
9568
|
async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
|
|
9598
|
-
const stagingRoot = mkdtempSync5(
|
|
9569
|
+
const stagingRoot = mkdtempSync5(join24(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
|
|
9599
9570
|
const digests = /* @__PURE__ */ new Map();
|
|
9600
9571
|
try {
|
|
9601
9572
|
await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
|
|
@@ -9606,7 +9577,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
9606
9577
|
// artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
|
|
9607
9578
|
whileAudioMixes: () => putBlobs(
|
|
9608
9579
|
publisher,
|
|
9609
|
-
listFiles(
|
|
9580
|
+
listFiles(join24(stagingRoot, "compiled")).map(
|
|
9610
9581
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
9611
9582
|
)
|
|
9612
9583
|
),
|
|
@@ -9622,7 +9593,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
9622
9593
|
}
|
|
9623
9594
|
throw error;
|
|
9624
9595
|
} finally {
|
|
9625
|
-
|
|
9596
|
+
rmSync11(stagingRoot, { recursive: true, force: true });
|
|
9626
9597
|
}
|
|
9627
9598
|
}
|
|
9628
9599
|
async function planV2(projectDir, config, planV2Dir) {
|
|
@@ -9638,7 +9609,7 @@ async function planV2(projectDir, config, planV2Dir) {
|
|
|
9638
9609
|
function resultFromManifest(planV2Dir, manifest) {
|
|
9639
9610
|
return {
|
|
9640
9611
|
planDir: planV2Dir,
|
|
9641
|
-
manifestPath:
|
|
9612
|
+
manifestPath: join24(planV2Dir, "plan.json"),
|
|
9642
9613
|
planProtocol: PLAN_PROTOCOL_V2,
|
|
9643
9614
|
planHash: manifest.planHash,
|
|
9644
9615
|
sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
|
|
@@ -9774,7 +9745,7 @@ function parsePlanV2Manifest(value) {
|
|
|
9774
9745
|
return parsed;
|
|
9775
9746
|
}
|
|
9776
9747
|
function readPlanV2Manifest(planV2Dir) {
|
|
9777
|
-
const manifestPath =
|
|
9748
|
+
const manifestPath = join24(planV2Dir, "plan.json");
|
|
9778
9749
|
if (!existsSync17(manifestPath)) {
|
|
9779
9750
|
throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
|
|
9780
9751
|
}
|
|
@@ -9820,16 +9791,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9820
9791
|
sourcePath: verifyBlob(planV2Dir, artifact)
|
|
9821
9792
|
}));
|
|
9822
9793
|
mkdirSync12(dirname11(destinationDir), { recursive: true });
|
|
9823
|
-
const tempDir = mkdtempSync5(
|
|
9794
|
+
const tempDir = mkdtempSync5(join24(dirname11(destinationDir), ".plan-v2-materialize-"));
|
|
9824
9795
|
try {
|
|
9825
9796
|
for (const { artifact, sourcePath } of verified) {
|
|
9826
|
-
const destinationPath =
|
|
9797
|
+
const destinationPath = join24(tempDir, ...artifact.path.split("/"));
|
|
9827
9798
|
mkdirSync12(dirname11(destinationPath), { recursive: true });
|
|
9828
9799
|
copyFileSync7(sourcePath, destinationPath);
|
|
9829
9800
|
}
|
|
9830
9801
|
if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
|
|
9831
9802
|
writeFileSync8(
|
|
9832
|
-
|
|
9803
|
+
join24(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
|
|
9833
9804
|
canonicalJsonStringify({
|
|
9834
9805
|
manifest,
|
|
9835
9806
|
target,
|
|
@@ -9839,7 +9810,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9839
9810
|
);
|
|
9840
9811
|
renameSync6(tempDir, destinationDir);
|
|
9841
9812
|
} catch (error) {
|
|
9842
|
-
|
|
9813
|
+
rmSync11(tempDir, { recursive: true, force: true });
|
|
9843
9814
|
throw error;
|
|
9844
9815
|
}
|
|
9845
9816
|
return {
|
|
@@ -9852,7 +9823,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9852
9823
|
// Join the artifact's OWN name, not the current constant: a plan written
|
|
9853
9824
|
// before the container change carries the legacy name, and materializing it
|
|
9854
9825
|
// under the new one would point at a file that was never written.
|
|
9855
|
-
audioPath: target.role === "assembler" ? audioArtifact ?
|
|
9826
|
+
audioPath: target.role === "assembler" ? audioArtifact ? join24(destinationDir, audioArtifact.path) : null : null
|
|
9856
9827
|
};
|
|
9857
9828
|
}
|
|
9858
9829
|
function assertRemoteSources(remoteSources, artifacts) {
|
|
@@ -9871,7 +9842,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
|
|
|
9871
9842
|
return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
|
|
9872
9843
|
}
|
|
9873
9844
|
function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
9874
|
-
const markerPath =
|
|
9845
|
+
const markerPath = join24(planDir, PLAN_V2_MATERIALIZATION_MARKER);
|
|
9875
9846
|
if (!existsSync17(markerPath)) return null;
|
|
9876
9847
|
const marker = readJsonFile(markerPath, "materialization marker");
|
|
9877
9848
|
if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
|
|
@@ -9889,7 +9860,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
9889
9860
|
assertRemoteSources(remoteSources, required);
|
|
9890
9861
|
for (const artifact of required) {
|
|
9891
9862
|
if (Object.hasOwn(remoteSources, artifact.path)) continue;
|
|
9892
|
-
const path =
|
|
9863
|
+
const path = join24(planDir, ...artifact.path.split("/"));
|
|
9893
9864
|
if (!existsSync17(path) || statSync6(path).size !== artifact.sizeBytes) {
|
|
9894
9865
|
throw new PlanV2IntegrityError(
|
|
9895
9866
|
`materialized artifact missing or truncated: ${artifact.path}`
|
|
@@ -9903,9 +9874,9 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
|
|
|
9903
9874
|
}
|
|
9904
9875
|
|
|
9905
9876
|
// src/services/distributed/planV2Execution.ts
|
|
9906
|
-
import { mkdtempSync as mkdtempSync7, rmSync as
|
|
9877
|
+
import { mkdtempSync as mkdtempSync7, rmSync as rmSync14 } from "node:fs";
|
|
9907
9878
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9908
|
-
import { join as
|
|
9879
|
+
import { join as join27 } from "node:path";
|
|
9909
9880
|
|
|
9910
9881
|
// src/services/distributed/assemble.ts
|
|
9911
9882
|
import {
|
|
@@ -9916,11 +9887,11 @@ import {
|
|
|
9916
9887
|
readFileSync as readFileSync11,
|
|
9917
9888
|
readdirSync as readdirSync8,
|
|
9918
9889
|
renameSync as renameSync7,
|
|
9919
|
-
rmSync as
|
|
9890
|
+
rmSync as rmSync12,
|
|
9920
9891
|
statSync as statSync7,
|
|
9921
9892
|
writeFileSync as writeFileSync9
|
|
9922
9893
|
} from "node:fs";
|
|
9923
|
-
import { dirname as dirname12, join as
|
|
9894
|
+
import { dirname as dirname12, join as join25 } from "node:path";
|
|
9924
9895
|
import {
|
|
9925
9896
|
appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
|
|
9926
9897
|
describeFfmpegFailure as describeFfmpegFailure4,
|
|
@@ -9982,7 +9953,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
9982
9953
|
};
|
|
9983
9954
|
try {
|
|
9984
9955
|
const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
|
|
9985
|
-
const finalPath =
|
|
9956
|
+
const finalPath = join25(workDir, `output.${format}`);
|
|
9986
9957
|
const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
|
|
9987
9958
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
9988
9959
|
renameSync7(finalPath, outputPath);
|
|
@@ -9993,7 +9964,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
9993
9964
|
});
|
|
9994
9965
|
} finally {
|
|
9995
9966
|
try {
|
|
9996
|
-
|
|
9967
|
+
rmSync12(workDir, { recursive: true, force: true });
|
|
9997
9968
|
} catch (err) {
|
|
9998
9969
|
log.warn("[assemble] failed to remove work dir", {
|
|
9999
9970
|
workDir,
|
|
@@ -10010,8 +9981,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
10010
9981
|
};
|
|
10011
9982
|
}
|
|
10012
9983
|
function readAssemblePlan(planDir, chunkPaths) {
|
|
10013
|
-
const planJsonPath =
|
|
10014
|
-
const chunksJsonPath =
|
|
9984
|
+
const planJsonPath = join25(planDir, "plan.json");
|
|
9985
|
+
const chunksJsonPath = join25(planDir, "meta", "chunks.json");
|
|
10015
9986
|
if (!existsSync18(planJsonPath)) {
|
|
10016
9987
|
throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
|
|
10017
9988
|
}
|
|
@@ -10043,7 +10014,7 @@ function videoInputArgs(ctx, videoSources) {
|
|
|
10043
10014
|
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
10044
10015
|
}
|
|
10045
10016
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
10046
|
-
const cfrOutputPath =
|
|
10017
|
+
const cfrOutputPath = join25(ctx.workDir, `cfr.${ctx.format}`);
|
|
10047
10018
|
const cfrArgs = [
|
|
10048
10019
|
...videoInputArgs(ctx, chunkPaths),
|
|
10049
10020
|
"-c:v",
|
|
@@ -10081,7 +10052,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
10081
10052
|
);
|
|
10082
10053
|
return;
|
|
10083
10054
|
}
|
|
10084
|
-
const paddedAudioPath =
|
|
10055
|
+
const paddedAudioPath = join25(ctx.workDir, "audio-padded.m4a");
|
|
10085
10056
|
const padTrimResult = await ctx.timed(
|
|
10086
10057
|
"audioMs",
|
|
10087
10058
|
() => padOrTrimAudioToVideoFrameCount({
|
|
@@ -10121,7 +10092,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
10121
10092
|
}
|
|
10122
10093
|
}
|
|
10123
10094
|
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
10124
|
-
const listPath =
|
|
10095
|
+
const listPath = join25(workDir, "concat-list.txt");
|
|
10125
10096
|
const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
|
|
10126
10097
|
writeFileSync9(listPath, `${body}
|
|
10127
10098
|
`, "utf-8");
|
|
@@ -10149,6 +10120,7 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
10149
10120
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
10150
10121
|
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
10151
10122
|
if (!chunksCarryAudio && !separateAudio) return false;
|
|
10123
|
+
if (planFormat(planDir) !== "mp4") return false;
|
|
10152
10124
|
if (cfr) {
|
|
10153
10125
|
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
|
|
10154
10126
|
}
|
|
@@ -10157,8 +10129,12 @@ function chunksAreStitchedMp4(planDir, cfr) {
|
|
|
10157
10129
|
function stitchesSeparateAudio(planDir) {
|
|
10158
10130
|
return lockedAudio(planDir).separateAudio === true;
|
|
10159
10131
|
}
|
|
10132
|
+
function planFormat(planDir) {
|
|
10133
|
+
const plan2 = JSON.parse(readFileSync11(join25(planDir, "plan.json"), "utf-8"));
|
|
10134
|
+
return plan2.dimensions.format;
|
|
10135
|
+
}
|
|
10160
10136
|
function lockedAudio(planDir) {
|
|
10161
|
-
const encoderJsonPath =
|
|
10137
|
+
const encoderJsonPath = join25(planDir, "meta", "encoder.json");
|
|
10162
10138
|
if (!existsSync18(encoderJsonPath)) return {};
|
|
10163
10139
|
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
10164
10140
|
}
|
|
@@ -10168,7 +10144,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
10168
10144
|
`[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.`
|
|
10169
10145
|
);
|
|
10170
10146
|
}
|
|
10171
|
-
const encoderJsonPath =
|
|
10147
|
+
const encoderJsonPath = join25(planDir, "meta", "encoder.json");
|
|
10172
10148
|
if (!existsSync18(encoderJsonPath)) {
|
|
10173
10149
|
throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
|
|
10174
10150
|
}
|
|
@@ -10181,7 +10157,7 @@ function readCfrEncoder(planDir, format) {
|
|
|
10181
10157
|
return encoderJson;
|
|
10182
10158
|
}
|
|
10183
10159
|
function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
|
|
10184
|
-
if (existsSync18(outputPath))
|
|
10160
|
+
if (existsSync18(outputPath)) rmSync12(outputPath, { recursive: true, force: true });
|
|
10185
10161
|
mkdirSync13(outputPath, { recursive: true });
|
|
10186
10162
|
let globalIdx = 0;
|
|
10187
10163
|
for (const chunkDir of chunkPaths) {
|
|
@@ -10195,8 +10171,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10195
10171
|
throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
|
|
10196
10172
|
}
|
|
10197
10173
|
for (const frame of frames) {
|
|
10198
|
-
const dst =
|
|
10199
|
-
cpSync3(
|
|
10174
|
+
const dst = join25(outputPath, formatExportFrameName(globalIdx, "png"));
|
|
10175
|
+
cpSync3(join25(chunkDir, frame), dst);
|
|
10200
10176
|
globalIdx += 1;
|
|
10201
10177
|
}
|
|
10202
10178
|
}
|
|
@@ -10206,13 +10182,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10206
10182
|
);
|
|
10207
10183
|
}
|
|
10208
10184
|
if (audioPath !== null && existsSync18(audioPath)) {
|
|
10209
|
-
const sidecar =
|
|
10185
|
+
const sidecar = join25(outputPath, MIXED_AUDIO_FILENAME4);
|
|
10210
10186
|
cpSync3(audioPath, sidecar);
|
|
10211
10187
|
}
|
|
10212
10188
|
let fileSize = 0;
|
|
10213
10189
|
for (const name of readdirSync8(outputPath)) {
|
|
10214
10190
|
try {
|
|
10215
|
-
fileSize += statSync7(
|
|
10191
|
+
fileSize += statSync7(join25(outputPath, name)).size;
|
|
10216
10192
|
} catch {
|
|
10217
10193
|
}
|
|
10218
10194
|
}
|
|
@@ -10226,8 +10202,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10226
10202
|
|
|
10227
10203
|
// src/services/distributed/renderChunk.ts
|
|
10228
10204
|
import { randomBytes } from "node:crypto";
|
|
10229
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as
|
|
10230
|
-
import { join as
|
|
10205
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "node:fs";
|
|
10206
|
+
import { join as join26 } from "node:path";
|
|
10231
10207
|
import {
|
|
10232
10208
|
assertSwiftShader,
|
|
10233
10209
|
BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
|
|
@@ -10329,7 +10305,7 @@ function hashChunkOutput(outputPath, kind) {
|
|
|
10329
10305
|
if (kind === "file") return sha256Hex(readFileSync12(outputPath));
|
|
10330
10306
|
const entries = readdirSync9(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
|
|
10331
10307
|
const lines = entries.map(
|
|
10332
|
-
(name) => `${name}\0${sha256Hex(readFileSync12(
|
|
10308
|
+
(name) => `${name}\0${sha256Hex(readFileSync12(join26(outputPath, name)))}`
|
|
10333
10309
|
);
|
|
10334
10310
|
return sha256Hex(lines.join("\0"));
|
|
10335
10311
|
}
|
|
@@ -10346,9 +10322,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
|
|
|
10346
10322
|
async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
10347
10323
|
const start = Date.now();
|
|
10348
10324
|
const log = defaultLogger;
|
|
10349
|
-
const planJsonPath =
|
|
10350
|
-
const encoderJsonPath =
|
|
10351
|
-
const chunksJsonPath =
|
|
10325
|
+
const planJsonPath = join26(planDir, "plan.json");
|
|
10326
|
+
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
10327
|
+
const chunksJsonPath = join26(planDir, "meta", "chunks.json");
|
|
10352
10328
|
if (!existsSync19(planJsonPath)) {
|
|
10353
10329
|
throw new RenderChunkValidationError(
|
|
10354
10330
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -10374,7 +10350,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10374
10350
|
}
|
|
10375
10351
|
const encoder = JSON.parse(readFileSync12(encoderJsonPath, "utf-8"));
|
|
10376
10352
|
const chunks = JSON.parse(readFileSync12(chunksJsonPath, "utf-8"));
|
|
10377
|
-
const videosJsonPath =
|
|
10353
|
+
const videosJsonPath = join26(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
|
|
10378
10354
|
let planVideos = null;
|
|
10379
10355
|
if (existsSync19(videosJsonPath)) {
|
|
10380
10356
|
try {
|
|
@@ -10406,7 +10382,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10406
10382
|
}
|
|
10407
10383
|
const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
|
|
10408
10384
|
const framesInChunk = range.endFrame - range.startFrame;
|
|
10409
|
-
const compiledDir =
|
|
10385
|
+
const compiledDir = join26(planDir, "compiled");
|
|
10410
10386
|
if (!existsSync19(compiledDir)) {
|
|
10411
10387
|
throw new RenderChunkValidationError(
|
|
10412
10388
|
MISSING_PLAN_ARTIFACT,
|
|
@@ -10464,7 +10440,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10464
10440
|
};
|
|
10465
10441
|
const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
10466
10442
|
mkdirSync14(workDir, { recursive: true });
|
|
10467
|
-
const framesDir =
|
|
10443
|
+
const framesDir = join26(workDir, "captured-frames");
|
|
10468
10444
|
mkdirSync14(framesDir, { recursive: true });
|
|
10469
10445
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
10470
10446
|
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
@@ -10489,7 +10465,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10489
10465
|
dimensions: plan2.dimensions,
|
|
10490
10466
|
encoder,
|
|
10491
10467
|
videoCount: planVideos?.videos.length ?? 0,
|
|
10492
|
-
html: readFileSync12(
|
|
10468
|
+
html: readFileSync12(join26(compiledDir, "index.html"), "utf-8"),
|
|
10493
10469
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
10494
10470
|
skipReadinessVideoIds: deferredVideos.filter(
|
|
10495
10471
|
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
@@ -10665,7 +10641,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10665
10641
|
return observedMode;
|
|
10666
10642
|
},
|
|
10667
10643
|
resetForScreenshotRetry: () => {
|
|
10668
|
-
|
|
10644
|
+
rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10669
10645
|
mkdirSync14(framesDir, { recursive: true });
|
|
10670
10646
|
capturePerfs.length = 0;
|
|
10671
10647
|
captureAttempts.length = 0;
|
|
@@ -10703,7 +10679,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10703
10679
|
if (isPngSequence) {
|
|
10704
10680
|
if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
|
|
10705
10681
|
} else {
|
|
10706
|
-
const outDir =
|
|
10682
|
+
const outDir = join26(outputChunkPath, "..");
|
|
10707
10683
|
if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
|
|
10708
10684
|
}
|
|
10709
10685
|
const encodeStarted = Date.now();
|
|
@@ -10758,8 +10734,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10758
10734
|
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
10759
10735
|
timeoutMs: cfg.ffmpegProcessTimeout
|
|
10760
10736
|
});
|
|
10761
|
-
|
|
10762
|
-
|
|
10737
|
+
rmSync13(videoOnlyPath, { force: true });
|
|
10738
|
+
rmSync13(audioScratchPath, { force: true });
|
|
10763
10739
|
}
|
|
10764
10740
|
encodeStageMs = Date.now() - encodeStarted;
|
|
10765
10741
|
} finally {
|
|
@@ -10805,8 +10781,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10805
10781
|
writeFileSync10(perfPath, `${JSON.stringify(perfPayload, null, 2)}
|
|
10806
10782
|
`, "utf-8");
|
|
10807
10783
|
try {
|
|
10808
|
-
|
|
10809
|
-
|
|
10784
|
+
rmSync13(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10785
|
+
rmSync13(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
10810
10786
|
} catch (err) {
|
|
10811
10787
|
log.warn("[renderChunk] failed to remove work dir", {
|
|
10812
10788
|
workDir,
|
|
@@ -10835,18 +10811,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10835
10811
|
|
|
10836
10812
|
// src/services/distributed/planV2Execution.ts
|
|
10837
10813
|
async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
|
|
10838
|
-
const workRoot = mkdtempSync7(
|
|
10839
|
-
const materializedPlanDir =
|
|
10814
|
+
const workRoot = mkdtempSync7(join27(tmpdir2(), "hf-plan-v2-chunk-"));
|
|
10815
|
+
const materializedPlanDir = join27(workRoot, "plan");
|
|
10840
10816
|
try {
|
|
10841
10817
|
materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
|
|
10842
10818
|
return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
|
|
10843
10819
|
} finally {
|
|
10844
|
-
|
|
10820
|
+
rmSync14(workRoot, { recursive: true, force: true });
|
|
10845
10821
|
}
|
|
10846
10822
|
}
|
|
10847
10823
|
async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
10848
|
-
const workRoot = mkdtempSync7(
|
|
10849
|
-
const materializedPlanDir =
|
|
10824
|
+
const workRoot = mkdtempSync7(join27(tmpdir2(), "hf-plan-v2-assembler-"));
|
|
10825
|
+
const materializedPlanDir = join27(workRoot, "plan");
|
|
10850
10826
|
try {
|
|
10851
10827
|
const materialized = materializePlanV2Target(
|
|
10852
10828
|
planV2Dir,
|
|
@@ -10861,7 +10837,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
10861
10837
|
options
|
|
10862
10838
|
);
|
|
10863
10839
|
} finally {
|
|
10864
|
-
|
|
10840
|
+
rmSync14(workRoot, { recursive: true, force: true });
|
|
10865
10841
|
}
|
|
10866
10842
|
}
|
|
10867
10843
|
|
|
@@ -10908,6 +10884,9 @@ function validateDistributedRenderConfig(config) {
|
|
|
10908
10884
|
}
|
|
10909
10885
|
validateIntDimension("config.width", config.width);
|
|
10910
10886
|
validateIntDimension("config.height", config.height);
|
|
10887
|
+
if (config.pageMotionBlur !== void 0 && config.pageMotionBlur !== "off") {
|
|
10888
|
+
throw new InvalidConfigError("config.pageMotionBlur", 'must be "off" when provided');
|
|
10889
|
+
}
|
|
10911
10890
|
if (!ALLOWED_FORMATS.includes(config.format)) {
|
|
10912
10891
|
throw new InvalidConfigError(
|
|
10913
10892
|
"config.format",
|
|
@@ -11110,7 +11089,7 @@ function validateIntDimension(field, value) {
|
|
|
11110
11089
|
// src/services/distributed/projectHash.ts
|
|
11111
11090
|
import { closeSync as closeSync5, openSync as openSync5, readdirSync as readdirSync10, readSync as readSync3, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
|
|
11112
11091
|
import { createHash as createHash6 } from "node:crypto";
|
|
11113
|
-
import { join as
|
|
11092
|
+
import { join as join28 } from "node:path";
|
|
11114
11093
|
var SITE_ID_SCHEME = "site-v2\0";
|
|
11115
11094
|
var READ_CHUNK_BYTES = 256 * 1024;
|
|
11116
11095
|
function listSiteFiles(projectDir) {
|
|
@@ -11119,7 +11098,7 @@ function listSiteFiles(projectDir) {
|
|
|
11119
11098
|
function walk(dir, rel) {
|
|
11120
11099
|
for (const name of readdirSync10(dir).sort()) {
|
|
11121
11100
|
if (rel === "" && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(name)) continue;
|
|
11122
|
-
const full =
|
|
11101
|
+
const full = join28(dir, name);
|
|
11123
11102
|
const relPath = rel === "" ? name : `${rel}/${name}`;
|
|
11124
11103
|
let stat;
|
|
11125
11104
|
try {
|
|
@@ -11149,7 +11128,7 @@ function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
|
|
|
11149
11128
|
for (const rel of files) {
|
|
11150
11129
|
hash.update(rel);
|
|
11151
11130
|
hash.update("\0");
|
|
11152
|
-
const fd = openSync5(
|
|
11131
|
+
const fd = openSync5(join28(projectDir, rel), "r");
|
|
11153
11132
|
try {
|
|
11154
11133
|
let read;
|
|
11155
11134
|
while ((read = readSync3(fd, chunk, 0, chunk.length, null)) > 0) {
|